АРЧ ЮИ - остановка прогона через гуй

This commit is contained in:
2026-04-18 18:14:30 +03:00
parent 9710d81676
commit 12550606bb
39 changed files with 1783 additions and 93 deletions
+65 -12
View File
@@ -355,7 +355,7 @@ function readReportedCaseIds(job) {
return output;
}
function isTerminalCaseStatus(status) {
return status === "completed" || status === "failed";
return status === "completed" || status === "failed" || status === "canceled";
}
function syncJobWithSessions(job) {
if (!job.run_id || !job.eval_target.startsWith("assistant_")) {
@@ -473,6 +473,7 @@ function buildEvalRouter(services) {
throw new http_1.ApiError("ASYNC_CASESET_EMPTY", "No runnable cases found in selected case-set.", 400);
}
const nowIso = new Date().toISOString();
const abortController = new AbortController();
const job = {
job_id: jobId,
status: "queued",
@@ -491,7 +492,8 @@ function buildEvalRouter(services) {
messages: []
})),
error: null,
report: null
report: null,
abort_controller: abortController
};
ASYNC_JOBS.set(job.job_id, job);
trimAsyncJobsStore();
@@ -500,25 +502,48 @@ function buildEvalRouter(services) {
const target = ASYNC_JOBS.get(job.job_id);
if (!target)
return;
if (target.status === "canceled") {
return;
}
target.status = "running";
target.updated_at = new Date().toISOString();
try {
const report = await services.evalService.run({
...payload,
caseSetFile: runtimeCaseSetFile,
runId
runId,
abortSignal: abortController.signal
});
target.report = report;
syncJobWithSessions(target);
target.completed_cases = target.total_cases;
target.status = "completed";
target.updated_at = new Date().toISOString();
const latestAfterRun = ASYNC_JOBS.get(job.job_id);
if (!latestAfterRun) {
return;
}
if (latestAfterRun.status === "canceled") {
latestAfterRun.updated_at = new Date().toISOString();
return;
}
latestAfterRun.report = report;
syncJobWithSessions(latestAfterRun);
latestAfterRun.completed_cases = latestAfterRun.total_cases;
latestAfterRun.status = "completed";
latestAfterRun.updated_at = new Date().toISOString();
latestAfterRun.abort_controller = null;
}
catch (error) {
syncJobWithSessions(target);
target.status = "failed";
target.error = error instanceof Error ? error.message : String(error);
target.updated_at = new Date().toISOString();
const latestAfterError = ASYNC_JOBS.get(job.job_id);
if (!latestAfterError) {
return;
}
if (latestAfterError.status === "canceled") {
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
return;
}
syncJobWithSessions(latestAfterError);
latestAfterError.status = "failed";
latestAfterError.error = error instanceof Error ? error.message : String(error);
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
}
})();
});
@@ -552,5 +577,33 @@ function buildEvalRouter(services) {
next(error);
}
});
router.post("/api/eval/run-async/:job_id/cancel", (req, res, next) => {
try {
const jobId = String(req.params.job_id ?? "").trim();
if (!jobId) {
throw new http_1.ApiError("INVALID_ASYNC_JOB_ID", "job_id is required.", 400);
}
const job = ASYNC_JOBS.get(jobId);
if (!job) {
throw new http_1.ApiError("ASYNC_JOB_NOT_FOUND", `Async eval job not found: ${jobId}`, 404);
}
if (!isTerminalCaseStatus(job.status)) {
job.status = "canceled";
job.error = "Остановлено оператором.";
job.updated_at = new Date().toISOString();
job.abort_controller?.abort();
job.abort_controller = null;
job.cases = job.cases.map((item) => item.status === "completed" ? item : { ...item, status: "canceled" });
syncJobWithSessions(job);
}
(0, http_1.ok)(res, {
ok: true,
job: snapshotJob(job)
});
}
catch (error) {
next(error);
}
});
return router;
}
@@ -56,6 +56,7 @@ async function runAssistantLivingChatLlmRuntime(input) {
apiKey: String(input.payload.apiKey ?? input.defaultApiKey ?? ""),
model: String(input.payload.model ?? input.defaultModel),
baseUrl: input.payload.baseUrl ?? input.defaultBaseUrl,
abortSignal: input.payload.abortSignal,
temperature,
maxOutputTokens
}, {
@@ -72,6 +72,9 @@ function hasPriorAssistantTurn(items) {
}
return items.some((item) => item && typeof item === "object" && item.role === "assistant");
}
function buildDeterministicSmalltalkLeadReply() {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function findLastAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
@@ -295,9 +298,11 @@ async function runAssistantLivingChatRuntime(input) {
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [chatText, proactiveOffer].filter((part) => String(part ?? "").trim().length > 0).join(" ");
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "llm_chat_with_proactive_scope_offer";
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
@@ -8,7 +8,7 @@ function createAssistantLivingModePolicy(deps) {
return /(база|док|документ|проводк|контрагент|договор|контракт|счет|сч[её]т|остат|сальдо|хвост|платеж|плат[её]ж|операц|поставщик|клиент|заказчик|дебитор|кредитор|оборот|баланс|период|месяц|год|инн|аванс|предоплат|отгруз|задолж|долг|склад|товар|номенклат|материал|mcp|bank|counterparty|contract|document|ledger|posting|account|organization|company|advance|prepay|shipment|receivab|payab|warehouse|inventory|stock|item|организац|компан|контор|фирм)/i.test(lower);
}
function hasDataRetrievalRequestSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
const lower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!lower) {
return false;
}
@@ -248,20 +248,33 @@ function createAssistantLivingModePolicy(deps) {
return hasCapabilitySignal && !hasRetrievalSignal;
}
function hasLivingChatSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
if (!lower) {
const rawLower = compactWhitespace(String(text ?? "").toLowerCase());
const repairedLower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
const variants = [rawLower, repairedLower].filter((value, index, items) => value.length > 0 && items.indexOf(value) === index);
if (variants.length === 0) {
return false;
}
if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) {
const hasDirectionalReply = variants.some((value) => /^(?:\u0430\s+)?(?:\u0442\u0443\u0442|\u0437\u0434\u0435\u0441\u044c|\u0442\u0430\u043c|\u0441\u044e\u0434\u0430|\u0442\u0443\u0434\u0430)[\s!?.,:;\-]*$/iu.test(value));
if (hasDirectionalReply) {
return true;
}
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
const hasShortGreeting = variants.some((value) => /^(?:\u0430\u0433\u0430|\u0443\u0433\u0443|\u043e\u043a|\u043e\u043a\u0435\u0439|\u044f\u0441\u043d\u043e|\u043f\u043e\u043d\u044f\u043b(?:\u0430)?|\u043f\u0440\u0438\u043d\u044f\u0442\u043e|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044e|\u0441\u0443\u043f\u0435\u0440|\u043a\u043b\u0430\u0441\u0441|\u043d\u043e\u0440\u043c|\u0433\u043e|\u0434\u0430\u0432\u0430\u0439|\u043f\u043e\u0433\u043d\u0430\u043b\u0438|\u043f\u0440\u0438\u0432\u0435\u0442(?:\u0438\u043a)?|\u0445\u0430\u0439|\u0439\u043e|yo|\u0447[её]\s+\u0442\u0430\u043c|\u0447[её]\s+\u043a\u0430\u043a|hello|hi|thanks?)$/iu.test(value));
if (hasShortGreeting) {
return true;
}
if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) {
const hasGreetingWithFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u043f\u0440\u0438\u0432\u0435\u0442\p{L}*|\u0445\u0430\u0439|\u0439\u043e|hello|hi|yo)(?=$|[\s,.!?;:()\-]).*(?:^|[\s,.!?;:()\-])(?:\u043a\u0430\u043a|\u0434\u0435\u043b\u0430|\u0442\u0430\u043c|\u0447[её])(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasGreetingWithFollowup) {
return true;
}
return hasSmallTalkSignal(lower);
const hasColloquialFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u0447[её]\s+\u043a\u0430\u043a|\u0447[её]\s+\u0442\u0430\u043c|\u043a\u0430\u043a\s+\u0442\u0430\u043c(?:\s+\u0434\u0435\u043b\u0430)?)(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasColloquialFollowup) {
return true;
}
const hasOpenSmalltalkPrompt = variants.some((value) => /(?:\u043a\u0430\u043a\s+\u0434\u0435\u043b\u0430|\u043a\u0430\u043a\s+\u0442\u044b|\u0447\u0442\u043e\s+\u043d\u043e\u0432\u043e\u0433\u043e|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438\s+\u043e\s+\u0441\u0435\u0431\u0435|\u0447\u0435\u043c\s+\u043c\u043e\u0436\u0435\u0448\u044c\s+\u043f\u043e\u043c\u043e\u0447\u044c|\u0434\u0430\u0432\u0430\u0439\s+\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043e\u0431\u0441\u0443\u0434\u0438\u043c|\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439|\u0447\u0442\u043e\s+\u0434\u0443\u043c\u0430\u0435\u0448\u044c)/iu.test(value));
if (hasOpenSmalltalkPrompt) {
return true;
}
return variants.some((value) => hasSmallTalkSignal(value));
}
function resolveLivingAssistantModeDecision(input) {
const userMessage = String(input?.userMessage ?? "");
@@ -3335,6 +3335,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
apiKey: payload?.apiKey,
model: payload?.model,
baseUrl: payload?.baseUrl,
abortSignal: payload?.abortSignal,
temperature: 0,
maxOutputTokens: payload?.maxOutputTokens,
promptVersion: "normalizer_v2_0_2",
+14 -1
View File
@@ -15,6 +15,11 @@ const http_1 = require("../utils/http");
const assistantService_1 = require("./assistantService");
const assistantSessionStore_1 = require("./assistantSessionStore");
const files_1 = require("../utils/files");
function throwIfAborted(signal) {
if (signal?.aborted) {
throw new Error("EVAL_RUN_ABORTED");
}
}
const BASELINE_METRICS = {
schema_validation_pass_rate: 100,
intent_class_accuracy: 72.73,
@@ -1601,17 +1606,20 @@ class EvalService {
const diagnostics = [];
let requestsTotal = 0;
for (const suiteCase of suiteCases) {
throwIfAborted(payload.abortSignal);
const sessionId = `${runId}-${suiteCase.case_id}`;
const turnResponses = [];
const notes = [];
const limitations = [];
try {
for (const turn of suiteCase.turns) {
throwIfAborted(payload.abortSignal);
const response = (await assistantService.handleMessage({
session_id: sessionId,
user_message: turn.user_message,
message: turn.user_message,
mode: "assistant",
abortSignal: payload.abortSignal,
llmProvider: payload.normalizeConfig.llmProvider,
apiKey: payload.normalizeConfig.apiKey,
model: payload.normalizeConfig.model,
@@ -1636,10 +1644,14 @@ class EvalService {
}));
turnResponses.push(response);
requestsTotal += 1;
throwIfAborted(payload.abortSignal);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage === "EVAL_RUN_ABORTED") {
throw error;
}
diagnostics.push({
suite_case: suiteCase,
session_id: sessionId,
@@ -2254,7 +2266,8 @@ class EvalService {
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
runId: payload.runId,
abortSignal: payload.abortSignal
});
}
if (evalTarget === "assistant_stage2") {
@@ -1363,6 +1363,7 @@ class NormalizerService {
apiKey: String(apiKey ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens
}, {
@@ -1408,6 +1409,7 @@ class NormalizerService {
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens: retryMaxOutputTokens
}, {
@@ -408,7 +408,8 @@ class OpenAIResponsesClient {
response = await fetch(url, {
method,
headers,
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined,
signal: config.abortSignal
});
}
catch (error) {
+69 -13
View File
@@ -8,7 +8,7 @@ import { ApiError, ok } from "../utils/http";
import type { EvalRunMode, NormalizeRequestPayload } from "../types/normalizer";
import type { EvalTarget } from "../types/assistantEval";
type EvalAsyncStatus = "queued" | "running" | "completed" | "failed";
type EvalAsyncStatus = "queued" | "running" | "completed" | "failed" | "canceled";
interface EvalAsyncCaseInfo {
case_id: string;
@@ -41,6 +41,7 @@ interface EvalAsyncJob {
cases: EvalAsyncCaseInfo[];
error: string | null;
report: Record<string, unknown> | null;
abort_controller?: AbortController | null;
}
const ASYNC_JOBS = new Map<string, EvalAsyncJob>();
@@ -428,7 +429,7 @@ function readReportedCaseIds(job: EvalAsyncJob): Set<string> {
}
function isTerminalCaseStatus(status: EvalAsyncStatus): boolean {
return status === "completed" || status === "failed";
return status === "completed" || status === "failed" || status === "canceled";
}
function syncJobWithSessions(job: EvalAsyncJob): void {
@@ -559,6 +560,7 @@ export function buildEvalRouter(services: AppServices): Router {
}
const nowIso = new Date().toISOString();
const abortController = new AbortController();
const job: EvalAsyncJob = {
job_id: jobId,
status: "queued",
@@ -577,7 +579,8 @@ export function buildEvalRouter(services: AppServices): Router {
messages: []
})),
error: null,
report: null
report: null,
abort_controller: abortController
};
ASYNC_JOBS.set(job.job_id, job);
trimAsyncJobsStore();
@@ -586,24 +589,47 @@ export function buildEvalRouter(services: AppServices): Router {
void (async () => {
const target = ASYNC_JOBS.get(job.job_id);
if (!target) return;
if (target.status === "canceled") {
return;
}
target.status = "running";
target.updated_at = new Date().toISOString();
try {
const report = await services.evalService.run({
...payload,
caseSetFile: runtimeCaseSetFile,
runId
runId,
abortSignal: abortController.signal
});
target.report = report;
syncJobWithSessions(target);
target.completed_cases = target.total_cases;
target.status = "completed";
target.updated_at = new Date().toISOString();
const latestAfterRun = ASYNC_JOBS.get(job.job_id);
if (!latestAfterRun) {
return;
}
if (latestAfterRun.status === "canceled") {
latestAfterRun.updated_at = new Date().toISOString();
return;
}
latestAfterRun.report = report;
syncJobWithSessions(latestAfterRun);
latestAfterRun.completed_cases = latestAfterRun.total_cases;
latestAfterRun.status = "completed";
latestAfterRun.updated_at = new Date().toISOString();
latestAfterRun.abort_controller = null;
} catch (error) {
syncJobWithSessions(target);
target.status = "failed";
target.error = error instanceof Error ? error.message : String(error);
target.updated_at = new Date().toISOString();
const latestAfterError = ASYNC_JOBS.get(job.job_id);
if (!latestAfterError) {
return;
}
if (latestAfterError.status === "canceled") {
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
return;
}
syncJobWithSessions(latestAfterError);
latestAfterError.status = "failed";
latestAfterError.error = error instanceof Error ? error.message : String(error);
latestAfterError.updated_at = new Date().toISOString();
latestAfterError.abort_controller = null;
}
})();
});
@@ -638,5 +664,35 @@ export function buildEvalRouter(services: AppServices): Router {
}
});
router.post("/api/eval/run-async/:job_id/cancel", (req, res, next) => {
try {
const jobId = String(req.params.job_id ?? "").trim();
if (!jobId) {
throw new ApiError("INVALID_ASYNC_JOB_ID", "job_id is required.", 400);
}
const job = ASYNC_JOBS.get(jobId);
if (!job) {
throw new ApiError("ASYNC_JOB_NOT_FOUND", `Async eval job not found: ${jobId}`, 404);
}
if (!isTerminalCaseStatus(job.status)) {
job.status = "canceled";
job.error = "Остановлено оператором.";
job.updated_at = new Date().toISOString();
job.abort_controller?.abort();
job.abort_controller = null;
job.cases = job.cases.map((item) =>
item.status === "completed" ? item : { ...item, status: "canceled" }
);
syncJobWithSessions(job);
}
ok(res, {
ok: true,
job: snapshotJob(job)
});
} catch (error) {
next(error);
}
});
return router;
}
@@ -25,6 +25,7 @@ export interface RunAssistantLivingChatLlmRuntimeInput {
apiKey?: unknown;
model?: unknown;
baseUrl?: unknown;
abortSignal?: AbortSignal;
temperature?: number;
maxOutputTokens?: number;
};
@@ -35,6 +36,7 @@ export interface RunAssistantLivingChatLlmRuntimeInput {
apiKey: string;
model: string;
baseUrl: unknown;
abortSignal?: AbortSignal;
temperature: number;
maxOutputTokens: number;
},
@@ -104,6 +106,7 @@ export async function runAssistantLivingChatLlmRuntime(
apiKey: String(input.payload.apiKey ?? input.defaultApiKey ?? ""),
model: String(input.payload.model ?? input.defaultModel),
baseUrl: input.payload.baseUrl ?? input.defaultBaseUrl,
abortSignal: input.payload.abortSignal,
temperature,
maxOutputTokens
},
@@ -144,6 +144,10 @@ function hasPriorAssistantTurn(items: unknown[]): boolean {
return items.some((item) => item && typeof item === "object" && (item as { role?: string }).role === "assistant");
}
function buildDeterministicSmalltalkLeadReply(): string {
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
}
function findLastAddressDebug(items: unknown[]): Record<string, unknown> | null {
if (!Array.isArray(items)) {
return null;
@@ -392,9 +396,11 @@ export async function runAssistantLivingChatRuntime(
}
const proactiveOffer = input.buildAssistantProactiveOrganizationOfferReply(proactiveScopeProbe);
if (proactiveOffer) {
chatText = [chatText, proactiveOffer].filter((part) => String(part ?? "").trim().length > 0).join(" ");
chatText = [buildDeterministicSmalltalkLeadReply(), proactiveOffer]
.filter((part) => String(part ?? "").trim().length > 0)
.join(" ");
livingChatProactiveScopeOfferApplied = true;
livingChatSource = "llm_chat_with_proactive_scope_offer";
livingChatSource = "deterministic_smalltalk_with_proactive_scope_offer";
if (!dataScopeProbe) {
dataScopeProbe = proactiveScopeProbe;
}
@@ -68,7 +68,7 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
}
function hasDataRetrievalRequestSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
const lower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!lower) {
return false;
}
@@ -319,20 +319,33 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
}
function hasLivingChatSignal(text) {
const lower = compactWhitespace(String(text ?? "").toLowerCase());
if (!lower) {
const rawLower = compactWhitespace(String(text ?? "").toLowerCase());
const repairedLower = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
const variants = [rawLower, repairedLower].filter((value, index, items) => value.length > 0 && items.indexOf(value) === index);
if (variants.length === 0) {
return false;
}
if (/^(?:а\s+)?(?:тут|здесь|там|сюда|туда)[\s!?.,:;\-]*$/iu.test(lower)) {
const hasDirectionalReply = variants.some((value) => /^(?:\u0430\s+)?(?:\u0442\u0443\u0442|\u0437\u0434\u0435\u0441\u044c|\u0442\u0430\u043c|\u0441\u044e\u0434\u0430|\u0442\u0443\u0434\u0430)[\s!?.,:;\-]*$/iu.test(value));
if (hasDirectionalReply) {
return true;
}
if (/^(ага|угу|ок|окей|ясно|понял|поняла|принято|спасибо|благодарю|супер|класс|норм|го|давай|погнали|привет|хай|йо|yo|че\s+там|ч[её]\s+как|че\s+как|hello|hi|thanks?)$/i.test(lower)) {
const hasShortGreeting = variants.some((value) => /^(?:\u0430\u0433\u0430|\u0443\u0433\u0443|\u043e\u043a|\u043e\u043a\u0435\u0439|\u044f\u0441\u043d\u043e|\u043f\u043e\u043d\u044f\u043b(?:\u0430)?|\u043f\u0440\u0438\u043d\u044f\u0442\u043e|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u0431\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044e|\u0441\u0443\u043f\u0435\u0440|\u043a\u043b\u0430\u0441\u0441|\u043d\u043e\u0440\u043c|\u0433\u043e|\u0434\u0430\u0432\u0430\u0439|\u043f\u043e\u0433\u043d\u0430\u043b\u0438|\u043f\u0440\u0438\u0432\u0435\u0442(?:\u0438\u043a)?|\u0445\u0430\u0439|\u0439\u043e|yo|\u0447[её]\s+\u0442\u0430\u043c|\u0447[её]\s+\u043a\u0430\u043a|hello|hi|thanks?)$/iu.test(value));
if (hasShortGreeting) {
return true;
}
if (/(как дела|как ты|что нового|расскажи о себе|чем можешь помочь|давай поговорим|поговорим|обсудим|посоветуй|что думаешь)/i.test(lower)) {
const hasGreetingWithFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u043f\u0440\u0438\u0432\u0435\u0442\p{L}*|\u0445\u0430\u0439|\u0439\u043e|hello|hi|yo)(?=$|[\s,.!?;:()\-]).*(?:^|[\s,.!?;:()\-])(?:\u043a\u0430\u043a|\u0434\u0435\u043b\u0430|\u0442\u0430\u043c|\u0447[её])(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasGreetingWithFollowup) {
return true;
}
return hasSmallTalkSignal(lower);
const hasColloquialFollowup = variants.some((value) => /(?:^|[\s,.!?;:()\-])(?:\u0447[её]\s+\u043a\u0430\u043a|\u0447[её]\s+\u0442\u0430\u043c|\u043a\u0430\u043a\s+\u0442\u0430\u043c(?:\s+\u0434\u0435\u043b\u0430)?)(?=$|[\s,.!?;:()\-])/iu.test(value));
if (hasColloquialFollowup) {
return true;
}
const hasOpenSmalltalkPrompt = variants.some((value) => /(?:\u043a\u0430\u043a\s+\u0434\u0435\u043b\u0430|\u043a\u0430\u043a\s+\u0442\u044b|\u0447\u0442\u043e\s+\u043d\u043e\u0432\u043e\u0433\u043e|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438\s+\u043e\s+\u0441\u0435\u0431\u0435|\u0447\u0435\u043c\s+\u043c\u043e\u0436\u0435\u0448\u044c\s+\u043f\u043e\u043c\u043e\u0447\u044c|\u0434\u0430\u0432\u0430\u0439\s+\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c|\u043e\u0431\u0441\u0443\u0434\u0438\u043c|\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439|\u0447\u0442\u043e\s+\u0434\u0443\u043c\u0430\u0435\u0448\u044c)/iu.test(value));
if (hasOpenSmalltalkPrompt) {
return true;
}
return variants.some((value) => hasSmallTalkSignal(value));
}
function resolveLivingAssistantModeDecision(input) {
@@ -3290,6 +3290,7 @@ async function runAddressLlmPreDecompose(normalizerService, payload, userMessage
apiKey: payload?.apiKey,
model: payload?.model,
baseUrl: payload?.baseUrl,
abortSignal: payload?.abortSignal,
temperature: 0,
maxOutputTokens: payload?.maxOutputTokens,
promptVersion: "normalizer_v2_0_2",
@@ -54,6 +54,12 @@ import { AssistantSessionStore } from "./assistantSessionStore";
import { NormalizerService } from "./normalizerService";
import { ensureDir, writeJsonFile } from "../utils/files";
function throwIfAborted(signal?: AbortSignal): void {
if (signal?.aborted) {
throw new Error("EVAL_RUN_ABORTED");
}
}
interface EvalCaseFile {
case_id: string;
raw_question: string;
@@ -1925,6 +1931,7 @@ export class EvalService {
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
abortSignal?: AbortSignal;
}): Promise<Record<string, unknown>> {
if (!FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1) {
throw new ApiError(
@@ -1943,6 +1950,7 @@ export class EvalService {
let requestsTotal = 0;
for (const suiteCase of suiteCases) {
throwIfAborted(payload.abortSignal);
const sessionId = `${runId}-${suiteCase.case_id}`;
const turnResponses: AssistantMessageResponsePayload[] = [];
const notes: string[] = [];
@@ -1950,11 +1958,13 @@ export class EvalService {
try {
for (const turn of suiteCase.turns) {
throwIfAborted(payload.abortSignal);
const response = (await assistantService.handleMessage({
session_id: sessionId,
user_message: turn.user_message,
message: turn.user_message,
mode: "assistant",
abortSignal: payload.abortSignal,
llmProvider: payload.normalizeConfig.llmProvider,
apiKey: payload.normalizeConfig.apiKey,
model: payload.normalizeConfig.model,
@@ -1979,9 +1989,13 @@ export class EvalService {
})) as AssistantMessageResponsePayload;
turnResponses.push(response);
requestsTotal += 1;
throwIfAborted(payload.abortSignal);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage === "EVAL_RUN_ABORTED") {
throw error;
}
diagnostics.push({
suite_case: suiteCase,
session_id: sessionId,
@@ -2634,6 +2648,7 @@ export class EvalService {
compareWithReportFile?: string;
analysisDate?: string;
runId?: string;
abortSignal?: AbortSignal;
}): Promise<Record<string, unknown>> {
const mode = payload.mode ?? "standard";
const evalTarget = payload.evalTarget ?? "normalizer";
@@ -2648,7 +2663,8 @@ export class EvalService {
caseSetFile: payload.caseSetFile,
compareWithReportFile: payload.compareWithReportFile,
analysisDate: analysisDate ?? undefined,
runId: payload.runId
runId: payload.runId,
abortSignal: payload.abortSignal
});
}
@@ -1608,6 +1608,7 @@ export class NormalizerService {
apiKey: String(apiKey ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens
},
@@ -1656,6 +1657,7 @@ export class NormalizerService {
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
model,
baseUrl,
abortSignal: payload.abortSignal,
temperature,
maxOutputTokens: retryMaxOutputTokens
},
@@ -9,6 +9,7 @@ export interface OpenAIRequestConfig {
apiKey: string;
model: string;
baseUrl?: string;
abortSignal?: AbortSignal;
temperature?: number;
maxOutputTokens?: number;
}
@@ -491,7 +492,8 @@ export class OpenAIResponsesClient {
response = await fetch(url, {
method,
headers,
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined
body: method === "POST" ? JSON.stringify(payload ?? {}) : undefined,
signal: config.abortSignal
});
} catch (error) {
lastNetworkError = error;
@@ -325,6 +325,7 @@ export interface AssistantMessageRequestPayload {
user_message?: string;
message?: string;
mode?: "assistant" | string;
abortSignal?: AbortSignal;
llmProvider?: NormalizeRequestPayload["llmProvider"];
apiKey?: string;
model?: string;
@@ -267,6 +267,7 @@ export interface NormalizeRequestPayload {
apiKey?: string;
model?: string;
baseUrl?: string;
abortSignal?: AbortSignal;
temperature?: number;
maxOutputTokens?: number;
promptVersion?: PromptVersion | string;
@@ -155,9 +155,10 @@ describe("assistant living chat runtime adapter", () => {
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("llm-text");
expect(output.chatText).toContain("Привет! Всё нормально.");
expect(output.chatText).toContain("offer:ООО Альтернатива Плюс, ООО Лайсвуд, РАЙМ");
expect(output.debug?.living_chat_response_source).toBe("llm_chat_with_proactive_scope_offer");
expect(output.chatText).not.toContain("llm-text");
expect(output.debug?.living_chat_response_source).toBe("deterministic_smalltalk_with_proactive_scope_offer");
expect(output.debug?.living_chat_proactive_scope_offer_applied).toBe(true);
expect(output.debug?.living_chat_data_scope_probe_org_count).toBe(3);
expect(output.debug?.assistant_known_organizations).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]);
@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import { createAssistantLivingModePolicy } from "../src/services/assistantLivingModePolicy";
function buildPolicy() {
function buildPolicy(options?: { repairAddressMojibake?: (text: string) => string }) {
return createAssistantLivingModePolicy({
featureAssistantLivingChatRouterV1: true,
compactWhitespace: (text: string) => text.replace(/\s+/g, " ").trim(),
repairAddressMojibake: (text: string) => text,
repairAddressMojibake: options?.repairAddressMojibake ?? ((text: string) => text),
toNonEmptyString: (value: unknown) => {
if (value === null || value === undefined) {
return null;
@@ -34,6 +34,8 @@ function buildPolicy() {
}
describe("assistantLivingModePolicy", () => {
const colloquialGreeting = "\u043f\u0440\u0438\u0432\u0435\u0442\u0438\u043a - \u0447\u0435 \u043a\u0430\u043a \u0442\u0430\u043c \u0434\u0435\u043b\u0430";
it("detects explicit recap wording as memory signal even when selected-object words are present", () => {
const policy = buildPolicy();
@@ -63,6 +65,21 @@ describe("assistantLivingModePolicy", () => {
expect(decision.reason).toBe("living_chat_signal_detected");
});
it("detects colloquial greeting with extra words as living chat signal", () => {
const policy = buildPolicy();
expect(policy.hasLivingChatSignal(colloquialGreeting)).toBe(true);
});
it("detects mojibake colloquial greeting after repair", () => {
const policy = buildPolicy({
repairAddressMojibake: (text: string) =>
text === "приветик - че как там дела" ? colloquialGreeting : text
});
expect(policy.hasLivingChatSignal("приветик - че как там дела")).toBe(true);
});
it("keeps explicit inventory question in deep mode", () => {
const policy = buildPolicy();
@@ -1,4 +1,90 @@
[
{
"generation_id": "gen-ag04181425-56e29e",
"created_at": "2026-04-18T14:25:37+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"count": 20,
"domain": "address_phase12_wider_saved_session_pool",
"questions": [
"приветик - че как там дела",
"Альтернатива Плюс",
"расскажи что можешь интересного",
"кайф - что там на складе по остаткам?",
"а исторические остатки на другие даты умеешь?",
"март 2016",
"По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"а кому продали?",
"ндс можешь прикинуть на дату покупки рабочей станции?",
"кто у нас самый доходный клиент за все время",
"кто нам должен денег на май 2017",
"а какой ндс мы должны примерно заплатить за этот период?",
"мы должны комуто денег на сегодня?",
"а нам?",
"ты умеешь считать дельту по договорам?",
"по чепурнову покажи все доки",
"а по свк",
"хвосты покажи по счету 60 на август 2022",
"Есть ли остатки товара, которые закупались очень давно",
"а по Альтернативе Плюс сколько лет активности в базе 1С?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260418142537_gen-ag04181425-56e29e.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260418142537_gen-ag04181425-56e29e.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Broad AGENT replay built from the repaired manual session contour and extended with explicit company selection. The scenario validates proactive multi-company entry, capability meta honesty, inventory roots and history, selected-object carryover, VAT bridges and period carryover, receivables/payables polarity flip, counterparty document follow-ups, account 60 tails, old-purchase inventory aging, and organization activity age in one long live session.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase12_wider_saved_session_pool.json",
"scenario_id": "address_truth_harness_phase12_wider_saved_session_pool",
"semantic_tags": [
"aggregate_revenue",
"bridge_inventory_to_vat",
"capability_over_followup",
"company_authority",
"company_selected",
"counterparty_followup",
"counterparty_root",
"cross_domain_pivot",
"date_carryover",
"display_label_integrity",
"documents",
"historical_date_anchor",
"human_answer_quality",
"inventory_aging",
"inventory_provenance",
"inventory_root",
"inventory_sale_trace",
"late_session_stability",
"meta_capability",
"meta_historical_capability",
"meta_smalltalk",
"organization_activity_age",
"organization_authority",
"proactive_scope_offer",
"same_date_restore",
"same_period_restore",
"selected_object",
"settlements_account_60",
"settlements_mirror_followup",
"settlements_payables",
"settlements_receivables",
"tail_authority_proof",
"today_scope",
"vat_followup"
],
"latest_acceptance": null
}
},
{
"generation_id": "gen-ag04171508-760111",
"created_at": "2026-04-17T15:08:06+00:00",
@@ -389,12 +475,13 @@
"created_at": "2026-04-16T18:26:26.191Z",
"mode": "saved_user_sessions",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"count": 31,
"count": 33,
"domain": null,
"questions": [
"приветик - че как там дела",
"расскажи что можешь интересного",
"кайф - что там на складе по остаткам?",
"АЛЬТЕРНАТИВА",
"а исторические остатки на другие даты умеешь?",
"давай на июль 2017",
"март 2016",
@@ -422,7 +509,8 @@
"хвосты покажи по счету 60 на август 2022",
"Есть ли остатки товара, которые закупались очень давно",
"Какие конкретно номенклатуры формируют остаток по складу на май 2020",
"а по Альтернативе Плюс сколько лет активности в базе 1С?"
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
"Как ты оценишь деятельность компании?"
],
"generated_by": "manual_reviewer",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260416182626_gen-mo1t93wq-jy0453e.json",
@@ -0,0 +1,307 @@
{
"saved_at": "2026-04-18T14:25:37+00:00",
"generation_id": "gen-ag04181425-56e29e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"agent_run": true,
"questions": [
"приветик - че как там дела",
"Альтернатива Плюс",
"расскажи что можешь интересного",
"кайф - что там на складе по остаткам?",
"а исторические остатки на другие даты умеешь?",
"март 2016",
"По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"а кому продали?",
"ндс можешь прикинуть на дату покупки рабочей станции?",
"кто у нас самый доходный клиент за все время",
"кто нам должен денег на май 2017",
"а какой ндс мы должны примерно заплатить за этот период?",
"мы должны комуто денег на сегодня?",
"а нам?",
"ты умеешь считать дельту по договорам?",
"по чепурнову покажи все доки",
"а по свк",
"хвосты покажи по счету 60 на август 2022",
"Есть ли остатки товара, которые закупались очень давно",
"а по Альтернативе Плюс сколько лет активности в базе 1С?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Broad AGENT replay built from the repaired manual session contour and extended with explicit company selection. The scenario validates proactive multi-company entry, capability meta honesty, inventory roots and history, selected-object carryover, VAT bridges and period carryover, receivables/payables polarity flip, counterparty document follow-ups, account 60 tails, old-purchase inventory aging, and organization activity age in one long live session.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase12_wider_saved_session_pool.json",
"scenario_id": "address_truth_harness_phase12_wider_saved_session_pool",
"semantic_tags": [
"aggregate_revenue",
"bridge_inventory_to_vat",
"capability_over_followup",
"company_authority",
"company_selected",
"counterparty_followup",
"counterparty_root",
"cross_domain_pivot",
"date_carryover",
"display_label_integrity",
"documents",
"historical_date_anchor",
"human_answer_quality",
"inventory_aging",
"inventory_provenance",
"inventory_root",
"inventory_sale_trace",
"late_session_stability",
"meta_capability",
"meta_historical_capability",
"meta_smalltalk",
"organization_activity_age",
"organization_authority",
"proactive_scope_offer",
"same_date_restore",
"same_period_restore",
"selected_object",
"settlements_account_60",
"settlements_mirror_followup",
"settlements_payables",
"settlements_receivables",
"tail_authority_proof",
"today_scope",
"vat_followup"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "приветик - че как там дела",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "Альтернатива Плюс",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "расскажи что можешь интересного",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "кайф - что там на складе по остаткам?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а исторические остатки на другие даты умеешь?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "март 2016",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "а кому продали?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "ндс можешь прикинуть на дату покупки рабочей станции?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "кто у нас самый доходный клиент за все время",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "кто нам должен денег на май 2017",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "а какой ндс мы должны примерно заплатить за этот период?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "мы должны комуто денег на сегодня?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-014",
"role": "user",
"text": "а нам?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-015",
"role": "user",
"text": "ты умеешь считать дельту по договорам?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-016",
"role": "user",
"text": "по чепурнову покажи все доки",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-017",
"role": "user",
"text": "а по свк",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-018",
"role": "user",
"text": "хвосты покажи по счету 60 на август 2022",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-019",
"role": "user",
"text": "Есть ли остатки товара, которые закупались очень давно",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-020",
"role": "user",
"text": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"created_at": "2026-04-18T14:25:37+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Broad AGENT replay built from the repaired manual session contour and extended with explicit company selection. The scenario validates proactive multi-company entry, capability meta honesty, inventory roots and history, selected-object carryover, VAT bridges and period carryover, receivables/payables polarity flip, counterparty document follow-ups, account 60 tails, old-purchase inventory aging, and organization activity age in one long live session.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase12_wider_saved_session_pool.json",
"scenario_id": "address_truth_harness_phase12_wider_saved_session_pool",
"semantic_tags": [
"aggregate_revenue",
"bridge_inventory_to_vat",
"capability_over_followup",
"company_authority",
"company_selected",
"counterparty_followup",
"counterparty_root",
"cross_domain_pivot",
"date_carryover",
"display_label_integrity",
"documents",
"historical_date_anchor",
"human_answer_quality",
"inventory_aging",
"inventory_provenance",
"inventory_root",
"inventory_sale_trace",
"late_session_stability",
"meta_capability",
"meta_historical_capability",
"meta_smalltalk",
"organization_activity_age",
"organization_authority",
"proactive_scope_offer",
"same_date_restore",
"same_period_restore",
"selected_object",
"settlements_account_60",
"settlements_mirror_followup",
"settlements_payables",
"settlements_receivables",
"tail_authority_proof",
"today_scope",
"vat_followup"
]
}
}
}
@@ -2,7 +2,7 @@
"suite_id": "assistant_saved_session_gen-mo1t93wq-jy0453e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-18T11:29:49.384Z",
"generated_at": "2026-04-18T14:55:46.799Z",
"generation_id": "gen-mo1t93wq-jy0453e",
"mode": "saved_user_sessions",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
@@ -27,6 +27,9 @@
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
@@ -110,6 +113,9 @@
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
@@ -0,0 +1,85 @@
{
"suite_id": "assistant_saved_session_gen-ag04181425-56e29e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-18T14:25:37+00:00",
"generation_id": "gen-ag04181425-56e29e",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"domain": "address_phase12_wider_saved_session_pool",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 12 wider saved-session replay for multi-trajectory organization authority",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "Альтернатива Плюс"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
}
]
}
]
}
@@ -0,0 +1,120 @@
{
"suite_id": "assistant_saved_session_runtime_job-IXCkeIOaTz",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}
@@ -0,0 +1,117 @@
{
"suite_id": "assistant_saved_session_runtime_job-W3Bqj1Q5Ar",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}
@@ -0,0 +1,36 @@
{
"suite_id": "assistant_saved_session_runtime_job-lHMIky2oa-",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "AGENT | Phase 6 provider/runtime replay across chat, meta, and address boundaries",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "AGENT | Phase 6 provider/runtime replay across chat, meta, and address boundaries",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет, как дела?"
},
{
"user_message": "по какой компании мы сейчас работаем?"
},
{
"user_message": "что ты можешь по 1С?"
},
{
"user_message": "какие остатки на складе на март 2021"
},
{
"user_message": "а исторические остатки тоже можешь?"
}
]
}
]
}
@@ -0,0 +1,117 @@
{
"suite_id": "assistant_saved_session_runtime_job-uh5xR80M-d",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}
@@ -1,6 +1,6 @@
{
"schema_version": "shared_llm_connection_v1",
"updated_at": "2026-04-17T10:55:35.860Z",
"updated_at": "2026-04-18T14:51:55.518Z",
"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
+2 -2
View File
@@ -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-CLCo6iY2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DkvyfMZP.css">
<script type="module" crossorigin src="/assets/index-DWgwfAPc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-sbJz56_k.css">
</head>
<body>
<div id="root"></div>
@@ -270,6 +270,12 @@ export const apiClient = {
return request(`/eval/run-async/${encodeURIComponent(jobId)}`);
},
async cancelEvalRunAsync(jobId: string): Promise<AsyncEvalRunStatusResponse> {
return request(`/eval/run-async/${encodeURIComponent(jobId)}/cancel`, {
method: "POST"
});
},
async startRun(): Promise<{ ok: boolean; run: RuntimeRun; runId: string; sessionId: string; status: string }> {
return request("/accounting-agent/v1/runs/start", {
method: "POST",
@@ -612,6 +612,14 @@ function CardLaunchIcon() {
);
}
function CardStopIcon() {
return (
<svg className="autoruns-card-stop-svg" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<rect x="4.2" y="4.2" width="7.6" height="7.6" rx="0.8" />
</svg>
);
}
function GroupChevronIcon({ expanded }: { expanded: boolean }) {
return (
<svg className={expanded ? "autoruns-group-chevron-svg expanded" : "autoruns-group-chevron-svg"} viewBox="0 0 16 16" aria-hidden="true" focusable="false">
@@ -702,6 +710,8 @@ export function AutoRunsHistoryPanel({
const [limitInput, setLimitInput] = useState(String(DEFAULT_FILTERS.limit));
const [autogenCountInput, setAutogenCountInput] = useState(String(DEFAULT_AUTOGEN_SETTINGS.count));
const [autogenPersonalityPromptHeight, setAutogenPersonalityPromptHeight] = useState(160);
const [runningAutogenGenerationId, setRunningAutogenGenerationId] = useState("");
const [autogenStopBusy, setAutogenStopBusy] = useState(false);
const [filtersGroupExpanded, setFiltersGroupExpanded] = useState(true);
const [generationContextGroupExpanded, setGenerationContextGroupExpanded] = useState(true);
const [autogenGroupExpanded, setAutogenGroupExpanded] = useState(true);
@@ -1507,6 +1517,8 @@ export function AutoRunsHistoryPanel({
if (payload.job.status === "completed") {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
const finalRunId = payload.job.report_summary?.run_id ?? payload.job.run_id;
await loadHistory({
keepSelection: true,
@@ -1521,11 +1533,25 @@ export function AutoRunsHistoryPanel({
if (payload.job.status === "failed") {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
setErrorText(`Запуск прогонов: ${payload.job.error ?? "неизвестная ошибка"}`);
log(`Autogen async run failed: ${payload.job.error ?? "unknown error"}`);
return;
}
if (payload.job.status === "canceled") {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
setActiveAsyncJob(null);
await loadHistory({ keepSelection: false });
await loadAutoGenHistory();
log(`Autogen async run canceled: job=${payload.job.job_id}`);
return;
}
stopAsyncJobPolling();
asyncJobPollTimerRef.current = window.setTimeout(() => {
void pollAsyncJobStatus(jobId);
@@ -1533,6 +1559,8 @@ export function AutoRunsHistoryPanel({
} catch (error) {
stopAsyncJobPolling();
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
const message = error instanceof Error ? error.message : String(error);
setErrorText(`Запуск прогонов: ${message}`);
log(`Autogen async status error: ${message}`);
@@ -1579,6 +1607,8 @@ export function AutoRunsHistoryPanel({
});
const liveJob = payload.job;
setRunningAutogenGenerationId(generation.generation_id);
setAutogenStopBusy(false);
setActiveAsyncJob(liveJob);
const liveRunId = toLiveRunId(liveJob.job_id);
const live = buildLiveRunDetail(liveJob, ALL_CASES_ID);
@@ -1602,6 +1632,8 @@ export function AutoRunsHistoryPanel({
setErrorText(`Запуск прогонов: ${message}`);
log(`Autogen run error: ${message}`);
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
}
}, [
analysisDate,
@@ -1616,6 +1648,36 @@ export function AutoRunsHistoryPanel({
stopAsyncJobPolling
]);
const stopAutogenCampaign = useCallback(async () => {
const jobId = activeAsyncJob?.job_id ?? "";
if (!jobId) {
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
setActiveAsyncJob(null);
stopAsyncJobPolling();
return;
}
setAutogenStopBusy(true);
setErrorText("");
try {
const payload = await apiClient.cancelEvalRunAsync(jobId);
stopAsyncJobPolling();
setActiveAsyncJob(null);
setAutogenRunBusy(false);
setAutogenStopBusy(false);
setRunningAutogenGenerationId("");
await loadHistory({ keepSelection: false });
await loadAutoGenHistory();
log(`Autogen async run stopped: job=${payload.job.job_id}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAutogenStopBusy(false);
setErrorText(`Остановка прогона: ${message}`);
log(`Autogen stop error: ${message}`);
}
}, [activeAsyncJob, loadAutoGenHistory, loadHistory, log, stopAsyncJobPolling]);
const openCommentModal = useCallback((message: AutoRunDialogMessage) => {
if (message.role !== "assistant") return;
const resolvedCaseId = message.case_id ?? selectedCaseId;
@@ -2750,10 +2812,13 @@ export function AutoRunsHistoryPanel({
type="button"
className="autoruns-run-launch-btn"
style={isSavedUserSessionsMode ? { display: "none" } : undefined}
disabled={autogenRunBusy || editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration}
onClick={() => void runAutogenCampaign()}
disabled={
autogenStopBusy ||
(!autogenRunBusy && (editableGeneratedQuestions.length === 0 || !selectedAutogenGeneration))
}
onClick={() => void (autogenRunBusy ? stopAutogenCampaign() : runAutogenCampaign())}
>
{autogenRunBusy ? "Запускаю..." : "Запустить прогон"}
{autogenRunBusy ? (autogenStopBusy ? "Останавливаю..." : "Остановить прогон") : "Запустить прогон"}
</button>
</div>
@@ -2955,7 +3020,10 @@ export function AutoRunsHistoryPanel({
{isSavedUserSessionsMode ? "Сохраненные пользовательские сессии пока пусты." : "История автогенераций пока пустая."}
</p>
) : null}
{visibleAutoGenHistory.slice(0, 30).map((item) => (
{visibleAutoGenHistory.slice(0, 30).map((item) => {
const isRunningThisGeneration = autogenRunBusy && runningAutogenGenerationId === item.generation_id;
const isAnotherGenerationRunning = autogenRunBusy && runningAutogenGenerationId !== item.generation_id;
return (
<article
key={item.generation_id}
className={[
@@ -2971,20 +3039,24 @@ export function AutoRunsHistoryPanel({
<button
type="button"
className="autoruns-saved-session-icon-btn"
disabled={autogenRunBusy}
disabled={autogenStopBusy || isAnotherGenerationRunning}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
if (isRunningThisGeneration) {
void stopAutogenCampaign();
return;
}
setSelectedAutogenGenerationId(item.generation_id);
void runAutogenCampaign(
item,
selectedAutogenGenerationId === item.generation_id ? editableGeneratedQuestions : item.questions
);
}}
title="Запустить прогон"
aria-label={`Запустить прогон для ${formatAutoGenGenerationTitle(item)}`}
title={isRunningThisGeneration ? "Остановить прогон" : "Запустить прогон"}
aria-label={`${isRunningThisGeneration ? "Остановить прогон" : "Запустить прогон"} для ${formatAutoGenGenerationTitle(item)}`}
>
<CardLaunchIcon />
{isRunningThisGeneration ? <CardStopIcon /> : <CardLaunchIcon />}
</button>
<button
type="button"
@@ -3182,7 +3254,8 @@ export function AutoRunsHistoryPanel({
</>
) : null}
</article>
))}
);
})}
</div>
) : null}
+2 -2
View File
@@ -392,13 +392,13 @@ export interface AutoGenPersonalityCatalogResponse {
export interface AsyncEvalRunCase {
case_id: string;
turns_total: number;
status: "queued" | "running" | "completed" | "failed";
status: "queued" | "running" | "completed" | "failed" | "canceled";
messages: AutoRunDialogMessage[];
}
export interface AsyncEvalRunJob {
job_id: string;
status: "queued" | "running" | "completed" | "failed";
status: "queued" | "running" | "completed" | "failed" | "canceled";
created_at: string;
updated_at: string;
eval_target: AutoRunTarget;
+7
View File
@@ -1627,6 +1627,13 @@ button:disabled {
stroke: none;
}
.autoruns-card-stop-svg {
width: 14px;
height: 14px;
fill: currentColor;
stroke: none;
}
.autoruns-card-chevron-svg.expanded {
transform: rotate(180deg);
}