АРЧ - UI и domain loop: синхронизировать локальную LLM-модель через shared config + КВИН 3
This commit is contained in:
parent
bc381c012e
commit
8866176be6
|
|
@ -41,8 +41,8 @@
|
||||||
"buyer_candidate": "Департамент капитального ремонта города Москвы"
|
"buyer_candidate": "Департамент капитального ремонта города Москвы"
|
||||||
},
|
},
|
||||||
"question_pool": {
|
"question_pool": {
|
||||||
"total_questions": 30,
|
"total_questions": 31,
|
||||||
"core_questions_total": 21,
|
"core_questions_total": 22,
|
||||||
"followup_checkpoints_total": 9,
|
"followup_checkpoints_total": 9,
|
||||||
"questions": [
|
"questions": [
|
||||||
{
|
{
|
||||||
|
|
@ -279,6 +279,15 @@
|
||||||
"wording_family": "ui_selected_object_colloquial",
|
"wording_family": "ui_selected_object_colloquial",
|
||||||
"semantic_goal": "проверить разговорный selected-object follow-up про поставщика без слова `поставщик`"
|
"semantic_goal": "проверить разговорный selected-object follow-up про поставщика без слова `поставщик`"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"question_id": "Q31",
|
||||||
|
"text": "По выбранному объекту \"...\": у кого куплено",
|
||||||
|
"layer": "selected_item_provenance",
|
||||||
|
"node_id": "N03_selected_item_supplier",
|
||||||
|
"role": "critical_child",
|
||||||
|
"wording_family": "ui_selected_object_colloquial",
|
||||||
|
"semantic_goal": "проверить selected-object follow-up с краткой страдательной формой `у кого куплено`, который должен оставаться в supplier provenance и наследовать историческую дату root-среза"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"question_id": "Q29",
|
"question_id": "Q29",
|
||||||
"text": "По выбранному объекту \"...\": где мы купили это",
|
"text": "По выбранному объекту \"...\": где мы купили это",
|
||||||
|
|
@ -1032,6 +1041,51 @@
|
||||||
"organization_scope"
|
"organization_scope"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"step_id": "step_03bb_selected_item_supplier_u_kogo_kupleno",
|
||||||
|
"question_id": "Q31",
|
||||||
|
"node_id": "N03_selected_item_supplier",
|
||||||
|
"node_role": "critical_child",
|
||||||
|
"paraphrase_family": "ui_selected_object_colloquial",
|
||||||
|
"title": "Selected item supplier passive colloquial wording",
|
||||||
|
"question": "По выбранному объекту \"{{bindings.focus_item_historical}}\": у кого куплено",
|
||||||
|
"depends_on": [
|
||||||
|
"step_01_snapshot_historical"
|
||||||
|
],
|
||||||
|
"analysis_context": {
|
||||||
|
"as_of_date": "2019-03-31",
|
||||||
|
"source": "binding_target_date_historical"
|
||||||
|
},
|
||||||
|
"expected_capability": "inventory_purchase_provenance_for_item",
|
||||||
|
"forbidden_capabilities": [
|
||||||
|
"confirmed_inventory_on_hand_as_of_date"
|
||||||
|
],
|
||||||
|
"forbidden_recipes": [
|
||||||
|
"address_inventory_on_hand_as_of_date_v1"
|
||||||
|
],
|
||||||
|
"required_state_objects": [
|
||||||
|
"focus_object"
|
||||||
|
],
|
||||||
|
"required_carryover_invariants": [
|
||||||
|
"selected_object",
|
||||||
|
"focus_object",
|
||||||
|
"date_scope",
|
||||||
|
"warehouse_scope",
|
||||||
|
"organization_scope"
|
||||||
|
],
|
||||||
|
"required_filters": {
|
||||||
|
"as_of_date": "2019-03-31",
|
||||||
|
"period_from": "2019-03-01",
|
||||||
|
"period_to": "2019-03-31"
|
||||||
|
},
|
||||||
|
"invariant_severity": {
|
||||||
|
"wrong_as_of_date": "P0",
|
||||||
|
"wrong_period_from": "P0",
|
||||||
|
"wrong_period_to": "P0",
|
||||||
|
"forbidden_route_selected": "P0",
|
||||||
|
"focus_object_missing": "P0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"step_id": "step_03c_selected_item_supplier_gde_my_kupili",
|
"step_id": "step_03c_selected_item_supplier_gde_my_kupili",
|
||||||
"question_id": "Q29",
|
"question_id": "Q29",
|
||||||
|
|
@ -1852,7 +1906,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern_id": "F12b_selected_object_supplier_u_kogo_kupili_misroute",
|
"pattern_id": "F12b_selected_object_supplier_u_kogo_kupili_misroute",
|
||||||
"symptom": "selected-object follow-up such as `у кого купили` stays on the root stock snapshot instead of switching to selected-item provenance",
|
"symptom": "selected-object follow-up such as `у кого купили` or `у кого куплено` stays on the root stock snapshot instead of switching to selected-item provenance",
|
||||||
"defect_class": "followup_action_resolution_gap"
|
"defect_class": "followup_action_resolution_gap"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1 = exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = exports.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1 = exports.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 = exports.FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
|
exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_HARD_GUARD_V1 = exports.FEATURE_ASSISTANT_ROUTE_EXPECTATION_AUDIT_V1 = exports.FEATURE_ASSISTANT_ROUTE_SHADOW_PAYABLES_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_HEURISTIC_V1 = exports.FEATURE_ASSISTANT_ROUTE_RECEIVABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_PAYABLES_CONFIRMED_V1 = exports.FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1 = exports.FEATURE_ASSISTANT_ROUTE_DRILLDOWN_V1 = exports.FEATURE_ASSISTANT_ROUTE_ADDRESS_GENERIC_V1 = exports.FEATURE_ASSISTANT_CAPABILITY_ROUTE_GUARD_V1 = exports.FEATURE_ASSISTANT_ADDRESS_NAVIGATION_STATE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
|
||||||
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_ANNOTATIONS_FILE = exports.ASSISTANT_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = void 0;
|
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.SHARED_LLM_CONNECTION_FILE = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_ANNOTATIONS_FILE = exports.ASSISTANT_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = void 0;
|
||||||
const path_1 = __importDefault(require("path"));
|
const path_1 = __importDefault(require("path"));
|
||||||
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
|
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
|
||||||
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
|
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
|
||||||
|
|
@ -90,6 +90,7 @@ exports.AUTORUN_ANNOTATIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "auto
|
||||||
exports.AUTORUN_ANNOTATIONS_FILE = path_1.default.resolve(exports.AUTORUN_ANNOTATIONS_DIR, "annotations.json");
|
exports.AUTORUN_ANNOTATIONS_FILE = path_1.default.resolve(exports.AUTORUN_ANNOTATIONS_DIR, "annotations.json");
|
||||||
exports.AUTORUN_GENERATOR_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_generators");
|
exports.AUTORUN_GENERATOR_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_generators");
|
||||||
exports.AUTORUN_GENERATOR_HISTORY_FILE = path_1.default.resolve(exports.AUTORUN_GENERATOR_DIR, "history.json");
|
exports.AUTORUN_GENERATOR_HISTORY_FILE = path_1.default.resolve(exports.AUTORUN_GENERATOR_DIR, "history.json");
|
||||||
|
exports.SHARED_LLM_CONNECTION_FILE = path_1.default.resolve(exports.DATA_DIR, "shared_llm_connection.json");
|
||||||
exports.PROMPTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "prompts");
|
exports.PROMPTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "prompts");
|
||||||
exports.REPORTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "reports");
|
exports.REPORTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "reports");
|
||||||
exports.EVAL_DATASETS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "eval_cases");
|
exports.EVAL_DATASETS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "eval_cases");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.buildSharedLlmConfigRouter = buildSharedLlmConfigRouter;
|
||||||
|
const express_1 = require("express");
|
||||||
|
const config_1 = require("../config");
|
||||||
|
const http_1 = require("../utils/http");
|
||||||
|
const files_1 = require("../utils/files");
|
||||||
|
const path_1 = __importDefault(require("path"));
|
||||||
|
function sanitizeString(value, fallback) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : fallback;
|
||||||
|
}
|
||||||
|
function sanitizeNumber(value, fallback) {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && value.trim().length > 0) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isFinite(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
function buildFallbackRecord() {
|
||||||
|
return {
|
||||||
|
schema_version: "shared_llm_connection_v1",
|
||||||
|
updated_at: "",
|
||||||
|
connection: {
|
||||||
|
llmProvider: "local",
|
||||||
|
model: "qwen2.5-14b-instruct-1m",
|
||||||
|
baseUrl: "http://127.0.0.1:1234/v1",
|
||||||
|
temperature: 0,
|
||||||
|
maxOutputTokens: 900
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function loadSharedRecord() {
|
||||||
|
const fallback = buildFallbackRecord();
|
||||||
|
const parsed = (0, files_1.readJsonFile)(config_1.SHARED_LLM_CONNECTION_FILE, null);
|
||||||
|
if (!parsed || typeof parsed !== "object" || !("connection" in parsed) || !parsed.connection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const connection = parsed.connection;
|
||||||
|
return {
|
||||||
|
schema_version: "shared_llm_connection_v1",
|
||||||
|
updated_at: sanitizeString(parsed.updated_at, ""),
|
||||||
|
connection: {
|
||||||
|
llmProvider: connection.llmProvider === "local" ? "local" : "openai",
|
||||||
|
model: sanitizeString(connection.model, fallback.connection.model),
|
||||||
|
baseUrl: sanitizeString(connection.baseUrl, fallback.connection.baseUrl),
|
||||||
|
temperature: sanitizeNumber(connection.temperature, fallback.connection.temperature),
|
||||||
|
maxOutputTokens: Math.max(1, Math.trunc(sanitizeNumber(connection.maxOutputTokens, fallback.connection.maxOutputTokens)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function buildSharedLlmConfigRouter() {
|
||||||
|
const router = (0, express_1.Router)();
|
||||||
|
router.get("/api/llm/shared-connection", (_req, res, next) => {
|
||||||
|
try {
|
||||||
|
const record = loadSharedRecord();
|
||||||
|
(0, http_1.ok)(res, {
|
||||||
|
ok: true,
|
||||||
|
connection: record?.connection ?? null,
|
||||||
|
updated_at: record?.updated_at ?? null,
|
||||||
|
exists: Boolean(record)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.post("/api/llm/shared-connection", (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const body = (req.body ?? {});
|
||||||
|
const fallback = buildFallbackRecord();
|
||||||
|
const record = {
|
||||||
|
schema_version: "shared_llm_connection_v1",
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
connection: {
|
||||||
|
llmProvider: body.llmProvider === "local" ? "local" : "openai",
|
||||||
|
model: sanitizeString(body.model, fallback.connection.model),
|
||||||
|
baseUrl: sanitizeString(body.baseUrl, fallback.connection.baseUrl),
|
||||||
|
temperature: sanitizeNumber(body.temperature, fallback.connection.temperature),
|
||||||
|
maxOutputTokens: Math.max(1, Math.trunc(sanitizeNumber(body.maxOutputTokens, fallback.connection.maxOutputTokens)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(0, files_1.ensureDir)(path_1.default.dirname(config_1.SHARED_LLM_CONNECTION_FILE));
|
||||||
|
(0, files_1.writeJsonFile)(config_1.SHARED_LLM_CONNECTION_FILE, record);
|
||||||
|
(0, http_1.ok)(res, {
|
||||||
|
ok: true,
|
||||||
|
connection: record.connection,
|
||||||
|
updated_at: record.updated_at
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ const eval_1 = require("./routes/eval");
|
||||||
const history_1 = require("./routes/history");
|
const history_1 = require("./routes/history");
|
||||||
const normalize_1 = require("./routes/normalize");
|
const normalize_1 = require("./routes/normalize");
|
||||||
const presets_1 = require("./routes/presets");
|
const presets_1 = require("./routes/presets");
|
||||||
|
const sharedLlmConfig_1 = require("./routes/sharedLlmConfig");
|
||||||
const testConnection_1 = require("./routes/testConnection");
|
const testConnection_1 = require("./routes/testConnection");
|
||||||
const inMemoryRuntimeAdapter_1 = require("./runtime/inMemoryRuntimeAdapter");
|
const inMemoryRuntimeAdapter_1 = require("./runtime/inMemoryRuntimeAdapter");
|
||||||
const assistantService_1 = require("./services/assistantService");
|
const assistantService_1 = require("./services/assistantService");
|
||||||
|
|
@ -59,6 +60,7 @@ function createApp() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.use((0, testConnection_1.buildTestConnectionRouter)(openaiClient));
|
app.use((0, testConnection_1.buildTestConnectionRouter)(openaiClient));
|
||||||
|
app.use((0, sharedLlmConfig_1.buildSharedLlmConfigRouter)());
|
||||||
app.use((0, normalize_1.buildNormalizeRouter)(services));
|
app.use((0, normalize_1.buildNormalizeRouter)(services));
|
||||||
app.use((0, eval_1.buildEvalRouter)(services));
|
app.use((0, eval_1.buildEvalRouter)(services));
|
||||||
app.use((0, assistant_1.buildAssistantRouter)(services));
|
app.use((0, assistant_1.buildAssistantRouter)(services));
|
||||||
|
|
|
||||||
|
|
@ -1345,7 +1345,7 @@ function hasSelectedObjectInventoryCue(text) {
|
||||||
}
|
}
|
||||||
function hasSelectedObjectInventoryProvenanceSignal(text) {
|
function hasSelectedObjectInventoryProvenanceSignal(text) {
|
||||||
return (hasSelectedObjectInventoryCue(text) &&
|
return (hasSelectedObjectInventoryCue(text) &&
|
||||||
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(text));
|
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(text));
|
||||||
}
|
}
|
||||||
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
|
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
|
||||||
return (hasSelectedObjectInventoryCue(text) &&
|
return (hasSelectedObjectInventoryCue(text) &&
|
||||||
|
|
@ -1353,7 +1353,7 @@ function hasSelectedObjectInventoryPurchaseDocumentsSignal(text) {
|
||||||
}
|
}
|
||||||
function hasInventoryProvenanceSignalV2(text) {
|
function hasInventoryProvenanceSignalV2(text) {
|
||||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
|
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
|
||||||
const hasSupplierCue = /(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(text);
|
const hasSupplierCue = /(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(text);
|
||||||
const hasPurchaseCue = /(?:куплен(?:ы|а|о)?|закупк|происхождени|откуда|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(text);
|
const hasPurchaseCue = /(?:куплен(?:ы|а|о)?|закупк|происхождени|откуда|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|когда\s+был\s+куплен|когда\s+куплен|дата\s+закупк|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставлен(?:ы|а)?|purchase\s+provenance|purchase\s+date)/iu.test(text);
|
||||||
return hasItemCue && hasSupplierCue && hasPurchaseCue;
|
return hasItemCue && hasSupplierCue && hasPurchaseCue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ function hasSelectedObjectInventoryFollowupSignal(text) {
|
||||||
if (!/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)/iu.test(text)) {
|
if (!/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)/iu.test(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:поставил|продал)|кому\s+(?:продали|реализовали)|когда\s+(?:примерно\s+)?купили|по\s+каким\s+документам\s+.*купили)/iu.test(text);
|
return /(?:у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:поставил|продал)|кому\s+(?:продали|реализовали)|когда\s+(?:примерно\s+)?купили|по\s+каким\s+документам\s+.*купили)/iu.test(text);
|
||||||
}
|
}
|
||||||
function hasDocsOrBankSignal(text) {
|
function hasDocsOrBankSignal(text) {
|
||||||
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|транзак|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
|
return /(?:док(?:и|умент|ументы|ументов)|docs?|documents?|банк|выписк|платеж|платёж|оплат|поступлен|списан|транзак|transactions?|bank\s+ops|bank\s+operations?)/iu.test(text);
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ function hasSameDateHint(text) {
|
||||||
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function hasExplicitPeriodLiteral(text) {
|
function hasExplicitPeriodLiteral(text) {
|
||||||
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
|
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function hasExplicitCurrentDateHint(text) {
|
function hasExplicitCurrentDateHint(text) {
|
||||||
return /(?:на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодня|сегодня|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)/iu.test(String(text ?? ""));
|
return /(?:на\s+текущ(?:ую|ая|ий|ее|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодняшн(?:юю|ий|ей|ем|его)\s+дат(?:у|а|е|ой|ою)|на\s+сегодня|сегодня|на\s+текущ(?:ий|ую)\s+момент|today|as\s+of\s+today|current\s+date|as\s+of\s+current\s+date)/iu.test(String(text ?? ""));
|
||||||
|
|
@ -257,7 +257,7 @@ function hasSelectedObjectInventorySignal(text) {
|
||||||
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function hasInventorySupplierFollowupCue(text) {
|
function hasInventorySupplierFollowupCue(text) {
|
||||||
return /(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(String(text ?? ""));
|
return /(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
function hasInventoryPurchaseDocumentsFollowupCue(text) {
|
function hasInventoryPurchaseDocumentsFollowupCue(text) {
|
||||||
return /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(String(text ?? ""));
|
return /(?:по\s+каким\s+документам\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|по\s+каким\s+документам\s+(?:был\s+)?куплен|какими\s+документами\s+(?:это|его|этот\s+товар|эту\s+позицию)\s+купили|какими\s+документами\s+(?:был\s+)?куплен|покажи\s+документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|документы\s+по\s+(?:этой\s+позиции|этому\s+товару|ней|нему)|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(String(text ?? ""));
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ export const AUTORUN_ANNOTATIONS_DIR = path.resolve(DATA_DIR, "autorun_annotatio
|
||||||
export const AUTORUN_ANNOTATIONS_FILE = path.resolve(AUTORUN_ANNOTATIONS_DIR, "annotations.json");
|
export const AUTORUN_ANNOTATIONS_FILE = path.resolve(AUTORUN_ANNOTATIONS_DIR, "annotations.json");
|
||||||
export const AUTORUN_GENERATOR_DIR = path.resolve(DATA_DIR, "autorun_generators");
|
export const AUTORUN_GENERATOR_DIR = path.resolve(DATA_DIR, "autorun_generators");
|
||||||
export const AUTORUN_GENERATOR_HISTORY_FILE = path.resolve(AUTORUN_GENERATOR_DIR, "history.json");
|
export const AUTORUN_GENERATOR_HISTORY_FILE = path.resolve(AUTORUN_GENERATOR_DIR, "history.json");
|
||||||
|
export const SHARED_LLM_CONNECTION_FILE = path.resolve(DATA_DIR, "shared_llm_connection.json");
|
||||||
|
|
||||||
export const PROMPTS_DIR = path.resolve(MODULE_ROOT, "prompts");
|
export const PROMPTS_DIR = path.resolve(MODULE_ROOT, "prompts");
|
||||||
export const REPORTS_DIR = path.resolve(MODULE_ROOT, "reports");
|
export const REPORTS_DIR = path.resolve(MODULE_ROOT, "reports");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { NextFunction, Request, Response, Router } from "express";
|
||||||
|
import { SHARED_LLM_CONNECTION_FILE } from "../config";
|
||||||
|
import { ok } from "../utils/http";
|
||||||
|
import { ensureDir, readJsonFile, writeJsonFile } from "../utils/files";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
type LlmProvider = "openai" | "local";
|
||||||
|
|
||||||
|
interface SharedLlmConnectionRecord {
|
||||||
|
schema_version: "shared_llm_connection_v1";
|
||||||
|
updated_at: string;
|
||||||
|
connection: {
|
||||||
|
llmProvider: LlmProvider;
|
||||||
|
model: string;
|
||||||
|
baseUrl: string;
|
||||||
|
temperature: number;
|
||||||
|
maxOutputTokens: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeString(value: unknown, fallback: string): string {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeNumber(value: unknown, fallback: number): number {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && value.trim().length > 0) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isFinite(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFallbackRecord(): SharedLlmConnectionRecord {
|
||||||
|
return {
|
||||||
|
schema_version: "shared_llm_connection_v1",
|
||||||
|
updated_at: "",
|
||||||
|
connection: {
|
||||||
|
llmProvider: "local",
|
||||||
|
model: "qwen2.5-14b-instruct-1m",
|
||||||
|
baseUrl: "http://127.0.0.1:1234/v1",
|
||||||
|
temperature: 0,
|
||||||
|
maxOutputTokens: 900
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSharedRecord(): SharedLlmConnectionRecord | null {
|
||||||
|
const fallback = buildFallbackRecord();
|
||||||
|
const parsed = readJsonFile<SharedLlmConnectionRecord | null>(SHARED_LLM_CONNECTION_FILE, null);
|
||||||
|
if (!parsed || typeof parsed !== "object" || !("connection" in parsed) || !parsed.connection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const connection = parsed.connection;
|
||||||
|
return {
|
||||||
|
schema_version: "shared_llm_connection_v1",
|
||||||
|
updated_at: sanitizeString(parsed.updated_at, ""),
|
||||||
|
connection: {
|
||||||
|
llmProvider: connection.llmProvider === "local" ? "local" : "openai",
|
||||||
|
model: sanitizeString(connection.model, fallback.connection.model),
|
||||||
|
baseUrl: sanitizeString(connection.baseUrl, fallback.connection.baseUrl),
|
||||||
|
temperature: sanitizeNumber(connection.temperature, fallback.connection.temperature),
|
||||||
|
maxOutputTokens: Math.max(1, Math.trunc(sanitizeNumber(connection.maxOutputTokens, fallback.connection.maxOutputTokens)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSharedLlmConfigRouter(): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/api/llm/shared-connection", (_req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const record = loadSharedRecord();
|
||||||
|
ok(res, {
|
||||||
|
ok: true,
|
||||||
|
connection: record?.connection ?? null,
|
||||||
|
updated_at: record?.updated_at ?? null,
|
||||||
|
exists: Boolean(record)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/api/llm/shared-connection", (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const body = (req.body ?? {}) as Record<string, unknown>;
|
||||||
|
const fallback = buildFallbackRecord();
|
||||||
|
const record: SharedLlmConnectionRecord = {
|
||||||
|
schema_version: "shared_llm_connection_v1",
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
connection: {
|
||||||
|
llmProvider: body.llmProvider === "local" ? "local" : "openai",
|
||||||
|
model: sanitizeString(body.model, fallback.connection.model),
|
||||||
|
baseUrl: sanitizeString(body.baseUrl, fallback.connection.baseUrl),
|
||||||
|
temperature: sanitizeNumber(body.temperature, fallback.connection.temperature),
|
||||||
|
maxOutputTokens: Math.max(
|
||||||
|
1,
|
||||||
|
Math.trunc(sanitizeNumber(body.maxOutputTokens, fallback.connection.maxOutputTokens))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ensureDir(path.dirname(SHARED_LLM_CONNECTION_FILE));
|
||||||
|
writeJsonFile(SHARED_LLM_CONNECTION_FILE, record);
|
||||||
|
ok(res, {
|
||||||
|
ok: true,
|
||||||
|
connection: record.connection,
|
||||||
|
updated_at: record.updated_at
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import { buildEvalRouter } from "./routes/eval";
|
||||||
import { buildHistoryRouter } from "./routes/history";
|
import { buildHistoryRouter } from "./routes/history";
|
||||||
import { buildNormalizeRouter } from "./routes/normalize";
|
import { buildNormalizeRouter } from "./routes/normalize";
|
||||||
import { buildPresetsRouter } from "./routes/presets";
|
import { buildPresetsRouter } from "./routes/presets";
|
||||||
|
import { buildSharedLlmConfigRouter } from "./routes/sharedLlmConfig";
|
||||||
import { buildTestConnectionRouter } from "./routes/testConnection";
|
import { buildTestConnectionRouter } from "./routes/testConnection";
|
||||||
import { InMemoryRuntimeAdapter } from "./runtime/inMemoryRuntimeAdapter";
|
import { InMemoryRuntimeAdapter } from "./runtime/inMemoryRuntimeAdapter";
|
||||||
import { AssistantService } from "./services/assistantService";
|
import { AssistantService } from "./services/assistantService";
|
||||||
|
|
@ -71,6 +72,7 @@ export function createApp(): express.Express {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(buildTestConnectionRouter(openaiClient));
|
app.use(buildTestConnectionRouter(openaiClient));
|
||||||
|
app.use(buildSharedLlmConfigRouter());
|
||||||
app.use(buildNormalizeRouter(services));
|
app.use(buildNormalizeRouter(services));
|
||||||
app.use(buildEvalRouter(services));
|
app.use(buildEvalRouter(services));
|
||||||
app.use(buildAssistantRouter(services));
|
app.use(buildAssistantRouter(services));
|
||||||
|
|
|
||||||
|
|
@ -1612,7 +1612,7 @@ function hasSelectedObjectInventoryCue(text: string): boolean {
|
||||||
function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean {
|
function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean {
|
||||||
return (
|
return (
|
||||||
hasSelectedObjectInventoryCue(text) &&
|
hasSelectedObjectInventoryCue(text) &&
|
||||||
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||||
text
|
text
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -1630,7 +1630,7 @@ function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolea
|
||||||
function hasInventoryProvenanceSignalV2(text: string): boolean {
|
function hasInventoryProvenanceSignalV2(text: string): boolean {
|
||||||
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
|
const hasItemCue = /(?:товар|номенклатур|sku|item|product|остат(?:ок|ки)|склад)/iu.test(text);
|
||||||
const hasSupplierCue =
|
const hasSupplierCue =
|
||||||
/(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(
|
/(?:от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:нам\s+)?поставил|кем\s+поставлен|поставщик|supplier|vendor)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
const hasPurchaseCue =
|
const hasPurchaseCue =
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ function hasSelectedObjectInventoryFollowupSignal(text: string): boolean {
|
||||||
if (!/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)/iu.test(text)) {
|
if (!/(?:по\s+выбранному\s+объекту|по\s+выбранной\s+позиции)/iu.test(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return /(?:у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:поставил|продал)|кому\s+(?:продали|реализовали)|когда\s+(?:примерно\s+)?купили|по\s+каким\s+документам\s+.*купили)/iu.test(
|
return /(?:у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|товар|позицию))?|где\s+куплено|кто\s+(?:поставил|продал)|кому\s+(?:продали|реализовали)|когда\s+(?:примерно\s+)?купили|по\s+каким\s+документам\s+.*купили)/iu.test(
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ function hasSameDateHint(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasExplicitPeriodLiteral(text: string): boolean {
|
function hasExplicitPeriodLiteral(text: string): boolean {
|
||||||
return /\b(?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?\b/.test(String(text ?? ""));
|
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasExplicitCurrentDateHint(text: string): boolean {
|
function hasExplicitCurrentDateHint(text: string): boolean {
|
||||||
|
|
@ -323,7 +323,7 @@ function hasSelectedObjectInventorySignal(text: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasInventorySupplierFollowupCue(text: string): boolean {
|
function hasInventorySupplierFollowupCue(text: string): boolean {
|
||||||
return /(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
return /(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(
|
||||||
String(text ?? "")
|
String(text ?? "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,60 @@ describe("inventory selected-object follow-up", () => {
|
||||||
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Производство мебели\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles selected-object colloquial supplier wording 'у кого куплено' as provenance follow-up", async () => {
|
||||||
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
|
fetched_rows: 1,
|
||||||
|
matched_rows: 1,
|
||||||
|
raw_rows: [
|
||||||
|
{
|
||||||
|
Period: "2020-06-18T00:00:00Z",
|
||||||
|
Registrator: "Поступление товаров и услуг 00000000101 от 18.06.2020 0:00:00",
|
||||||
|
AccountDt: "41.01",
|
||||||
|
AccountKt: "60.01",
|
||||||
|
Amount: 498472.5,
|
||||||
|
SubcontoDt1: "Конструкция трансформер рабочей станции 1300*900*2000",
|
||||||
|
SubcontoDt3: "Основной склад",
|
||||||
|
SubcontoKt1: "ООО \\Гамма-мебель\\",
|
||||||
|
SubcontoKt2: "Договор поставки № 11 от 15.06.2020",
|
||||||
|
Organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
'По выбранному объекту "Конструкция трансформер рабочей станции 1300*900*2000": у кого куплено',
|
||||||
|
{
|
||||||
|
followupContext: {
|
||||||
|
previous_intent: "inventory_on_hand_as_of_date",
|
||||||
|
previous_filters: {
|
||||||
|
as_of_date: "2020-06-30",
|
||||||
|
period_from: "2020-06-01",
|
||||||
|
period_to: "2020-06-30",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
},
|
||||||
|
previous_anchor_type: "unknown",
|
||||||
|
previous_anchor_value: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||||
|
expect(result?.debug.detected_intent).toBe("inventory_purchase_provenance_for_item");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_inventory_purchase_provenance_for_item_v1");
|
||||||
|
expect(result?.debug.extracted_filters?.item).toBe("Конструкция трансформер рабочей станции 1300*900*2000");
|
||||||
|
expect(result?.debug.extracted_filters?.as_of_date).toBe("2020-06-30");
|
||||||
|
expect(result?.debug.extracted_filters?.period_from).toBe("2020-06-01");
|
||||||
|
expect(result?.debug.extracted_filters?.period_to).toBe("2020-06-30");
|
||||||
|
expect(result?.debug.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
|
||||||
|
expect(result?.debug.capability_route_mode).toBe("exact");
|
||||||
|
expect(String(result?.reply_text ?? "")).toContain("ООО \\Гамма-мебель\\");
|
||||||
|
});
|
||||||
|
|
||||||
it("handles selected-object wording 'где мы купили это' as provenance follow-up", async () => {
|
it("handles selected-object wording 'где мы купили это' as provenance follow-up", async () => {
|
||||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||||
fetched_rows: 1,
|
fetched_rows: 1,
|
||||||
|
|
|
||||||
|
|
@ -373,6 +373,112 @@ describe("assistant address follow-up carryover", () => {
|
||||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps historical stock date window for selected-object supplier wording 'у кого куплено'", async () => {
|
||||||
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
|
const rootMessage = 'какие у нас остатки на складе на июнь 2020';
|
||||||
|
const followupMessage = 'По выбранному объекту "Конструкция трансформер рабочей станции 1300*900*2000": у кого куплено';
|
||||||
|
|
||||||
|
const addressQueryService = {
|
||||||
|
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||||
|
calls.push({ message, options });
|
||||||
|
if (message === rootMessage) {
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
reply_text:
|
||||||
|
"На 30.06.2020 на складе подтверждено 12 позиций с остатком на 755.392,33 ₽.\n1. Конструкция трансформер рабочей станции 1300*900*2000 | склад: Основной склад",
|
||||||
|
reply_type: "factual",
|
||||||
|
response_type: "FACTUAL_LIST",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "inventory_on_hand_as_of_date",
|
||||||
|
detected_intent_confidence: "high",
|
||||||
|
extracted_filters: {
|
||||||
|
as_of_date: "2020-06-30",
|
||||||
|
period_from: "2020-06-01",
|
||||||
|
period_to: "2020-06-30",
|
||||||
|
warehouse: "Основной склад",
|
||||||
|
organization: "ООО \\Альтернатива Плюс\\"
|
||||||
|
},
|
||||||
|
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
|
||||||
|
capability_id: "confirmed_inventory_on_hand_as_of_date",
|
||||||
|
capability_route_mode: "exact",
|
||||||
|
anchor_type: "unknown",
|
||||||
|
anchor_value_raw: null,
|
||||||
|
anchor_value_resolved: null,
|
||||||
|
reasons: ["address_action_detected", "address_entity_detected"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
handled: true,
|
||||||
|
reply_text:
|
||||||
|
"Поставщик по позиции Конструкция трансформер рабочей станции 1300*900*2000: ООО \\Гамма-мебель\\.",
|
||||||
|
reply_type: "factual",
|
||||||
|
response_type: "FACTUAL_SUMMARY",
|
||||||
|
debug: {
|
||||||
|
detected_mode: "address_query",
|
||||||
|
detected_intent: "inventory_purchase_provenance_for_item",
|
||||||
|
detected_intent_confidence: "medium",
|
||||||
|
extracted_filters: {
|
||||||
|
item: "Конструкция трансформер рабочей станции 1300*900*2000",
|
||||||
|
as_of_date: "2020-06-30",
|
||||||
|
period_from: "2020-06-01",
|
||||||
|
period_to: "2020-06-30"
|
||||||
|
},
|
||||||
|
selected_recipe: "address_inventory_purchase_provenance_for_item_v1",
|
||||||
|
capability_id: "inventory_inventory_purchase_provenance_for_item",
|
||||||
|
capability_route_mode: "exact",
|
||||||
|
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
|
||||||
|
},
|
||||||
|
...(options?.followupContext ? {} : {})
|
||||||
|
};
|
||||||
|
})
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const normalizerService = {
|
||||||
|
normalize: vi.fn(async () => ({
|
||||||
|
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||||
|
reply_type: "partial_coverage",
|
||||||
|
debug: {}
|
||||||
|
}))
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sessions = new AssistantSessionStore();
|
||||||
|
const service = new AssistantService(
|
||||||
|
normalizerService,
|
||||||
|
sessions as any,
|
||||||
|
{} as any,
|
||||||
|
{ persistSession: vi.fn() } as any,
|
||||||
|
addressQueryService
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionId = `asst-address-followup-u-kogo-kupleno-${Date.now()}`;
|
||||||
|
const first = await service.handleMessage({
|
||||||
|
session_id: sessionId,
|
||||||
|
user_message: rootMessage,
|
||||||
|
useMock: true
|
||||||
|
} as any);
|
||||||
|
expect(first.ok).toBe(true);
|
||||||
|
expect(first.reply_type).toBe("factual");
|
||||||
|
|
||||||
|
const second = await service.handleMessage({
|
||||||
|
session_id: sessionId,
|
||||||
|
user_message: followupMessage,
|
||||||
|
useMock: true
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(second.ok).toBe(true);
|
||||||
|
expect(second.reply_type).toBe("factual");
|
||||||
|
expect(calls).toHaveLength(2);
|
||||||
|
expect(calls[1].message).toBe(followupMessage);
|
||||||
|
expect(calls[1].options?.followupContext?.previous_intent).toBe("inventory_on_hand_as_of_date");
|
||||||
|
expect(calls[1].options?.followupContext?.previous_filters?.as_of_date).toBe("2020-06-30");
|
||||||
|
expect(calls[1].options?.followupContext?.previous_filters?.period_from).toBe("2020-06-01");
|
||||||
|
expect(calls[1].options?.followupContext?.previous_filters?.period_to).toBe("2020-06-30");
|
||||||
|
expect(calls[1].options?.followupContext?.previous_filters?.warehouse).toBe("Основной склад");
|
||||||
|
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("treats typo imperative 'показывыай' as implicit continuation and switches to suggested follow-up intent", async () => {
|
it("treats typo imperative 'показывыай' as implicit continuation and switches to suggested follow-up intent", async () => {
|
||||||
const calls: Array<{ message: string; options?: any }> = [];
|
const calls: Array<{ message: string; options?: any }> = [];
|
||||||
const firstMessage = "покажи документы по свк за 2020";
|
const firstMessage = "покажи документы по свк за 2020";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"schema_version": "shared_llm_connection_v1",
|
||||||
|
"updated_at": "2026-04-15T06:12:46.714Z",
|
||||||
|
"connection": {
|
||||||
|
"llmProvider": "local",
|
||||||
|
"model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"baseUrl": "http://127.0.0.1:1234/v1",
|
||||||
|
"temperature": 0.8,
|
||||||
|
"maxOutputTokens": 900
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NDC AI Normalizer Playground</title>
|
<title>NDC AI Normalizer Playground</title>
|
||||||
<script type="module" crossorigin src="/assets/index-B9mz4jx4.js"></script>
|
<script type="module" crossorigin src="/assets/index-Qq5WpuqR.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dcuz1nX5.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dcuz1nX5.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ export default function App() {
|
||||||
});
|
});
|
||||||
const presetAutoloadDoneRef = useRef(false);
|
const presetAutoloadDoneRef = useRef(false);
|
||||||
const skipPresetAutoloadRef = useRef(false);
|
const skipPresetAutoloadRef = useRef(false);
|
||||||
|
const sharedConnectionSyncReadyRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
@ -192,22 +193,45 @@ export default function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
|
const bootstrapSharedConnection = async () => {
|
||||||
if (cached) {
|
const cached = localStorage.getItem(SESSION_CONFIG_KEY);
|
||||||
try {
|
if (cached) {
|
||||||
const parsed = JSON.parse(cached) as Partial<ConnectionState>;
|
try {
|
||||||
setConnection((prev) => ({
|
const parsed = JSON.parse(cached) as Partial<ConnectionState>;
|
||||||
...prev,
|
setConnection((prev) => ({
|
||||||
llmProvider: parsed.llmProvider === "local" ? "local" : "openai",
|
...prev,
|
||||||
model: parsed.model ?? prev.model,
|
llmProvider: parsed.llmProvider === "local" ? "local" : "openai",
|
||||||
baseUrl: parsed.baseUrl ?? prev.baseUrl,
|
model: parsed.model ?? prev.model,
|
||||||
temperature: parsed.temperature ?? prev.temperature,
|
baseUrl: parsed.baseUrl ?? prev.baseUrl,
|
||||||
maxOutputTokens: parsed.maxOutputTokens ?? prev.maxOutputTokens
|
temperature: parsed.temperature ?? prev.temperature,
|
||||||
}));
|
maxOutputTokens: parsed.maxOutputTokens ?? prev.maxOutputTokens
|
||||||
} catch {
|
}));
|
||||||
// ignore broken local cache
|
} catch {
|
||||||
|
// ignore broken local cache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
try {
|
||||||
|
const payload = await apiClient.loadSharedConnectionConfig();
|
||||||
|
if (payload.connection && payload.connection.llmProvider === "local") {
|
||||||
|
setConnection((prev) => ({
|
||||||
|
...prev,
|
||||||
|
llmProvider: "local",
|
||||||
|
model: payload.connection?.model ?? prev.model,
|
||||||
|
baseUrl: payload.connection?.baseUrl ?? prev.baseUrl,
|
||||||
|
temperature: payload.connection?.temperature ?? prev.temperature,
|
||||||
|
maxOutputTokens: payload.connection?.maxOutputTokens ?? prev.maxOutputTokens
|
||||||
|
}));
|
||||||
|
log(`Shared local LLM config loaded: ${payload.connection.model}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Shared local config load error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
sharedConnectionSyncReadyRef.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void bootstrapSharedConnection();
|
||||||
|
|
||||||
const cachedAutorunsLayout = localStorage.getItem(AUTORUNS_LAYOUT_CONFIG_KEY);
|
const cachedAutorunsLayout = localStorage.getItem(AUTORUNS_LAYOUT_CONFIG_KEY);
|
||||||
if (cachedAutorunsLayout) {
|
if (cachedAutorunsLayout) {
|
||||||
|
|
@ -304,6 +328,23 @@ export default function App() {
|
||||||
void refreshRuns();
|
void refreshRuns();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sharedConnectionSyncReadyRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (connection.llmProvider !== "local") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
void apiClient
|
||||||
|
.saveSharedConnectionConfig(connection)
|
||||||
|
.catch((error) =>
|
||||||
|
log(`Shared local config sync error: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
);
|
||||||
|
}, 250);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [connection.baseUrl, connection.llmProvider, connection.maxOutputTokens, connection.model, connection.temperature]);
|
||||||
|
|
||||||
async function refreshHistory() {
|
async function refreshHistory() {
|
||||||
try {
|
try {
|
||||||
const payload = await apiClient.loadHistory();
|
const payload = await apiClient.loadHistory();
|
||||||
|
|
@ -369,6 +410,17 @@ export default function App() {
|
||||||
maxOutputTokens: connection.maxOutputTokens
|
maxOutputTokens: connection.maxOutputTokens
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (connection.llmProvider === "local") {
|
||||||
|
void apiClient
|
||||||
|
.saveSharedConnectionConfig(connection)
|
||||||
|
.then(() => {
|
||||||
|
log("Local config saved and synced to shared agent config (without API key).");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
log(`Local config saved, but shared sync failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
log("Local config saved (without API key).");
|
log("Local config saved (without API key).");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,32 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = {
|
export const apiClient = {
|
||||||
|
async loadSharedConnectionConfig(): Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
connection: Omit<ConnectionState, "apiKey"> | null;
|
||||||
|
updated_at: string | null;
|
||||||
|
exists: boolean;
|
||||||
|
}> {
|
||||||
|
return request("/llm/shared-connection");
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveSharedConnectionConfig(connection: ConnectionState): Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
connection: Omit<ConnectionState, "apiKey">;
|
||||||
|
updated_at: string;
|
||||||
|
}> {
|
||||||
|
return request("/llm/shared-connection", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
llmProvider: connection.llmProvider,
|
||||||
|
model: connection.model,
|
||||||
|
baseUrl: connection.baseUrl,
|
||||||
|
temperature: connection.temperature,
|
||||||
|
maxOutputTokens: connection.maxOutputTokens
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async listModels(connection: ConnectionState): Promise<{ ok: boolean; models: string[]; count: number; timestamp: string }> {
|
async listModels(connection: ConnectionState): Promise<{ ok: boolean; models: string[]; count: number; timestamp: string }> {
|
||||||
return request("/llm/models", {
|
return request("/llm/models", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,57 @@ import { useEffect, useState } from "react";
|
||||||
import { PanelFrame } from "./PanelFrame";
|
import { PanelFrame } from "./PanelFrame";
|
||||||
import type { ConnectionState } from "../state/types";
|
import type { ConnectionState } from "../state/types";
|
||||||
|
|
||||||
|
const LOCAL_BASE_URL = "http://127.0.0.1:1234/v1";
|
||||||
|
const OPENAI_BASE_URL = "https://api.openai.com/v1";
|
||||||
|
const LOCAL_QWEN25_MODEL = "qwen2.5-14b-instruct-1m";
|
||||||
|
const LOCAL_QWEN3_MODEL = "unsloth/qwen3-30b-a3b-instruct-2507";
|
||||||
|
|
||||||
|
type ProviderPreset = "openai" | "local_qwen25" | "local_qwen3" | "local_custom";
|
||||||
|
|
||||||
|
interface ModelOptionDescriptor {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILTIN_LOCAL_MODEL_OPTIONS: ModelOptionDescriptor[] = [
|
||||||
|
{
|
||||||
|
value: LOCAL_QWEN25_MODEL,
|
||||||
|
label: "Qwen2.5 14B Instruct 1M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: LOCAL_QWEN3_MODEL,
|
||||||
|
label: "Qwen3 30B A3B Instruct 2507"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function deriveProviderPreset(value: ConnectionState): ProviderPreset {
|
||||||
|
if (value.llmProvider !== "local") {
|
||||||
|
return "openai";
|
||||||
|
}
|
||||||
|
if (value.model === LOCAL_QWEN3_MODEL) {
|
||||||
|
return "local_qwen3";
|
||||||
|
}
|
||||||
|
if (value.model === LOCAL_QWEN25_MODEL) {
|
||||||
|
return "local_qwen25";
|
||||||
|
}
|
||||||
|
return "local_custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModelOptions(modelOptions: string[], isLocal: boolean): ModelOptionDescriptor[] {
|
||||||
|
const merged = new Map<string, ModelOptionDescriptor>();
|
||||||
|
if (isLocal) {
|
||||||
|
for (const option of BUILTIN_LOCAL_MODEL_OPTIONS) {
|
||||||
|
merged.set(option.value, option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const modelId of modelOptions) {
|
||||||
|
if (!merged.has(modelId)) {
|
||||||
|
merged.set(modelId, { value: modelId, label: modelId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(merged.values());
|
||||||
|
}
|
||||||
|
|
||||||
interface ConnectionPanelProps {
|
interface ConnectionPanelProps {
|
||||||
value: ConnectionState;
|
value: ConnectionState;
|
||||||
modelOptions: string[];
|
modelOptions: string[];
|
||||||
|
|
@ -26,7 +77,9 @@ export function ConnectionPanel({
|
||||||
busy
|
busy
|
||||||
}: ConnectionPanelProps) {
|
}: ConnectionPanelProps) {
|
||||||
const isLocal = value.llmProvider === "local";
|
const isLocal = value.llmProvider === "local";
|
||||||
const modelInCatalog = modelOptions.includes(value.model);
|
const providerPreset = deriveProviderPreset(value);
|
||||||
|
const availableModelOptions = buildModelOptions(modelOptions, isLocal);
|
||||||
|
const modelInCatalog = availableModelOptions.some((option) => option.value === value.model);
|
||||||
const [temperatureInput, setTemperatureInput] = useState(String(value.temperature));
|
const [temperatureInput, setTemperatureInput] = useState(String(value.temperature));
|
||||||
const [maxOutputTokensInput, setMaxOutputTokensInput] = useState(String(value.maxOutputTokens));
|
const [maxOutputTokensInput, setMaxOutputTokensInput] = useState(String(value.maxOutputTokens));
|
||||||
|
|
||||||
|
|
@ -78,18 +131,47 @@ export function ConnectionPanel({
|
||||||
<label>
|
<label>
|
||||||
Provider
|
Provider
|
||||||
<select
|
<select
|
||||||
value={value.llmProvider}
|
value={providerPreset}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const nextProvider = event.target.value === "local" ? "local" : "openai";
|
const selected = event.target.value as ProviderPreset;
|
||||||
|
if (selected === "openai") {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
llmProvider: "openai",
|
||||||
|
baseUrl: OPENAI_BASE_URL
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selected === "local_qwen25") {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
llmProvider: "local",
|
||||||
|
model: LOCAL_QWEN25_MODEL,
|
||||||
|
baseUrl: LOCAL_BASE_URL
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selected === "local_qwen3") {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
llmProvider: "local",
|
||||||
|
model: LOCAL_QWEN3_MODEL,
|
||||||
|
baseUrl: LOCAL_BASE_URL
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
llmProvider: nextProvider,
|
llmProvider: "local",
|
||||||
baseUrl: nextProvider === "local" ? "http://127.0.0.1:1234/v1" : "https://api.openai.com/v1"
|
model: value.llmProvider === "local" ? value.model : LOCAL_QWEN25_MODEL,
|
||||||
|
baseUrl: LOCAL_BASE_URL
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="openai">OpenAI (token)</option>
|
<option value="openai">OpenAI (token)</option>
|
||||||
<option value="local">Local (LM Studio / OpenAI-compatible)</option>
|
<option value="local_qwen25">Qwen2.5 14B Instruct 1M (Local LM Studio)</option>
|
||||||
|
<option value="local_qwen3">Qwen3 30B A3B Instruct 2507 (Local LM Studio)</option>
|
||||||
|
<option value="local_custom">Local custom (LM Studio / OpenAI-compatible)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
@ -106,20 +188,20 @@ export function ConnectionPanel({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="__manual__">Manual input</option>
|
<option value="__manual__">Manual input</option>
|
||||||
{modelOptions.map((modelId) => (
|
{availableModelOptions.map((option) => (
|
||||||
<option key={modelId} value={modelId}>
|
<option key={option.value} value={option.value}>
|
||||||
{modelId}
|
{option.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Model ID (manual)
|
Model ID (manual / current)
|
||||||
<input
|
<input
|
||||||
value={value.model}
|
value={value.model}
|
||||||
onChange={(event) => onChange({ ...value, model: event.target.value })}
|
onChange={(event) => onChange({ ...value, model: event.target.value })}
|
||||||
placeholder="qwen2.5-14b-instruct or lmstudio loaded model id"
|
placeholder="qwen2.5-14b-instruct-1m or unsloth/qwen3-30b-a3b-instruct-2507"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
@ -140,7 +222,7 @@ export function ConnectionPanel({
|
||||||
<input
|
<input
|
||||||
value={value.baseUrl}
|
value={value.baseUrl}
|
||||||
onChange={(event) => onChange({ ...value, baseUrl: event.target.value })}
|
onChange={(event) => onChange({ ...value, baseUrl: event.target.value })}
|
||||||
placeholder={isLocal ? "http://127.0.0.1:1234/v1" : "https://api.openai.com/v1"}
|
placeholder={isLocal ? LOCAL_BASE_URL : OPENAI_BASE_URL}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,15 @@ DEFAULT_ARTIFACTS_ROOT = REPO_ROOT / "artifacts" / "domain_runs"
|
||||||
DEFAULT_SESSIONS_DIR = REPO_ROOT / "llm_normalizer" / "data" / "assistant_sessions"
|
DEFAULT_SESSIONS_DIR = REPO_ROOT / "llm_normalizer" / "data" / "assistant_sessions"
|
||||||
DEFAULT_REPORTS_DIR = REPO_ROOT / "llm_normalizer" / "reports"
|
DEFAULT_REPORTS_DIR = REPO_ROOT / "llm_normalizer" / "reports"
|
||||||
DEFAULT_LOOP_SCHEMA_DIR = REPO_ROOT / "docs" / "orchestration" / "schemas"
|
DEFAULT_LOOP_SCHEMA_DIR = REPO_ROOT / "docs" / "orchestration" / "schemas"
|
||||||
|
SHARED_LLM_CONNECTION_CONFIG = REPO_ROOT / "llm_normalizer" / "data" / "shared_llm_connection.json"
|
||||||
DEFAULT_BACKEND_URL = "http://127.0.0.1:8787"
|
DEFAULT_BACKEND_URL = "http://127.0.0.1:8787"
|
||||||
DEFAULT_PROMPT_VERSION = "address_query_runtime_v1"
|
DEFAULT_PROMPT_VERSION = "address_query_runtime_v1"
|
||||||
DEFAULT_LLM_PROVIDER = "local"
|
BASE_DEFAULT_LLM_PROVIDER = "local"
|
||||||
DEFAULT_LLM_MODEL = "qwen2.5-14b-instruct-1m"
|
BASE_DEFAULT_LLM_MODEL = "qwen2.5-14b-instruct-1m"
|
||||||
DEFAULT_LLM_BASE_URL = "http://127.0.0.1:1234/v1"
|
BASE_DEFAULT_LLM_BASE_URL = "http://127.0.0.1:1234/v1"
|
||||||
DEFAULT_LLM_API_KEY = ""
|
DEFAULT_LLM_API_KEY = ""
|
||||||
DEFAULT_TEMPERATURE = 0.0
|
BASE_DEFAULT_TEMPERATURE = 0.0
|
||||||
DEFAULT_MAX_OUTPUT_TOKENS = 900
|
BASE_DEFAULT_MAX_OUTPUT_TOKENS = 900
|
||||||
TECH_SECTION_HEADER = "### technical_debug_payload_json"
|
TECH_SECTION_HEADER = "### technical_debug_payload_json"
|
||||||
SCENARIO_MANIFEST_SCHEMA_VERSION = "domain_scenario_manifest_v1"
|
SCENARIO_MANIFEST_SCHEMA_VERSION = "domain_scenario_manifest_v1"
|
||||||
SCENARIO_STATE_SCHEMA_VERSION = "domain_scenario_state_v1"
|
SCENARIO_STATE_SCHEMA_VERSION = "domain_scenario_state_v1"
|
||||||
|
|
@ -35,6 +36,51 @@ SCENARIO_PACK_SCHEMA_VERSION = "domain_scenario_pack_v1"
|
||||||
ACTIVE_DOMAIN_CONTRACT_SCHEMA_VERSION = "active_domain_contract_v1"
|
ACTIVE_DOMAIN_CONTRACT_SCHEMA_VERSION = "active_domain_contract_v1"
|
||||||
AUTONOMOUS_LOOP_SCHEMA_VERSION = "domain_autonomous_loop_v1"
|
AUTONOMOUS_LOOP_SCHEMA_VERSION = "domain_autonomous_loop_v1"
|
||||||
|
|
||||||
|
|
||||||
|
def load_shared_local_llm_defaults(config_path: Path | None = None) -> dict[str, Any]:
|
||||||
|
defaults: dict[str, Any] = {
|
||||||
|
"llm_provider": BASE_DEFAULT_LLM_PROVIDER,
|
||||||
|
"llm_model": BASE_DEFAULT_LLM_MODEL,
|
||||||
|
"llm_base_url": BASE_DEFAULT_LLM_BASE_URL,
|
||||||
|
"temperature": BASE_DEFAULT_TEMPERATURE,
|
||||||
|
"max_output_tokens": BASE_DEFAULT_MAX_OUTPUT_TOKENS,
|
||||||
|
}
|
||||||
|
target = config_path or SHARED_LLM_CONNECTION_CONFIG
|
||||||
|
if not target.exists():
|
||||||
|
return defaults
|
||||||
|
try:
|
||||||
|
raw = json.loads(target.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return defaults
|
||||||
|
connection = raw.get("connection")
|
||||||
|
if not isinstance(connection, dict):
|
||||||
|
return defaults
|
||||||
|
if str(connection.get("llmProvider") or "").strip().lower() != "local":
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
model = str(connection.get("model") or "").strip()
|
||||||
|
base_url = str(connection.get("baseUrl") or "").strip()
|
||||||
|
temperature = connection.get("temperature")
|
||||||
|
max_output_tokens = connection.get("maxOutputTokens")
|
||||||
|
|
||||||
|
if model:
|
||||||
|
defaults["llm_model"] = model
|
||||||
|
if base_url:
|
||||||
|
defaults["llm_base_url"] = base_url
|
||||||
|
if isinstance(temperature, (int, float)) and not isinstance(temperature, bool):
|
||||||
|
defaults["temperature"] = float(temperature)
|
||||||
|
if isinstance(max_output_tokens, (int, float)) and not isinstance(max_output_tokens, bool):
|
||||||
|
defaults["max_output_tokens"] = max(1, int(max_output_tokens))
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
|
||||||
|
SHARED_LLM_DEFAULTS = load_shared_local_llm_defaults()
|
||||||
|
DEFAULT_LLM_PROVIDER = str(SHARED_LLM_DEFAULTS["llm_provider"])
|
||||||
|
DEFAULT_LLM_MODEL = str(SHARED_LLM_DEFAULTS["llm_model"])
|
||||||
|
DEFAULT_LLM_BASE_URL = str(SHARED_LLM_DEFAULTS["llm_base_url"])
|
||||||
|
DEFAULT_TEMPERATURE = float(SHARED_LLM_DEFAULTS["temperature"])
|
||||||
|
DEFAULT_MAX_OUTPUT_TOKENS = int(SHARED_LLM_DEFAULTS["max_output_tokens"])
|
||||||
|
|
||||||
TOP_LEVEL_NOISE_PATTERNS = (
|
TOP_LEVEL_NOISE_PATTERNS = (
|
||||||
re.compile(r"^(?:status|статус(?: результата)?)\b", re.IGNORECASE),
|
re.compile(r"^(?:status|статус(?: результата)?)\b", re.IGNORECASE),
|
||||||
re.compile(r"^(?:что учтено|сводка)\b", re.IGNORECASE),
|
re.compile(r"^(?:что учтено|сводка)\b", re.IGNORECASE),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from scripts.domain_case_loop import (
|
||||||
evaluate_analyst_gate,
|
evaluate_analyst_gate,
|
||||||
evaluate_deterministic_loop_gate,
|
evaluate_deterministic_loop_gate,
|
||||||
load_scenario_pack,
|
load_scenario_pack,
|
||||||
|
load_shared_local_llm_defaults,
|
||||||
merge_scenario_date_scope,
|
merge_scenario_date_scope,
|
||||||
select_primary_repair_focus,
|
select_primary_repair_focus,
|
||||||
restore_line_collapsed_files_from_snapshot,
|
restore_line_collapsed_files_from_snapshot,
|
||||||
|
|
@ -66,6 +67,65 @@ def test_merge_scenario_date_scope_preserves_historical_anchor_on_followup() ->
|
||||||
assert merged["source"] == "current_analysis"
|
assert merged["source"] == "current_analysis"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_shared_local_llm_defaults_uses_ui_selected_local_model(tmp_path) -> None:
|
||||||
|
config_path = tmp_path / "shared_llm_connection.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": "shared_llm_connection_v1",
|
||||||
|
"updated_at": "2026-04-15T06:00:00Z",
|
||||||
|
"connection": {
|
||||||
|
"llmProvider": "local",
|
||||||
|
"model": "unsloth/qwen3-30b-a3b-instruct-2507",
|
||||||
|
"baseUrl": "http://127.0.0.1:1234/v1",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"maxOutputTokens": 1200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
defaults = load_shared_local_llm_defaults(config_path)
|
||||||
|
|
||||||
|
assert defaults["llm_provider"] == "local"
|
||||||
|
assert defaults["llm_model"] == "unsloth/qwen3-30b-a3b-instruct-2507"
|
||||||
|
assert defaults["llm_base_url"] == "http://127.0.0.1:1234/v1"
|
||||||
|
assert defaults["temperature"] == 0.2
|
||||||
|
assert defaults["max_output_tokens"] == 1200
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_shared_local_llm_defaults_ignores_non_local_provider(tmp_path) -> None:
|
||||||
|
config_path = tmp_path / "shared_llm_connection.json"
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": "shared_llm_connection_v1",
|
||||||
|
"updated_at": "2026-04-15T06:00:00Z",
|
||||||
|
"connection": {
|
||||||
|
"llmProvider": "openai",
|
||||||
|
"model": "gpt-4o-mini",
|
||||||
|
"baseUrl": "https://api.openai.com/v1",
|
||||||
|
"temperature": 0,
|
||||||
|
"maxOutputTokens": 700,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
defaults = load_shared_local_llm_defaults(config_path)
|
||||||
|
|
||||||
|
assert defaults["llm_provider"] == "local"
|
||||||
|
assert defaults["llm_model"] == "qwen2.5-14b-instruct-1m"
|
||||||
|
|
||||||
|
|
||||||
def test_load_scenario_pack_accepts_active_domain_contract(tmp_path) -> None:
|
def test_load_scenario_pack_accepts_active_domain_contract(tmp_path) -> None:
|
||||||
manifest_path = tmp_path / "active_domain_contract.json"
|
manifest_path = tmp_path / "active_domain_contract.json"
|
||||||
manifest_path.write_text(
|
manifest_path.write_text(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue