@iola_adm/iola-cli 0.1.98 → 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 +166 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.98",
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);
@@ -6524,6 +6540,68 @@ function buildCasualDirectAnswer(question) {
6524
6540
  return "";
6525
6541
  }
6526
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
+
6527
6605
  function printToolPlan(plan) {
6528
6606
  console.log("План выполнения:");
6529
6607
  plan.steps.forEach((step, index) => {
@@ -6662,15 +6740,68 @@ async function searchPublicEntities(args = {}) {
6662
6740
  }
6663
6741
 
6664
6742
  async function resolvePublicEntityField(args = {}) {
6665
- const payload = await postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
6666
- 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,
6667
6748
  entity_number: args.entity_number ?? args.number,
6668
6749
  entity_name: args.entity_name || args.name,
6669
6750
  inn: args.inn,
6670
- field: normalizeEntityField(args.field),
6751
+ field: requestedField,
6671
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,
6672
6787
  });
6673
- 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];
6674
6805
  }
6675
6806
 
6676
6807
  function normalizeEntityLayer(layer) {
@@ -6682,7 +6813,7 @@ function normalizeEntityLayer(layer) {
6682
6813
 
6683
6814
  function normalizeEntityField(field) {
6684
6815
  const value = String(field || "").toLocaleLowerCase("ru-RU");
6685
- 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";
6686
6817
  if (value === "site" || value === "url" || value === "website" || value.includes("сайт")) return "website";
6687
6818
  if (value === "mail" || value === "email" || value.includes("почт")) return "email";
6688
6819
  if (value === "phone" || value.includes("тел")) return "phone";
@@ -6691,6 +6822,31 @@ function normalizeEntityField(field) {
6691
6822
  return value || "name";
6692
6823
  }
6693
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
+
6694
6850
  function availableToolNames(options = {}) {
6695
6851
  const names = new Set(LOCAL_TOOLS);
6696
6852
  for (const tool of getLocalMcpToolNames()) names.add(tool);