@iola_adm/iola-cli 0.1.70 → 0.1.72

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 +74 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
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
@@ -766,7 +766,7 @@ async function startAgentReadline() {
766
766
  }
767
767
 
768
768
  async function startAgentRawInput() {
769
- const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, rawMode: true, pendingOutput: "", aiStatus: null };
769
+ const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, renderedLines: 0, rawMode: true, pendingOutput: "", aiStatus: null };
770
770
  const wasRaw = input.isRaw;
771
771
  activateRawInput(input);
772
772
 
@@ -1330,14 +1330,18 @@ function renderAgentInput(state) {
1330
1330
  output.write(`\x1b[${cursorColumn + 1}G`);
1331
1331
  }
1332
1332
  state.renderedInputLines = inputLines.length;
1333
+ state.renderedLines = renderedLines.length;
1333
1334
  }
1334
1335
 
1335
1336
  function clearAgentInputArea(state = null) {
1336
1337
  if (!output.isTTY) return;
1337
- const inputLines = Math.max(1, Number(state?.renderedInputLines || 1));
1338
- if (inputLines > 1) output.write(`\x1b[${inputLines - 1}A`);
1338
+ const renderedLines = Math.max(1, Number(state?.renderedLines || state?.renderedInputLines || 1));
1339
+ if (renderedLines > 1) output.write(`\x1b[${renderedLines - 1}A`);
1339
1340
  output.write("\r\x1b[0J");
1340
- if (state) state.renderedInputLines = 0;
1341
+ if (state) {
1342
+ state.renderedInputLines = 0;
1343
+ state.renderedLines = 0;
1344
+ }
1341
1345
  }
1342
1346
 
1343
1347
  function startActivityIndicator(label = "работаю") {
@@ -5965,6 +5969,20 @@ async function aiAsk(args, context = {}) {
5965
5969
  const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
5966
5970
  const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
5967
5971
  const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
5972
+ const directAnswer = buildDirectDataAnswer(question, dataContext);
5973
+ if (directAnswer) {
5974
+ if (historyEnabled) {
5975
+ recordAskHistory({ question, answer: directAnswer, providerConfig, dataContext, error: "", sessionId });
5976
+ appendSessionExchange(sessionId, question, directAnswer, dataContext, "");
5977
+ }
5978
+ emitEvent(options, "answer", { length: directAnswer.length, sessionId, direct: true });
5979
+ if (options.output) {
5980
+ await assertPermission("writeFiles");
5981
+ await writeFile(options.output, directAnswer, "utf8");
5982
+ }
5983
+ if (!options.quiet) console.log(directAnswer);
5984
+ return directAnswer;
5985
+ }
5968
5986
  const messages = await buildAiMessages(question, dataContext, history, options, config);
5969
5987
  let answer = "";
5970
5988
  let errorMessage = "";
@@ -6014,6 +6032,28 @@ async function aiAsk(args, context = {}) {
6014
6032
  return answer;
6015
6033
  }
6016
6034
 
6035
+ function buildDirectDataAnswer(question, dataContext) {
6036
+ const normalized = question.toLocaleLowerCase("ru-RU");
6037
+ if (!/(директор|руководител)/iu.test(normalized)) return "";
6038
+ const terms = extractSearchTerms(question).filter((term) => !/^\d+$/.test(term));
6039
+ if (terms.length < 2) return "";
6040
+ const rows = [
6041
+ ...dataContext.schools.map((item) => ({ layer: "schools", layerName: "школы", ...item })),
6042
+ ...dataContext.kindergartens.map((item) => ({ layer: "kindergartens", layerName: "детские сады", ...item })),
6043
+ ];
6044
+ const matches = rows.filter((item) => {
6045
+ const head = String(item.head || "").toLocaleLowerCase("ru-RU");
6046
+ return terms.every((term) => head.includes(term));
6047
+ });
6048
+ if (matches.length !== 1) return "";
6049
+ const item = matches[0];
6050
+ return [
6051
+ `${item.head} — руководитель ${item.name}.`,
6052
+ item.address ? `Адрес: ${item.address}.` : "",
6053
+ `Источник: слой ${item.layer}, ИНН ${item.inn || "-"}.`,
6054
+ ].filter(Boolean).join("\n");
6055
+ }
6056
+
6017
6057
  async function resolveUsableAiProfile(config, options = {}) {
6018
6058
  const explicit = Boolean(options.profile || options.provider);
6019
6059
  const providerConfig = resolveAiProfile(config, options);
@@ -6431,8 +6471,8 @@ async function buildDataContext(question) {
6431
6471
  const mcpBaseUrl = await getMcpBaseUrl();
6432
6472
  const [layers, schools, kindergartens] = await Promise.all([
6433
6473
  fetchJson(`${mcpBaseUrl}/mcp-version`),
6434
- fetchJson(`${apiBaseUrl}/schools?limit=100&offset=0`),
6435
- fetchJson(`${apiBaseUrl}/kindergartens?limit=100&offset=0`),
6474
+ fetchAllApiItems(`${apiBaseUrl}/schools`),
6475
+ fetchAllApiItems(`${apiBaseUrl}/kindergartens`),
6436
6476
  ]);
6437
6477
  const queryTerms = extractSearchTerms(question);
6438
6478
  const patterns = extractStructuredPatterns(question);
@@ -6457,6 +6497,18 @@ async function buildDataContext(question) {
6457
6497
  };
6458
6498
  }
6459
6499
 
6500
+ async function fetchAllApiItems(endpoint, limit = 500, maxItems = 5000) {
6501
+ const all = [];
6502
+ for (let offset = 0; offset < maxItems; offset += limit) {
6503
+ const separator = endpoint.includes("?") ? "&" : "?";
6504
+ const payload = await fetchJson(`${endpoint}${separator}limit=${limit}&offset=${offset}`);
6505
+ const items = normalizeItems(payload);
6506
+ all.push(...items);
6507
+ if (items.length < limit) break;
6508
+ }
6509
+ return all;
6510
+ }
6511
+
6460
6512
  function emptyDataContext(question) {
6461
6513
  return {
6462
6514
  enabled: false,
@@ -6477,7 +6529,13 @@ function shouldUseDataContext(question, options = {}) {
6477
6529
  if (/^(привет|здравствуй|здравствуйте|добрый день|доброе утро|добрый вечер|hi|hello|hey)[!.?\s]*$/iu.test(normalized)) return false;
6478
6530
  if (/^(спасибо|благодарю|ок|окей|понял|поняла|ясно|хорошо|да|нет)[!.?\s]*$/iu.test(normalized)) return false;
6479
6531
  if (normalized.length <= 24 && /^(как дела|что нового|ты тут|ты здесь|кто ты)[?.!\s]*$/iu.test(normalized)) return false;
6480
- return /\b(школ|сад|детсад|детский сад|лицей|гимнази|инн|адрес|телефон|почт|email|сайт|лиценз|руководител|директор|слой|слои|данн|отчет|отчёт|выгруз|csv|json|найди|покажи|список|карточк|организац|учрежден|йошкар|ола|петрова|строител|советск|первомайск)\b/iu.test(normalized);
6532
+ const dataKeywords = [
6533
+ "школ", "сад", "детсад", "детский сад", "лицей", "гимнази", "инн", "адрес", "телефон",
6534
+ "почт", "email", "сайт", "лиценз", "руководител", "директор", "слой", "слои", "данн",
6535
+ "отчет", "отчёт", "выгруз", "csv", "json", "найди", "покажи", "список", "карточк",
6536
+ "организац", "учрежден", "йошкар", "ола", "петрова", "строител", "советск", "первомайск",
6537
+ ];
6538
+ return dataKeywords.some((keyword) => normalized.includes(keyword));
6481
6539
  }
6482
6540
 
6483
6541
  function extractSearchTerms(question) {
@@ -6487,7 +6545,13 @@ function extractSearchTerms(question) {
6487
6545
  .split(/\s+/)
6488
6546
  .map((term) => term.trim())
6489
6547
  .filter(Boolean)
6490
- .filter((term) => !["какие", "какая", "какой", "есть", "найди", "покажи", "контакты", "адрес", "телефон", "школы", "школа", "сад", "детский", "детские", "сады", "улица", "ул"].includes(term));
6548
+ .filter((term) => ![
6549
+ "в", "во", "на", "по", "и", "а", "ну", "так", "слушай", "скажи", "подскажи",
6550
+ "какие", "какая", "какой", "каком", "какой", "есть", "найди", "покажи",
6551
+ "контакты", "адрес", "телефон", "школы", "школа", "школе", "сад", "детский",
6552
+ "детские", "сады", "улица", "ул", "директор", "руководитель",
6553
+ ].includes(term))
6554
+ .filter((term) => term.length > 2 || /^\d+$/.test(term));
6491
6555
 
6492
6556
  return normalized.length > 0 ? normalized : [question];
6493
6557
  }
@@ -6535,8 +6599,10 @@ function scoreItem(item, terms, patterns, layer) {
6535
6599
  const text = JSON.stringify(summary).toLocaleLowerCase("ru-RU");
6536
6600
  const name = String(summary.name || "").toLocaleLowerCase("ru-RU");
6537
6601
  const address = String(summary.address || "").toLocaleLowerCase("ru-RU");
6602
+ const head = String(summary.head || "").toLocaleLowerCase("ru-RU");
6538
6603
  const generalTerms = terms.filter((term) => !/^\d+$/.test(term));
6539
6604
  let score = generalTerms.reduce((value, term) => value + (text.includes(term.toLocaleLowerCase("ru-RU")) ? 1 : 0), 0);
6605
+ score += generalTerms.reduce((value, term) => value + (head.includes(term.toLocaleLowerCase("ru-RU")) ? 5 : 0), 0);
6540
6606
 
6541
6607
  for (const inn of patterns.inns) {
6542
6608
  if (String(summary.inn) === inn) {