@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +186 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.101",
3
+ "version": "0.1.103",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
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
- const result = await executeToolPlan(validated, { ...options, runId });
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}{3,}/gu)]
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
- return await postJson(endpoint, payload);
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
- return await postJson(endpoint, { ...payload, field: fallbackField });
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);