@iola_adm/iola-cli 0.1.101 → 0.1.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +186 -10
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6512,7 +6512,15 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
6512
6512
|
}
|
|
6513
6513
|
}
|
|
6514
6514
|
const runId = `run-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
6515
|
-
|
|
6515
|
+
let result;
|
|
6516
|
+
try {
|
|
6517
|
+
result = await executeToolPlan(validated, { ...options, runId });
|
|
6518
|
+
} catch (error) {
|
|
6519
|
+
const friendlyError = formatToolExecutionError(error, validated);
|
|
6520
|
+
if (!friendlyError) throw error;
|
|
6521
|
+
if (!options.quiet) console.log(friendlyError);
|
|
6522
|
+
return friendlyError;
|
|
6523
|
+
}
|
|
6516
6524
|
const answer = formatToolResult(result, options);
|
|
6517
6525
|
|
|
6518
6526
|
if (!options["no-history"] && isFeatureEnabled("sqlite-history")) {
|
|
@@ -6614,12 +6622,12 @@ function buildPersonRoleDirectAnswer(question) {
|
|
|
6614
6622
|
|
|
6615
6623
|
function extractPersonNameTokens(question) {
|
|
6616
6624
|
const stopWords = new Set([
|
|
6617
|
-
"так", "а", "и", "или", "кто", "что", "какой", "какая", "какого", "какой-то", "это",
|
|
6625
|
+
"так", "а", "в", "во", "и", "или", "кто", "что", "какой", "какая", "какого", "каком", "какую", "какой-то", "это",
|
|
6618
6626
|
"директор", "директора", "директором", "руководитель", "руководителем", "заведующая", "заведующий",
|
|
6619
|
-
"школа", "школы", "школе", "сад", "сада", "детский", "детского", "гимназия", "лицей",
|
|
6627
|
+
"школа", "школы", "школе", "школу", "сад", "сада", "саду", "детсад", "детсаду", "детский", "детского", "детском", "гимназия", "лицей",
|
|
6620
6628
|
"является", "возглавляет", "найди", "покажи",
|
|
6621
6629
|
]);
|
|
6622
|
-
return [...String(question || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{
|
|
6630
|
+
return [...String(question || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{2,}/gu)]
|
|
6623
6631
|
.map((match) => match[0])
|
|
6624
6632
|
.filter((token) => !stopWords.has(token));
|
|
6625
6633
|
}
|
|
@@ -6686,7 +6694,7 @@ function normalizeIolaRouterPlan(raw, question, options = {}) {
|
|
|
6686
6694
|
if (casualAnswer) return { directAnswer: casualAnswer };
|
|
6687
6695
|
return inferToolPlan(question, options);
|
|
6688
6696
|
}
|
|
6689
|
-
return { steps: [{ tool, args: payload.args || {} }] };
|
|
6697
|
+
return { steps: [{ tool, args: { ...(payload.args || {}), source_question: question } }] };
|
|
6690
6698
|
}
|
|
6691
6699
|
if (payload.action === "direct_answer") {
|
|
6692
6700
|
return { directAnswer: payload.answer || "" };
|
|
@@ -6776,22 +6784,36 @@ async function resolvePublicEntityField(args = {}) {
|
|
|
6776
6784
|
const endpoint = `${await getApiBaseUrl()}/resolve-entity-field`;
|
|
6777
6785
|
const requestedField = normalizeEntityField(args.field);
|
|
6778
6786
|
const layer = normalizeEntityLayer(args.layer);
|
|
6787
|
+
const strictQuestionNumber = extractEntityNumberFromQuestion(args.source_question, layer);
|
|
6779
6788
|
const payload = {
|
|
6780
6789
|
layer,
|
|
6781
|
-
entity_number: args.entity_number ?? args.number,
|
|
6790
|
+
entity_number: strictQuestionNumber || (args.entity_number ?? args.number),
|
|
6782
6791
|
entity_name: args.entity_name || args.name,
|
|
6783
6792
|
inn: args.inn,
|
|
6784
6793
|
field: requestedField,
|
|
6785
6794
|
must_refute_user_value: args.must_refute_user_value,
|
|
6795
|
+
source_question: args.source_question,
|
|
6796
|
+
strict_entity_number: Boolean(strictQuestionNumber),
|
|
6786
6797
|
};
|
|
6787
6798
|
try {
|
|
6788
|
-
|
|
6799
|
+
const resolved = await postJson(endpoint, stripInternalResolveArgs(payload));
|
|
6800
|
+
const correctedByNumber = await correctResolvedEntityByQuestionNumber(resolved, payload);
|
|
6801
|
+
if (correctedByNumber) return correctedByNumber;
|
|
6802
|
+
return await correctResolvedEntityByQuestionName(resolved, payload) || resolved;
|
|
6789
6803
|
} catch (error) {
|
|
6804
|
+
if (payload.strict_entity_number && isEntityNotFoundError(error)) throw error;
|
|
6805
|
+
if (isLocalEntityValidationError(error)) throw error;
|
|
6790
6806
|
const fallbackField = pickResolveFieldFallback(requestedField, error);
|
|
6791
6807
|
if (fallbackField && fallbackField !== requestedField) {
|
|
6792
6808
|
try {
|
|
6793
|
-
|
|
6809
|
+
const fallbackPayload = { ...payload, field: fallbackField };
|
|
6810
|
+
const resolved = await postJson(endpoint, stripInternalResolveArgs(fallbackPayload));
|
|
6811
|
+
const correctedByNumber = await correctResolvedEntityByQuestionNumber(resolved, fallbackPayload);
|
|
6812
|
+
if (correctedByNumber) return correctedByNumber;
|
|
6813
|
+
return await correctResolvedEntityByQuestionName(resolved, fallbackPayload) || resolved;
|
|
6794
6814
|
} catch (retryError) {
|
|
6815
|
+
if (payload.strict_entity_number && isEntityNotFoundError(retryError)) throw retryError;
|
|
6816
|
+
if (isLocalEntityValidationError(retryError)) throw retryError;
|
|
6795
6817
|
const resolvedBySearch = await resolvePublicEntityFieldViaSearch({ ...payload, field: fallbackField }, retryError);
|
|
6796
6818
|
if (resolvedBySearch) return resolvedBySearch;
|
|
6797
6819
|
throw retryError;
|
|
@@ -6807,17 +6829,142 @@ async function resolvePublicEntityFieldViaSearch(payload, originalError) {
|
|
|
6807
6829
|
const details = parseErrorJsonDetails(originalError);
|
|
6808
6830
|
if (details?.error !== "entity_not_found") return null;
|
|
6809
6831
|
if (payload.inn) return null;
|
|
6832
|
+
if (payload.strict_entity_number) return null;
|
|
6810
6833
|
const query = payload.entity_name || buildEntitySearchQuery(payload.layer, payload.entity_number);
|
|
6811
6834
|
if (!query) return null;
|
|
6812
6835
|
const candidates = await searchPublicEntities({ layer: payload.layer, query, limit: 10 });
|
|
6813
6836
|
const candidate = pickResolvedEntityCandidate(candidates, payload);
|
|
6814
6837
|
if (!candidate?.inn) return null;
|
|
6815
|
-
return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
|
|
6838
|
+
return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, stripInternalResolveArgs({
|
|
6816
6839
|
layer: payload.layer,
|
|
6817
6840
|
inn: candidate.inn,
|
|
6818
6841
|
field: payload.field,
|
|
6819
6842
|
must_refute_user_value: payload.must_refute_user_value,
|
|
6820
|
-
});
|
|
6843
|
+
}));
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6846
|
+
function stripInternalResolveArgs(payload) {
|
|
6847
|
+
const { source_question: _sourceQuestion, strict_entity_number: _strictEntityNumber, ...publicPayload } = payload || {};
|
|
6848
|
+
return publicPayload;
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6851
|
+
async function correctResolvedEntityByQuestionNumber(resolved, payload) {
|
|
6852
|
+
if (!payload.strict_entity_number || !payload.entity_number) return null;
|
|
6853
|
+
const resolvedEntity = resolved?.entity || resolved || {};
|
|
6854
|
+
if (itemNameHasNumber(resolvedEntity, payload.entity_number)) return null;
|
|
6855
|
+
|
|
6856
|
+
const candidates = await searchPublicEntities({ layer: payload.layer, query: buildEntitySearchQuery(payload.layer, payload.entity_number), limit: 10 });
|
|
6857
|
+
const candidate = candidates.find((item) => itemNameHasNumber(item, payload.entity_number));
|
|
6858
|
+
if (!candidate?.inn) throw createEntityNotFoundError(payload, buildEntitySearchQuery(payload.layer, payload.entity_number));
|
|
6859
|
+
if (candidate.inn === resolvedEntity.inn) return null;
|
|
6860
|
+
|
|
6861
|
+
return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, stripInternalResolveArgs({
|
|
6862
|
+
layer: payload.layer,
|
|
6863
|
+
inn: candidate.inn,
|
|
6864
|
+
field: payload.field,
|
|
6865
|
+
must_refute_user_value: payload.must_refute_user_value,
|
|
6866
|
+
}));
|
|
6867
|
+
}
|
|
6868
|
+
|
|
6869
|
+
async function correctResolvedEntityByQuestionName(resolved, payload) {
|
|
6870
|
+
const questionNameQuery = extractEntityNameQueryFromQuestion(payload.source_question, payload.layer);
|
|
6871
|
+
if (!questionNameQuery) return null;
|
|
6872
|
+
const resolvedEntity = resolved?.entity || resolved || {};
|
|
6873
|
+
if (entityNameMatchesQuery(resolvedEntity.name, questionNameQuery)) return null;
|
|
6874
|
+
|
|
6875
|
+
const candidates = await searchPublicEntities({ layer: payload.layer, query: questionNameQuery, limit: 5 });
|
|
6876
|
+
const candidate = pickNamedEntityCandidate(candidates, questionNameQuery);
|
|
6877
|
+
if (!candidate?.inn) throw createEntityNotFoundError(payload, questionNameQuery);
|
|
6878
|
+
if (candidate.inn === resolvedEntity.inn) return null;
|
|
6879
|
+
|
|
6880
|
+
return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, stripInternalResolveArgs({
|
|
6881
|
+
layer: payload.layer,
|
|
6882
|
+
inn: candidate.inn,
|
|
6883
|
+
field: payload.field,
|
|
6884
|
+
must_refute_user_value: payload.must_refute_user_value,
|
|
6885
|
+
}));
|
|
6886
|
+
}
|
|
6887
|
+
|
|
6888
|
+
function extractEntityNameQueryFromQuestion(question, layer) {
|
|
6889
|
+
let text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6890
|
+
if (extractEntityNumberFromQuestion(text, layer)) return "";
|
|
6891
|
+
const correction = text.match(/(?:просил|просила|просили)\s+(.+?)\s+а\s+не(?:\s|$)/iu);
|
|
6892
|
+
if (correction?.[1]) text = correction[1];
|
|
6893
|
+
|
|
6894
|
+
const stopWords = new Set([
|
|
6895
|
+
"а", "в", "во", "где", "же", "и", "или", "как", "какая", "какие", "какой", "кто", "на", "не",
|
|
6896
|
+
"найди", "находится", "подскажи", "покажи", "просил", "скажи", "так", "там", "это",
|
|
6897
|
+
"адрес", "директор", "директора", "заведующая", "заведующий", "инн", "почта", "сайт", "телефон",
|
|
6898
|
+
"гимназия", "гимназии", "детсад", "детсада", "детский", "лицей", "лицея", "лицее", "мбдоу", "мбоу", "сад", "сада", "садик",
|
|
6899
|
+
"сош", "школа", "школе", "школу", "школы",
|
|
6900
|
+
]);
|
|
6901
|
+
const tokens = [...text.normalize("NFC").matchAll(/[\p{L}\d]+/gu)]
|
|
6902
|
+
.map((match) => normalizeEntityText(match[0]))
|
|
6903
|
+
.filter((token) => token && !stopWords.has(token) && !/^\d+$/.test(token));
|
|
6904
|
+
const uniqueTokens = [...new Set(tokens)];
|
|
6905
|
+
if (uniqueTokens.length === 0) return "";
|
|
6906
|
+
if (uniqueTokens.length === 1 && uniqueTokens[0].length < 5) return "";
|
|
6907
|
+
return uniqueTokens.join(" ");
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6910
|
+
function pickNamedEntityCandidate(candidates, query) {
|
|
6911
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
|
6912
|
+
const tokens = entityQueryTokens(query);
|
|
6913
|
+
const exact = candidates.find((item) => entityNameMatchesQuery(item.name, query));
|
|
6914
|
+
if (exact) return exact;
|
|
6915
|
+
if (candidates.length === 1 && Number(candidates[0].score || 0) >= 0.5) return candidates[0];
|
|
6916
|
+
return candidates.find((item) => {
|
|
6917
|
+
const name = normalizeEntityText(item.name || "");
|
|
6918
|
+
return Number(item.score || 0) >= 0.8 && tokens.filter((token) => name.includes(token)).length >= Math.ceil(tokens.length / 2);
|
|
6919
|
+
}) || null;
|
|
6920
|
+
}
|
|
6921
|
+
|
|
6922
|
+
function entityNameMatchesQuery(name, query) {
|
|
6923
|
+
const normalizedName = normalizeEntityText(name || "");
|
|
6924
|
+
const tokens = entityQueryTokens(query);
|
|
6925
|
+
return tokens.length > 0 && tokens.every((token) => normalizedName.includes(token));
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6928
|
+
function entityQueryTokens(query) {
|
|
6929
|
+
return [...String(query || "").matchAll(/[\p{L}\d]+/gu)]
|
|
6930
|
+
.map((match) => normalizeEntityText(match[0]))
|
|
6931
|
+
.filter(Boolean);
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
function normalizeEntityText(text) {
|
|
6935
|
+
return String(text || "").toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
|
|
6936
|
+
}
|
|
6937
|
+
|
|
6938
|
+
function extractEntityNumberFromQuestion(question, layer) {
|
|
6939
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6940
|
+
const isKindergarten = layer === "kindergartens";
|
|
6941
|
+
const isSchool = layer === "schools";
|
|
6942
|
+
const patterns = isKindergarten
|
|
6943
|
+
? [/(?:детск\w*\s+сад\w*|детсад\w*|сад\w*)\s*(?:№|номер|n)?\s*(\d{1,4})/iu, /№\s*(\d{1,4})/iu]
|
|
6944
|
+
: isSchool
|
|
6945
|
+
? [/(?:школ\w*|сош|гимнази\w*|лице\w*)\s*(?:№|номер|n)?\s*(\d{1,4})/iu, /№\s*(\d{1,4})/iu]
|
|
6946
|
+
: [/№\s*(\d{1,4})/iu];
|
|
6947
|
+
for (const pattern of patterns) {
|
|
6948
|
+
const match = text.match(pattern);
|
|
6949
|
+
if (match?.[1]) return match[1];
|
|
6950
|
+
}
|
|
6951
|
+
return "";
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
function createEntityNotFoundError(payload, query = "") {
|
|
6955
|
+
const detail = {
|
|
6956
|
+
error: "entity_not_found",
|
|
6957
|
+
message: "No public entity matched the provided selector",
|
|
6958
|
+
layer: payload.layer,
|
|
6959
|
+
entity_number: payload.entity_number,
|
|
6960
|
+
entity_name: payload.entity_name || query,
|
|
6961
|
+
local_validation: true,
|
|
6962
|
+
};
|
|
6963
|
+
return new Error(`Request failed: 404 Not Found (${awaitedApiPlaceholder()})\n${JSON.stringify({ detail })}`);
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6966
|
+
function awaitedApiPlaceholder() {
|
|
6967
|
+
return "local-validation";
|
|
6821
6968
|
}
|
|
6822
6969
|
|
|
6823
6970
|
function buildEntitySearchQuery(layer, number) {
|
|
@@ -6880,6 +7027,35 @@ function parseErrorJsonDetails(error) {
|
|
|
6880
7027
|
}
|
|
6881
7028
|
}
|
|
6882
7029
|
|
|
7030
|
+
function isEntityNotFoundError(error) {
|
|
7031
|
+
return parseErrorJsonDetails(error)?.error === "entity_not_found";
|
|
7032
|
+
}
|
|
7033
|
+
|
|
7034
|
+
function isLocalEntityValidationError(error) {
|
|
7035
|
+
return Boolean(parseErrorJsonDetails(error)?.local_validation);
|
|
7036
|
+
}
|
|
7037
|
+
|
|
7038
|
+
function formatToolExecutionError(error, plan) {
|
|
7039
|
+
const details = parseErrorJsonDetails(error);
|
|
7040
|
+
if (details?.error !== "entity_not_found") return "";
|
|
7041
|
+
if (details.local_validation && details.entity_name) {
|
|
7042
|
+
return `В открытом слое не нашел организацию по названию "${details.entity_name}". Проверьте название.`;
|
|
7043
|
+
}
|
|
7044
|
+
|
|
7045
|
+
const step = (plan?.steps || []).find((item) => item.tool === "resolve_entity_field" || item.tool === "search_entities");
|
|
7046
|
+
const args = step?.args || {};
|
|
7047
|
+
const layer = normalizeEntityLayer(args.layer);
|
|
7048
|
+
const number = args.entity_number ?? args.number;
|
|
7049
|
+
const name = args.entity_name || args.name;
|
|
7050
|
+
const entityLabel = layer === "kindergartens" ? "детский сад" : "школу";
|
|
7051
|
+
const selector = number !== undefined && number !== null && number !== ""
|
|
7052
|
+
? `${entityLabel} № ${number}`
|
|
7053
|
+
: name
|
|
7054
|
+
? `${entityLabel} "${name}"`
|
|
7055
|
+
: "такую организацию";
|
|
7056
|
+
return `В открытом слое не нашел ${selector}. Проверьте номер или название.`;
|
|
7057
|
+
}
|
|
7058
|
+
|
|
6883
7059
|
function availableToolNames(options = {}) {
|
|
6884
7060
|
const names = new Set(LOCAL_TOOLS);
|
|
6885
7061
|
for (const tool of getLocalMcpToolNames()) names.add(tool);
|