@iola_adm/iola-cli 0.1.102 → 0.1.104

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 +83 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
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
@@ -6570,10 +6570,10 @@ function buildCasualDirectAnswer(question) {
6570
6570
  ].join("\n");
6571
6571
  }
6572
6572
  if (/^(привет|здравствуй|здравствуйте|добрый день|доброе утро|добрый вечер|hi|hello|hey)([!.?\s]+(как дела|как ты|что нового)[?.!\s]*)?$/iu.test(normalized)) {
6573
- return "Привет. Работаю нормально. Могу помочь с открытыми данными Йошкар-Олы: школами, детскими садами, адресами, телефонами, сайтами и ИНН.";
6573
+ return "Привет.";
6574
6574
  }
6575
6575
  if (/^(как дела|как ты|что нового|ты тут|ты здесь)[?.!\s]*$/iu.test(normalized)) {
6576
- return "Я на месте. Могу искать и проверять открытые городские данные.";
6576
+ return "Я на месте.";
6577
6577
  }
6578
6578
  if (/^(спасибо|благодарю)[!.?\s]*$/iu.test(normalized)) {
6579
6579
  return "Пожалуйста.";
@@ -6622,12 +6622,12 @@ function buildPersonRoleDirectAnswer(question) {
6622
6622
 
6623
6623
  function extractPersonNameTokens(question) {
6624
6624
  const stopWords = new Set([
6625
- "так", "а", "и", "или", "кто", "что", "какой", "какая", "какого", "какой-то", "это",
6625
+ "так", "а", "в", "во", "и", "или", "кто", "что", "какой", "какая", "какого", "каком", "какую", "какой-то", "это",
6626
6626
  "директор", "директора", "директором", "руководитель", "руководителем", "заведующая", "заведующий",
6627
- "школа", "школы", "школе", "сад", "сада", "детский", "детского", "гимназия", "лицей",
6627
+ "школа", "школы", "школе", "школу", "сад", "сада", "саду", "детсад", "детсаду", "детский", "детского", "детском", "гимназия", "лицей",
6628
6628
  "является", "возглавляет", "найди", "покажи",
6629
6629
  ]);
6630
- return [...String(question || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{3,}/gu)]
6630
+ return [...String(question || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{2,}/gu)]
6631
6631
  .map((match) => match[0])
6632
6632
  .filter((token) => !stopWords.has(token));
6633
6633
  }
@@ -6784,26 +6784,36 @@ async function resolvePublicEntityField(args = {}) {
6784
6784
  const endpoint = `${await getApiBaseUrl()}/resolve-entity-field`;
6785
6785
  const requestedField = normalizeEntityField(args.field);
6786
6786
  const layer = normalizeEntityLayer(args.layer);
6787
+ const strictQuestionNumber = extractEntityNumberFromQuestion(args.source_question, layer);
6787
6788
  const payload = {
6788
6789
  layer,
6789
- entity_number: args.entity_number ?? args.number,
6790
+ entity_number: strictQuestionNumber || (args.entity_number ?? args.number),
6790
6791
  entity_name: args.entity_name || args.name,
6791
6792
  inn: args.inn,
6792
6793
  field: requestedField,
6793
6794
  must_refute_user_value: args.must_refute_user_value,
6794
6795
  source_question: args.source_question,
6796
+ strict_entity_number: Boolean(strictQuestionNumber),
6795
6797
  };
6796
6798
  try {
6797
6799
  const resolved = await postJson(endpoint, stripInternalResolveArgs(payload));
6800
+ const correctedByNumber = await correctResolvedEntityByQuestionNumber(resolved, payload);
6801
+ if (correctedByNumber) return correctedByNumber;
6798
6802
  return await correctResolvedEntityByQuestionName(resolved, payload) || resolved;
6799
6803
  } catch (error) {
6804
+ if (payload.strict_entity_number && isEntityNotFoundError(error)) throw error;
6805
+ if (isLocalEntityValidationError(error)) throw error;
6800
6806
  const fallbackField = pickResolveFieldFallback(requestedField, error);
6801
6807
  if (fallbackField && fallbackField !== requestedField) {
6802
6808
  try {
6803
6809
  const fallbackPayload = { ...payload, field: fallbackField };
6804
6810
  const resolved = await postJson(endpoint, stripInternalResolveArgs(fallbackPayload));
6811
+ const correctedByNumber = await correctResolvedEntityByQuestionNumber(resolved, fallbackPayload);
6812
+ if (correctedByNumber) return correctedByNumber;
6805
6813
  return await correctResolvedEntityByQuestionName(resolved, fallbackPayload) || resolved;
6806
6814
  } catch (retryError) {
6815
+ if (payload.strict_entity_number && isEntityNotFoundError(retryError)) throw retryError;
6816
+ if (isLocalEntityValidationError(retryError)) throw retryError;
6807
6817
  const resolvedBySearch = await resolvePublicEntityFieldViaSearch({ ...payload, field: fallbackField }, retryError);
6808
6818
  if (resolvedBySearch) return resolvedBySearch;
6809
6819
  throw retryError;
@@ -6819,6 +6829,7 @@ async function resolvePublicEntityFieldViaSearch(payload, originalError) {
6819
6829
  const details = parseErrorJsonDetails(originalError);
6820
6830
  if (details?.error !== "entity_not_found") return null;
6821
6831
  if (payload.inn) return null;
6832
+ if (payload.strict_entity_number) return null;
6822
6833
  const query = payload.entity_name || buildEntitySearchQuery(payload.layer, payload.entity_number);
6823
6834
  if (!query) return null;
6824
6835
  const candidates = await searchPublicEntities({ layer: payload.layer, query, limit: 10 });
@@ -6833,10 +6844,28 @@ async function resolvePublicEntityFieldViaSearch(payload, originalError) {
6833
6844
  }
6834
6845
 
6835
6846
  function stripInternalResolveArgs(payload) {
6836
- const { source_question: _sourceQuestion, ...publicPayload } = payload || {};
6847
+ const { source_question: _sourceQuestion, strict_entity_number: _strictEntityNumber, ...publicPayload } = payload || {};
6837
6848
  return publicPayload;
6838
6849
  }
6839
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
+
6840
6869
  async function correctResolvedEntityByQuestionName(resolved, payload) {
6841
6870
  const questionNameQuery = extractEntityNameQueryFromQuestion(payload.source_question, payload.layer);
6842
6871
  if (!questionNameQuery) return null;
@@ -6845,7 +6874,8 @@ async function correctResolvedEntityByQuestionName(resolved, payload) {
6845
6874
 
6846
6875
  const candidates = await searchPublicEntities({ layer: payload.layer, query: questionNameQuery, limit: 5 });
6847
6876
  const candidate = pickNamedEntityCandidate(candidates, questionNameQuery);
6848
- if (!candidate?.inn || candidate.inn === resolvedEntity.inn) return null;
6877
+ if (!candidate?.inn) throw createEntityNotFoundError(payload, questionNameQuery);
6878
+ if (candidate.inn === resolvedEntity.inn) return null;
6849
6879
 
6850
6880
  return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, stripInternalResolveArgs({
6851
6881
  layer: payload.layer,
@@ -6857,6 +6887,7 @@ async function correctResolvedEntityByQuestionName(resolved, payload) {
6857
6887
 
6858
6888
  function extractEntityNameQueryFromQuestion(question, layer) {
6859
6889
  let text = String(question || "").toLocaleLowerCase("ru-RU");
6890
+ if (extractEntityNumberFromQuestion(text, layer)) return "";
6860
6891
  const correction = text.match(/(?:просил|просила|просили)\s+(.+?)\s+а\s+не(?:\s|$)/iu);
6861
6892
  if (correction?.[1]) text = correction[1];
6862
6893
 
@@ -6864,7 +6895,7 @@ function extractEntityNameQueryFromQuestion(question, layer) {
6864
6895
  "а", "в", "во", "где", "же", "и", "или", "как", "какая", "какие", "какой", "кто", "на", "не",
6865
6896
  "найди", "находится", "подскажи", "покажи", "просил", "скажи", "так", "там", "это",
6866
6897
  "адрес", "директор", "директора", "заведующая", "заведующий", "инн", "почта", "сайт", "телефон",
6867
- "гимназия", "детсад", "детсада", "детский", "лицей", "мбдоу", "мбоу", "сад", "сада", "садик",
6898
+ "гимназия", "гимназии", "детсад", "детсада", "детский", "лицей", "лицея", "лицее", "мбдоу", "мбоу", "сад", "сада", "садик",
6868
6899
  "сош", "школа", "школе", "школу", "школы",
6869
6900
  ]);
6870
6901
  const tokens = [...text.normalize("NFC").matchAll(/[\p{L}\d]+/gu)]
@@ -6904,6 +6935,38 @@ function normalizeEntityText(text) {
6904
6935
  return String(text || "").toLocaleLowerCase("ru-RU").replace(/ё/g, "е");
6905
6936
  }
6906
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";
6968
+ }
6969
+
6907
6970
  function buildEntitySearchQuery(layer, number) {
6908
6971
  if (number === undefined || number === null || number === "") return "";
6909
6972
  const label = layer === "kindergartens" ? "детский сад" : "школа";
@@ -6964,9 +7027,20 @@ function parseErrorJsonDetails(error) {
6964
7027
  }
6965
7028
  }
6966
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
+
6967
7038
  function formatToolExecutionError(error, plan) {
6968
7039
  const details = parseErrorJsonDetails(error);
6969
7040
  if (details?.error !== "entity_not_found") return "";
7041
+ if (details.local_validation && details.entity_name) {
7042
+ return `В открытом слое не нашел организацию по названию "${details.entity_name}". Проверьте название.`;
7043
+ }
6970
7044
 
6971
7045
  const step = (plan?.steps || []).find((item) => item.tool === "resolve_entity_field" || item.tool === "search_entities");
6972
7046
  const args = step?.args || {};