@iola_adm/iola-cli 0.1.71 → 0.1.73

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: education
3
+ description: Вопросы по образовательным учреждениям Йошкар-Олы: школы, детские сады, руководители, контакты, адреса и лицензии.
4
+ ---
5
+
6
+ Используй MCP tools слоя данных, а не прямую догадку модели.
7
+
8
+ Основные слои:
9
+
10
+ - `schools` — муниципальные школы, лицеи и гимназии.
11
+ - `kindergartens` — муниципальные детские сады.
12
+
13
+ Для поиска используй:
14
+
15
+ - `mcp:iola-local:layer.list` — список доступных слоев.
16
+ - `mcp:iola-local:layer.schema` — схема слоя.
17
+ - `mcp:iola-local:layer.query` — поиск по слою.
18
+ - `mcp:iola-local:layer.get` — точная карточка записи.
19
+
20
+ Если вопрос можно ответить точным совпадением по руководителю, ИНН, названию или адресу, отвечай только по найденной записи и указывай источник: слой, название и ИНН.
21
+
22
+ Если совпадений нет или их несколько, прямо скажи, что данных недостаточно или нужно уточнение. Не выдумывай школы, детские сады, руководителей, адреса, телефоны, лицензии и сайты.
package/src/cli.js CHANGED
@@ -180,7 +180,7 @@ const DEFAULT_AI_CONFIG = {
180
180
  suggestions: true,
181
181
  },
182
182
  skills: {
183
- enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
183
+ enabled: ["education", "open-data", "reports", "local-model", "local-files", "browser-agent"],
184
184
  },
185
185
  daemon: {
186
186
  host: "127.0.0.1",
@@ -236,11 +236,19 @@ const AGENTS = {
236
236
  const DATASETS = {
237
237
  schools: {
238
238
  title: "Школы",
239
+ category: "Образование",
239
240
  endpoint: "schools",
241
+ aliases: ["школ", "лицей", "гимнази"],
242
+ searchFields: ["name", "address", "head", "inn"],
243
+ personFields: ["head"],
240
244
  },
241
245
  kindergartens: {
242
246
  title: "Детские сады",
247
+ category: "Образование",
243
248
  endpoint: "kindergartens",
249
+ aliases: ["сад", "детсад", "детский сад", "доу", "мбдоу"],
250
+ searchFields: ["name", "address", "head", "inn"],
251
+ personFields: ["head"],
244
252
  },
245
253
  };
246
254
  const SLASH_COMMANDS = [
@@ -5969,6 +5977,20 @@ async function aiAsk(args, context = {}) {
5969
5977
  const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
5970
5978
  const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
5971
5979
  const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
5980
+ const directAnswer = buildDirectDataAnswer(question, dataContext);
5981
+ if (directAnswer) {
5982
+ if (historyEnabled) {
5983
+ recordAskHistory({ question, answer: directAnswer, providerConfig, dataContext, error: "", sessionId });
5984
+ appendSessionExchange(sessionId, question, directAnswer, dataContext, "");
5985
+ }
5986
+ emitEvent(options, "answer", { length: directAnswer.length, sessionId, direct: true });
5987
+ if (options.output) {
5988
+ await assertPermission("writeFiles");
5989
+ await writeFile(options.output, directAnswer, "utf8");
5990
+ }
5991
+ if (!options.quiet) console.log(directAnswer);
5992
+ return directAnswer;
5993
+ }
5972
5994
  const messages = await buildAiMessages(question, dataContext, history, options, config);
5973
5995
  let answer = "";
5974
5996
  let errorMessage = "";
@@ -6018,6 +6040,28 @@ async function aiAsk(args, context = {}) {
6018
6040
  return answer;
6019
6041
  }
6020
6042
 
6043
+ function buildDirectDataAnswer(question, dataContext) {
6044
+ const normalized = question.toLocaleLowerCase("ru-RU");
6045
+ if (!/(директор|руководител)/iu.test(normalized)) return "";
6046
+ const terms = extractSearchTerms(question).filter((term) => !/^\d+$/.test(term));
6047
+ if (terms.length < 2) return "";
6048
+ const rows = [
6049
+ ...dataContext.schools.map((item) => ({ layer: "schools", layerName: "школы", ...item })),
6050
+ ...dataContext.kindergartens.map((item) => ({ layer: "kindergartens", layerName: "детские сады", ...item })),
6051
+ ];
6052
+ const matches = rows.filter((item) => {
6053
+ const head = String(item.head || "").toLocaleLowerCase("ru-RU");
6054
+ return terms.every((term) => head.includes(term));
6055
+ });
6056
+ if (matches.length !== 1) return "";
6057
+ const item = matches[0];
6058
+ return [
6059
+ `${item.head} — руководитель ${item.name}.`,
6060
+ item.address ? `Адрес: ${item.address}.` : "",
6061
+ `Источник: слой ${item.layer}, ИНН ${item.inn || "-"}.`,
6062
+ ].filter(Boolean).join("\n");
6063
+ }
6064
+
6021
6065
  async function resolveUsableAiProfile(config, options = {}) {
6022
6066
  const explicit = Boolean(options.profile || options.provider);
6023
6067
  const providerConfig = resolveAiProfile(config, options);
@@ -6431,33 +6475,71 @@ function emitEvent(options, type, data) {
6431
6475
 
6432
6476
  async function buildDataContext(question) {
6433
6477
  await assertPermission("externalApi");
6434
- const apiBaseUrl = await getApiBaseUrl();
6435
- const mcpBaseUrl = await getMcpBaseUrl();
6436
- const [layers, schools, kindergartens] = await Promise.all([
6437
- fetchJson(`${mcpBaseUrl}/mcp-version`),
6438
- fetchJson(`${apiBaseUrl}/schools?limit=100&offset=0`),
6439
- fetchJson(`${apiBaseUrl}/kindergartens?limit=100&offset=0`),
6440
- ]);
6441
6478
  const queryTerms = extractSearchTerms(question);
6442
6479
  const patterns = extractStructuredPatterns(question);
6443
- const includeSchools = patterns.targetLayers.length === 0 || patterns.targetLayers.includes("schools");
6444
- const includeKindergartens = patterns.targetLayers.length === 0 || patterns.targetLayers.includes("kindergartens");
6445
- const schoolItems = includeSchools
6446
- ? findRelevantItems(normalizeItems(schools), queryTerms, patterns, "schools").slice(0, 8)
6447
- : [];
6448
- const kindergartenItems = includeKindergartens
6449
- ? findRelevantItems(normalizeItems(kindergartens), queryTerms, patterns, "kindergartens").slice(0, 8)
6450
- : [];
6480
+ const layers = await callMcpTool("layer.list", { category: "Образование" });
6481
+ const targetLayerIds = resolveTargetLayerIds(patterns);
6482
+ const layerResults = await Promise.all(targetLayerIds.map((layer) =>
6483
+ callMcpTool("layer.query", { layer, query: question, terms: queryTerms, patterns, limit: 8 })));
6484
+ const layerMap = Object.fromEntries(layerResults.map((result) => [result.layer, result.items || []]));
6451
6485
 
6452
6486
  return {
6453
- layers: layers.data_layers || [],
6487
+ layers,
6454
6488
  query: {
6455
6489
  text: question,
6456
6490
  terms: queryTerms,
6457
6491
  patterns,
6458
6492
  },
6459
- schools: schoolItems.map(selectPublicSummary),
6460
- kindergartens: kindergartenItems.map(selectPublicSummary),
6493
+ schools: layerMap.schools || [],
6494
+ kindergartens: layerMap.kindergartens || [],
6495
+ };
6496
+ }
6497
+
6498
+ function resolveTargetLayerIds(patterns = {}) {
6499
+ const knownLayers = Object.keys(DATASETS);
6500
+ if (patterns.targetLayers?.length) return patterns.targetLayers.filter((layer) => DATASETS[layer]);
6501
+ return knownLayers;
6502
+ }
6503
+
6504
+ async function fetchAllApiItems(endpoint, limit = 500, maxItems = 5000) {
6505
+ const all = [];
6506
+ for (let offset = 0; offset < maxItems; offset += limit) {
6507
+ const separator = endpoint.includes("?") ? "&" : "?";
6508
+ const payload = await fetchJson(`${endpoint}${separator}limit=${limit}&offset=${offset}`);
6509
+ const items = normalizeItems(payload);
6510
+ all.push(...items);
6511
+ if (items.length < limit) break;
6512
+ }
6513
+ return all;
6514
+ }
6515
+
6516
+ async function queryLayer(layer, args = {}) {
6517
+ const meta = DATASETS[layer];
6518
+ if (!meta) throw new Error(`Неизвестный слой: ${layer}`);
6519
+ const endpoint = `${await getApiBaseUrl()}/${meta.endpoint}`;
6520
+ const items = await fetchAllApiItems(endpoint);
6521
+ const terms = args.terms || extractSearchTerms(args.query || "");
6522
+ const patterns = args.patterns || extractStructuredPatterns(args.query || "");
6523
+ const limit = Number(args.limit || 20);
6524
+ return {
6525
+ layer,
6526
+ schema: layerSchema(layer),
6527
+ items: findRelevantItems(normalizeItems(items), terms, patterns, layer).slice(0, limit).map(selectPublicSummary),
6528
+ };
6529
+ }
6530
+
6531
+ function layerSchema(layer) {
6532
+ const meta = DATASETS[layer];
6533
+ if (!meta) throw new Error(`Неизвестный слой: ${layer}`);
6534
+ return {
6535
+ id: layer,
6536
+ title: meta.title,
6537
+ category: meta.category,
6538
+ endpoint: meta.endpoint,
6539
+ aliases: meta.aliases || [],
6540
+ searchFields: meta.searchFields || [],
6541
+ personFields: meta.personFields || [],
6542
+ sourceFields: ["layer", "name", "inn"],
6461
6543
  };
6462
6544
  }
6463
6545
 
@@ -6481,7 +6563,13 @@ function shouldUseDataContext(question, options = {}) {
6481
6563
  if (/^(привет|здравствуй|здравствуйте|добрый день|доброе утро|добрый вечер|hi|hello|hey)[!.?\s]*$/iu.test(normalized)) return false;
6482
6564
  if (/^(спасибо|благодарю|ок|окей|понял|поняла|ясно|хорошо|да|нет)[!.?\s]*$/iu.test(normalized)) return false;
6483
6565
  if (normalized.length <= 24 && /^(как дела|что нового|ты тут|ты здесь|кто ты)[?.!\s]*$/iu.test(normalized)) return false;
6484
- return /\b(школ|сад|детсад|детский сад|лицей|гимнази|инн|адрес|телефон|почт|email|сайт|лиценз|руководител|директор|слой|слои|данн|отчет|отчёт|выгруз|csv|json|найди|покажи|список|карточк|организац|учрежден|йошкар|ола|петрова|строител|советск|первомайск)\b/iu.test(normalized);
6566
+ const dataKeywords = [
6567
+ "школ", "сад", "детсад", "детский сад", "лицей", "гимнази", "инн", "адрес", "телефон",
6568
+ "почт", "email", "сайт", "лиценз", "руководител", "директор", "слой", "слои", "данн",
6569
+ "отчет", "отчёт", "выгруз", "csv", "json", "найди", "покажи", "список", "карточк",
6570
+ "организац", "учрежден", "йошкар", "ола", "петрова", "строител", "советск", "первомайск",
6571
+ ];
6572
+ return dataKeywords.some((keyword) => normalized.includes(keyword));
6485
6573
  }
6486
6574
 
6487
6575
  function extractSearchTerms(question) {
@@ -6491,7 +6579,13 @@ function extractSearchTerms(question) {
6491
6579
  .split(/\s+/)
6492
6580
  .map((term) => term.trim())
6493
6581
  .filter(Boolean)
6494
- .filter((term) => !["какие", "какая", "какой", "есть", "найди", "покажи", "контакты", "адрес", "телефон", "школы", "школа", "сад", "детский", "детские", "сады", "улица", "ул"].includes(term));
6582
+ .filter((term) => ![
6583
+ "в", "во", "на", "по", "и", "а", "ну", "так", "слушай", "скажи", "подскажи",
6584
+ "какие", "какая", "какой", "каком", "какой", "есть", "найди", "покажи",
6585
+ "контакты", "адрес", "телефон", "школы", "школа", "школе", "сад", "детский",
6586
+ "детские", "сады", "улица", "ул", "директор", "руководитель",
6587
+ ].includes(term))
6588
+ .filter((term) => term.length > 2 || /^\d+$/.test(term));
6495
6589
 
6496
6590
  return normalized.length > 0 ? normalized : [question];
6497
6591
  }
@@ -6539,8 +6633,10 @@ function scoreItem(item, terms, patterns, layer) {
6539
6633
  const text = JSON.stringify(summary).toLocaleLowerCase("ru-RU");
6540
6634
  const name = String(summary.name || "").toLocaleLowerCase("ru-RU");
6541
6635
  const address = String(summary.address || "").toLocaleLowerCase("ru-RU");
6636
+ const head = String(summary.head || "").toLocaleLowerCase("ru-RU");
6542
6637
  const generalTerms = terms.filter((term) => !/^\d+$/.test(term));
6543
6638
  let score = generalTerms.reduce((value, term) => value + (text.includes(term.toLocaleLowerCase("ru-RU")) ? 1 : 0), 0);
6639
+ score += generalTerms.reduce((value, term) => value + (head.includes(term.toLocaleLowerCase("ru-RU")) ? 5 : 0), 0);
6544
6640
 
6545
6641
  for (const inn of patterns.inns) {
6546
6642
  if (String(summary.inn) === inn) {
@@ -7781,6 +7877,10 @@ function mcpTools() {
7781
7877
  const schema = (properties = {}) => ({ type: "object", properties, additionalProperties: false });
7782
7878
  return [
7783
7879
  { name: "status", description: "Статус локальной БД, sync и активного AI-профиля.", inputSchema: schema() },
7880
+ { name: "layer.list", description: "Список слоев данных и их схем.", inputSchema: schema({ category: { type: "string" } }) },
7881
+ { name: "layer.schema", description: "Схема слоя данных.", inputSchema: schema({ layer: { type: "string" } }) },
7882
+ { name: "layer.query", description: "Поиск по слою данных через общий retrieval.", inputSchema: schema({ layer: { type: "string" }, query: { type: "string" }, terms: { type: "array" }, patterns: { type: "object" }, limit: { type: "number" } }) },
7883
+ { name: "layer.get", description: "Получить запись слоя по ИНН или названию.", inputSchema: schema({ layer: { type: "string" }, query: { type: "string" }, inn: { type: "string" } }) },
7784
7884
  { name: "search", description: "Поиск по локальным открытым данным Йошкар-Олы.", inputSchema: schema({ query: { type: "string" }, dataset: { type: "string" }, limit: { type: "number" } }) },
7785
7885
  { name: "card", description: "Карточка объекта по названию или ИНН.", inputSchema: schema({ query: { type: "string" } }) },
7786
7886
  { name: "quality", description: "Проверки качества данных.", inputSchema: schema({ scope: { type: "string" } }) },
@@ -7798,6 +7898,7 @@ function mcpTools() {
7798
7898
  function mcpResources() {
7799
7899
  return [
7800
7900
  { uri: "iola://status", name: "Статус CLI", mimeType: "application/json" },
7901
+ { uri: "iola://layers", name: "Слои данных", mimeType: "application/json" },
7801
7902
  { uri: "iola://sync", name: "Статус синхронизации", mimeType: "application/json" },
7802
7903
  { uri: "iola://settings", name: "Эффективные настройки", mimeType: "application/json" },
7803
7904
  { uri: "iola://skills", name: "Skills", mimeType: "application/json" },
@@ -7815,6 +7916,17 @@ function mcpPrompts() {
7815
7916
  }
7816
7917
 
7817
7918
  async function callMcpTool(name, args = {}) {
7919
+ if (name === "layer.list") {
7920
+ return Object.entries(DATASETS)
7921
+ .map(([id, meta]) => layerSchema(id))
7922
+ .filter((layer) => !args.category || layer.category === args.category);
7923
+ }
7924
+ if (name === "layer.schema") return layerSchema(args.layer);
7925
+ if (name === "layer.query") return queryLayer(args.layer, args);
7926
+ if (name === "layer.get") {
7927
+ const result = await queryLayer(args.layer, { query: args.inn || args.query || "", terms: [args.inn || args.query || ""], limit: 1 });
7928
+ return result.items[0] || null;
7929
+ }
7818
7930
  if (name === "index.search") return searchDocs(args.query || "", Number(args.limit || 20));
7819
7931
  if (name === "report") {
7820
7932
  const output = args.output || `${args.name || "education-contacts"}.${args.format || "xlsx"}`;
@@ -7834,6 +7946,7 @@ async function callMcpTool(name, args = {}) {
7834
7946
 
7835
7947
  async function readMcpResource(uri) {
7836
7948
  if (uri === "iola://status") return JSON.stringify({ db: getDbStatus(), sync: getSyncStatus() }, null, 2);
7949
+ if (uri === "iola://layers") return JSON.stringify(Object.fromEntries(Object.keys(DATASETS).map((id) => [id, layerSchema(id)])), null, 2);
7837
7950
  if (uri === "iola://sync") return JSON.stringify(getSyncStatus(), null, 2);
7838
7951
  if (uri === "iola://settings") return JSON.stringify(await loadConfig(), null, 2);
7839
7952
  if (uri === "iola://skills") return JSON.stringify(listSkills(await loadConfig()), null, 2);
@@ -8982,6 +9095,9 @@ function sanitizeConfig(config) {
8982
9095
  }
8983
9096
  }
8984
9097
  }
9098
+ if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("open-data") && !next.skills.enabled.includes("education")) {
9099
+ next.skills.enabled = ["education", ...next.skills.enabled];
9100
+ }
8985
9101
  return next;
8986
9102
  }
8987
9103