@iola_adm/iola-cli 0.1.97 → 0.1.100

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 +173 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.97",
3
+ "version": "0.1.100",
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
@@ -899,8 +899,7 @@ async function startAgentRawInput() {
899
899
  }
900
900
  } finally {
901
901
  clearAgentInputArea(state);
902
- clearAgentStatusBar(state);
903
- finishAgentTerminalLine();
902
+ finishAgentTerminalLine(state);
904
903
  if (!wasRaw) input.setRawMode(false);
905
904
  input.pause();
906
905
  }
@@ -1425,9 +1424,21 @@ function clearAgentStatusBar(state) {
1425
1424
  state.statusRows = 0;
1426
1425
  }
1427
1426
 
1428
- function finishAgentTerminalLine() {
1427
+ function finishAgentTerminalLine(state = null) {
1429
1428
  if (!output.isTTY) return;
1430
- output.write("\r\x1b[0K\n");
1429
+ const rows = Number(output.rows || state?.statusRows || 0);
1430
+ output.write("\x1b[r");
1431
+ if (rows >= 1) {
1432
+ output.write(`\x1b[${rows};1H\x1b[2K\n`);
1433
+ } else {
1434
+ output.write("\r\x1b[0K\n");
1435
+ }
1436
+ if (state) {
1437
+ state.statusBar = false;
1438
+ state.statusRows = 0;
1439
+ state.renderedInputLines = 0;
1440
+ state.renderedLines = 0;
1441
+ }
1431
1442
  }
1432
1443
 
1433
1444
  function startActivityIndicator(label = "работаю") {
@@ -6302,7 +6313,7 @@ function buildDirectDataAnswer(question, dataContext) {
6302
6313
 
6303
6314
  function detectDirectDataFields(normalizedQuestion) {
6304
6315
  const fields = [];
6305
- if (/(директор|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
6316
+ if (/(директ|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
6306
6317
  if (/(сайт|website|url|ссылка)/iu.test(normalizedQuestion)) fields.push("website");
6307
6318
  if (/(телефон|номер телефона|позвонить)/iu.test(normalizedQuestion)) fields.push("phone");
6308
6319
  if (/(почт|email|e-mail|имейл|электронн)/iu.test(normalizedQuestion)) fields.push("email");
@@ -6448,6 +6459,11 @@ async function localToolAsk(question, providerConfig, options) {
6448
6459
  return casualAnswer;
6449
6460
  }
6450
6461
  await ensureLocalData();
6462
+ const personRoleAnswer = buildPersonRoleDirectAnswer(question);
6463
+ if (personRoleAnswer) {
6464
+ if (!options.quiet) console.log(personRoleAnswer);
6465
+ return personRoleAnswer;
6466
+ }
6451
6467
  const plan = await buildLocalToolPlan(question, providerConfig, options);
6452
6468
  if (plan.directAnswer) {
6453
6469
  if (!options.quiet) console.log(plan.directAnswer);
@@ -6505,6 +6521,13 @@ function buildCasualDirectAnswer(question) {
6505
6521
  if (/^(кто ты|что ты|какая ты модель|что ты за модель|что за модель|какая модель|назови модель|ты какая модель|ты кто)([?.!\s]*)$/iu.test(normalized)) {
6506
6522
  return "Я IOLA, первая городская модель искусственного интеллекта Йошкар-Олы. Работаю локально в CLI и отвечаю по открытым городским данным через проверяемые слои и API.";
6507
6523
  }
6524
+ if (/^(что ты умеешь|что умеешь|что можешь|чем можешь помочь|какие у тебя возможности|твои возможности)([?.!\s]*)$/iu.test(normalized)) {
6525
+ return [
6526
+ "Я помогаю работать с открытыми городскими данными Йошкар-Олы.",
6527
+ "Умею искать школы и детские сады, находить адреса, телефоны, сайты, email и ИНН, проверять сведения через слои данных и API, а также готовить простые списки и выгрузки.",
6528
+ "Если данных нет в открытом слое, я скажу об этом прямо.",
6529
+ ].join("\n");
6530
+ }
6508
6531
  if (/^(привет|здравствуй|здравствуйте|добрый день|доброе утро|добрый вечер|hi|hello|hey)([!.?\s]+(как дела|как ты|что нового)[?.!\s]*)?$/iu.test(normalized)) {
6509
6532
  return "Привет. Работаю нормально. Могу помочь с открытыми данными Йошкар-Олы: школами, детскими садами, адресами, телефонами, сайтами и ИНН.";
6510
6533
  }
@@ -6517,6 +6540,68 @@ function buildCasualDirectAnswer(question) {
6517
6540
  return "";
6518
6541
  }
6519
6542
 
6543
+ function buildPersonRoleDirectAnswer(question) {
6544
+ const normalized = String(question || "").toLocaleLowerCase("ru-RU");
6545
+ const asksHead = /(директ|руководител|заведующ|возглавляет)/iu.test(normalized);
6546
+ const asksOrganization = /(какой|какая|где|чего|организац|учрежден|школ|сад|детсад|гимнази|лицей)/iu.test(normalized);
6547
+ if (!asksHead || !asksOrganization) return "";
6548
+
6549
+ const nameTokens = extractPersonNameTokens(question);
6550
+ if (nameTokens.length < 2) return "";
6551
+
6552
+ const dataset = normalized.includes("сад") || normalized.includes("детсад")
6553
+ ? "kindergartens"
6554
+ : normalized.includes("школ") || normalized.includes("гимнази") || normalized.includes("лицей")
6555
+ ? "schools"
6556
+ : "all";
6557
+ const rows = searchLocalRecords("", { dataset, limit: 10000 })
6558
+ .filter((row) => personTokensMatchHead(row.head, nameTokens));
6559
+
6560
+ if (rows.length === 0) {
6561
+ return `В открытых данных не нашел руководителя с ФИО: ${formatPersonTokens(nameTokens)}.`;
6562
+ }
6563
+
6564
+ const exactRows = rows.filter((row) => personTokensMatchHead(row.head, nameTokens, { strict: true }));
6565
+ const matches = exactRows.length > 0 ? exactRows : rows;
6566
+ if (matches.length === 1) {
6567
+ const row = matches[0];
6568
+ return [
6569
+ `${row.head} является руководителем: ${row.name}.`,
6570
+ row.address ? `Адрес: ${row.address}` : "",
6571
+ row.inn ? `ИНН: ${row.inn}` : "",
6572
+ "Источник: открытый слой образования.",
6573
+ ].filter(Boolean).join("\n");
6574
+ }
6575
+
6576
+ return [
6577
+ `Нашел несколько организаций для ФИО ${formatPersonTokens(nameTokens)}:`,
6578
+ ...matches.slice(0, 10).map((row) => `- ${row.head}: ${row.name}${row.inn ? `, ИНН ${row.inn}` : ""}`),
6579
+ ].join("\n");
6580
+ }
6581
+
6582
+ function extractPersonNameTokens(question) {
6583
+ const stopWords = new Set([
6584
+ "так", "а", "и", "или", "кто", "что", "какой", "какая", "какого", "какой-то", "это",
6585
+ "директор", "директора", "директором", "руководитель", "руководителем", "заведующая", "заведующий",
6586
+ "школа", "школы", "школе", "сад", "сада", "детский", "детского", "гимназия", "лицей",
6587
+ "является", "возглавляет", "найди", "покажи",
6588
+ ]);
6589
+ return [...String(question || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{3,}/gu)]
6590
+ .map((match) => match[0])
6591
+ .filter((token) => !stopWords.has(token));
6592
+ }
6593
+
6594
+ function personTokensMatchHead(head, tokens, options = {}) {
6595
+ const headTokens = new Set([...String(head || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{3,}/gu)].map((match) => match[0]));
6596
+ if (options.strict) return tokens.every((token) => headTokens.has(token));
6597
+ const headText = [...headTokens].join(" ");
6598
+ return tokens.every((token) => headTokens.has(token) || headText.includes(token));
6599
+ }
6600
+
6601
+ function formatPersonTokens(tokens) {
6602
+ return tokens.map(capitalizeFirst).join(" ");
6603
+ }
6604
+
6520
6605
  function printToolPlan(plan) {
6521
6606
  console.log("План выполнения:");
6522
6607
  plan.steps.forEach((step, index) => {
@@ -6655,15 +6740,68 @@ async function searchPublicEntities(args = {}) {
6655
6740
  }
6656
6741
 
6657
6742
  async function resolvePublicEntityField(args = {}) {
6658
- const payload = await postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
6659
- layer: normalizeEntityLayer(args.layer),
6743
+ const endpoint = `${await getApiBaseUrl()}/resolve-entity-field`;
6744
+ const requestedField = normalizeEntityField(args.field);
6745
+ const layer = normalizeEntityLayer(args.layer);
6746
+ const payload = {
6747
+ layer,
6660
6748
  entity_number: args.entity_number ?? args.number,
6661
6749
  entity_name: args.entity_name || args.name,
6662
6750
  inn: args.inn,
6663
- field: normalizeEntityField(args.field),
6751
+ field: requestedField,
6664
6752
  must_refute_user_value: args.must_refute_user_value,
6753
+ };
6754
+ try {
6755
+ return await postJson(endpoint, payload);
6756
+ } catch (error) {
6757
+ const fallbackField = pickResolveFieldFallback(requestedField, error);
6758
+ if (fallbackField && fallbackField !== requestedField) {
6759
+ try {
6760
+ return await postJson(endpoint, { ...payload, field: fallbackField });
6761
+ } catch (retryError) {
6762
+ const resolvedBySearch = await resolvePublicEntityFieldViaSearch({ ...payload, field: fallbackField }, retryError);
6763
+ if (resolvedBySearch) return resolvedBySearch;
6764
+ throw retryError;
6765
+ }
6766
+ }
6767
+ const resolvedBySearch = await resolvePublicEntityFieldViaSearch(payload, error);
6768
+ if (resolvedBySearch) return resolvedBySearch;
6769
+ throw error;
6770
+ }
6771
+ }
6772
+
6773
+ async function resolvePublicEntityFieldViaSearch(payload, originalError) {
6774
+ const details = parseErrorJsonDetails(originalError);
6775
+ if (details?.error !== "entity_not_found") return null;
6776
+ if (payload.inn) return null;
6777
+ const query = payload.entity_name || buildEntitySearchQuery(payload.layer, payload.entity_number);
6778
+ if (!query) return null;
6779
+ const candidates = await searchPublicEntities({ layer: payload.layer, query, limit: 10 });
6780
+ const candidate = pickResolvedEntityCandidate(candidates, payload);
6781
+ if (!candidate?.inn) return null;
6782
+ return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
6783
+ layer: payload.layer,
6784
+ inn: candidate.inn,
6785
+ field: payload.field,
6786
+ must_refute_user_value: payload.must_refute_user_value,
6665
6787
  });
6666
- return payload;
6788
+ }
6789
+
6790
+ function buildEntitySearchQuery(layer, number) {
6791
+ if (number === undefined || number === null || number === "") return "";
6792
+ const label = layer === "kindergartens" ? "детский сад" : "школа";
6793
+ return `${label} ${number}`;
6794
+ }
6795
+
6796
+ function pickResolvedEntityCandidate(candidates, payload) {
6797
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
6798
+ const number = payload.entity_number === undefined || payload.entity_number === null ? "" : String(payload.entity_number);
6799
+ if (number) {
6800
+ const exact = candidates.find((item) => itemNameHasNumber(item, number));
6801
+ if (exact) return exact;
6802
+ }
6803
+ if (candidates.length === 1) return candidates[0];
6804
+ return candidates.find((item) => Number(item.score || 0) > 0) || candidates[0];
6667
6805
  }
6668
6806
 
6669
6807
  function normalizeEntityLayer(layer) {
@@ -6675,7 +6813,7 @@ function normalizeEntityLayer(layer) {
6675
6813
 
6676
6814
  function normalizeEntityField(field) {
6677
6815
  const value = String(field || "").toLocaleLowerCase("ru-RU");
6678
- if (value === "director" || value === "head" || value.includes("директор") || value.includes("руковод")) return "head";
6816
+ if (value === "director" || value === "directors" || value === "directs" || value === "direct" || value === "head" || value === "head_name" || value.includes("директ") || value.includes("руковод")) return "director";
6679
6817
  if (value === "site" || value === "url" || value === "website" || value.includes("сайт")) return "website";
6680
6818
  if (value === "mail" || value === "email" || value.includes("почт")) return "email";
6681
6819
  if (value === "phone" || value.includes("тел")) return "phone";
@@ -6684,6 +6822,31 @@ function normalizeEntityField(field) {
6684
6822
  return value || "name";
6685
6823
  }
6686
6824
 
6825
+ function pickResolveFieldFallback(requestedField, error) {
6826
+ const details = parseErrorJsonDetails(error);
6827
+ if (details?.error !== "field_not_public") return "";
6828
+ const publicFields = new Set((details.public_fields || []).map((field) => String(field)));
6829
+ const aliases = {
6830
+ director: ["director", "head_name", "head"],
6831
+ head: ["director", "head_name", "head"],
6832
+ head_name: ["director", "head_name", "head"],
6833
+ license_status: ["license_status", "license_number", "license_date"],
6834
+ }[requestedField] || [];
6835
+ return aliases.find((field) => field !== requestedField && publicFields.has(field)) || "";
6836
+ }
6837
+
6838
+ function parseErrorJsonDetails(error) {
6839
+ const text = error instanceof Error ? error.message : String(error || "");
6840
+ const match = text.match(/\{[\s\S]*\}$/);
6841
+ if (!match) return null;
6842
+ try {
6843
+ const parsed = JSON.parse(match[0]);
6844
+ return parsed.detail || parsed;
6845
+ } catch {
6846
+ return null;
6847
+ }
6848
+ }
6849
+
6687
6850
  function availableToolNames(options = {}) {
6688
6851
  const names = new Set(LOCAL_TOOLS);
6689
6852
  for (const tool of getLocalMcpToolNames()) names.add(tool);