@iola_adm/iola-cli 0.1.101 → 0.1.102

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 +108 -6
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.102",
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")) {
@@ -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 || "" };
@@ -6783,14 +6791,18 @@ async function resolvePublicEntityField(args = {}) {
6783
6791
  inn: args.inn,
6784
6792
  field: requestedField,
6785
6793
  must_refute_user_value: args.must_refute_user_value,
6794
+ source_question: args.source_question,
6786
6795
  };
6787
6796
  try {
6788
- return await postJson(endpoint, payload);
6797
+ const resolved = await postJson(endpoint, stripInternalResolveArgs(payload));
6798
+ return await correctResolvedEntityByQuestionName(resolved, payload) || resolved;
6789
6799
  } catch (error) {
6790
6800
  const fallbackField = pickResolveFieldFallback(requestedField, error);
6791
6801
  if (fallbackField && fallbackField !== requestedField) {
6792
6802
  try {
6793
- return await postJson(endpoint, { ...payload, field: fallbackField });
6803
+ const fallbackPayload = { ...payload, field: fallbackField };
6804
+ const resolved = await postJson(endpoint, stripInternalResolveArgs(fallbackPayload));
6805
+ return await correctResolvedEntityByQuestionName(resolved, fallbackPayload) || resolved;
6794
6806
  } catch (retryError) {
6795
6807
  const resolvedBySearch = await resolvePublicEntityFieldViaSearch({ ...payload, field: fallbackField }, retryError);
6796
6808
  if (resolvedBySearch) return resolvedBySearch;
@@ -6812,12 +6824,84 @@ async function resolvePublicEntityFieldViaSearch(payload, originalError) {
6812
6824
  const candidates = await searchPublicEntities({ layer: payload.layer, query, limit: 10 });
6813
6825
  const candidate = pickResolvedEntityCandidate(candidates, payload);
6814
6826
  if (!candidate?.inn) return null;
6815
- return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
6827
+ return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, stripInternalResolveArgs({
6816
6828
  layer: payload.layer,
6817
6829
  inn: candidate.inn,
6818
6830
  field: payload.field,
6819
6831
  must_refute_user_value: payload.must_refute_user_value,
6820
- });
6832
+ }));
6833
+ }
6834
+
6835
+ function stripInternalResolveArgs(payload) {
6836
+ const { source_question: _sourceQuestion, ...publicPayload } = payload || {};
6837
+ return publicPayload;
6838
+ }
6839
+
6840
+ async function correctResolvedEntityByQuestionName(resolved, payload) {
6841
+ const questionNameQuery = extractEntityNameQueryFromQuestion(payload.source_question, payload.layer);
6842
+ if (!questionNameQuery) return null;
6843
+ const resolvedEntity = resolved?.entity || resolved || {};
6844
+ if (entityNameMatchesQuery(resolvedEntity.name, questionNameQuery)) return null;
6845
+
6846
+ const candidates = await searchPublicEntities({ layer: payload.layer, query: questionNameQuery, limit: 5 });
6847
+ const candidate = pickNamedEntityCandidate(candidates, questionNameQuery);
6848
+ if (!candidate?.inn || candidate.inn === resolvedEntity.inn) return null;
6849
+
6850
+ return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, stripInternalResolveArgs({
6851
+ layer: payload.layer,
6852
+ inn: candidate.inn,
6853
+ field: payload.field,
6854
+ must_refute_user_value: payload.must_refute_user_value,
6855
+ }));
6856
+ }
6857
+
6858
+ function extractEntityNameQueryFromQuestion(question, layer) {
6859
+ let text = String(question || "").toLocaleLowerCase("ru-RU");
6860
+ const correction = text.match(/(?:просил|просила|просили)\s+(.+?)\s+а\s+не(?:\s|$)/iu);
6861
+ if (correction?.[1]) text = correction[1];
6862
+
6863
+ const stopWords = new Set([
6864
+ "а", "в", "во", "где", "же", "и", "или", "как", "какая", "какие", "какой", "кто", "на", "не",
6865
+ "найди", "находится", "подскажи", "покажи", "просил", "скажи", "так", "там", "это",
6866
+ "адрес", "директор", "директора", "заведующая", "заведующий", "инн", "почта", "сайт", "телефон",
6867
+ "гимназия", "детсад", "детсада", "детский", "лицей", "мбдоу", "мбоу", "сад", "сада", "садик",
6868
+ "сош", "школа", "школе", "школу", "школы",
6869
+ ]);
6870
+ const tokens = [...text.normalize("NFC").matchAll(/[\p{L}\d]+/gu)]
6871
+ .map((match) => normalizeEntityText(match[0]))
6872
+ .filter((token) => token && !stopWords.has(token) && !/^\d+$/.test(token));
6873
+ const uniqueTokens = [...new Set(tokens)];
6874
+ if (uniqueTokens.length === 0) return "";
6875
+ if (uniqueTokens.length === 1 && uniqueTokens[0].length < 5) return "";
6876
+ return uniqueTokens.join(" ");
6877
+ }
6878
+
6879
+ function pickNamedEntityCandidate(candidates, query) {
6880
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
6881
+ const tokens = entityQueryTokens(query);
6882
+ const exact = candidates.find((item) => entityNameMatchesQuery(item.name, query));
6883
+ if (exact) return exact;
6884
+ if (candidates.length === 1 && Number(candidates[0].score || 0) >= 0.5) return candidates[0];
6885
+ return candidates.find((item) => {
6886
+ const name = normalizeEntityText(item.name || "");
6887
+ return Number(item.score || 0) >= 0.8 && tokens.filter((token) => name.includes(token)).length >= Math.ceil(tokens.length / 2);
6888
+ }) || null;
6889
+ }
6890
+
6891
+ function entityNameMatchesQuery(name, query) {
6892
+ const normalizedName = normalizeEntityText(name || "");
6893
+ const tokens = entityQueryTokens(query);
6894
+ return tokens.length > 0 && tokens.every((token) => normalizedName.includes(token));
6895
+ }
6896
+
6897
+ function entityQueryTokens(query) {
6898
+ return [...String(query || "").matchAll(/[\p{L}\d]+/gu)]
6899
+ .map((match) => normalizeEntityText(match[0]))
6900
+ .filter(Boolean);
6901
+ }
6902
+
6903
+ function normalizeEntityText(text) {
6904
+ return String(text || "").toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
6821
6905
  }
6822
6906
 
6823
6907
  function buildEntitySearchQuery(layer, number) {
@@ -6880,6 +6964,24 @@ function parseErrorJsonDetails(error) {
6880
6964
  }
6881
6965
  }
6882
6966
 
6967
+ function formatToolExecutionError(error, plan) {
6968
+ const details = parseErrorJsonDetails(error);
6969
+ if (details?.error !== "entity_not_found") return "";
6970
+
6971
+ const step = (plan?.steps || []).find((item) => item.tool === "resolve_entity_field" || item.tool === "search_entities");
6972
+ const args = step?.args || {};
6973
+ const layer = normalizeEntityLayer(args.layer);
6974
+ const number = args.entity_number ?? args.number;
6975
+ const name = args.entity_name || args.name;
6976
+ const entityLabel = layer === "kindergartens" ? "детский сад" : "школу";
6977
+ const selector = number !== undefined && number !== null && number !== ""
6978
+ ? `${entityLabel} № ${number}`
6979
+ : name
6980
+ ? `${entityLabel} "${name}"`
6981
+ : "такую организацию";
6982
+ return `В открытом слое не нашел ${selector}. Проверьте номер или название.`;
6983
+ }
6984
+
6883
6985
  function availableToolNames(options = {}) {
6884
6986
  const names = new Set(LOCAL_TOOLS);
6885
6987
  for (const tool of getLocalMcpToolNames()) names.add(tool);