@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 +1 -1
- package/skills/education/SKILL.md +22 -0
- package/src/cli.js +137 -21
package/package.json
CHANGED
|
@@ -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
|
|
6444
|
-
const
|
|
6445
|
-
const
|
|
6446
|
-
|
|
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
|
|
6487
|
+
layers,
|
|
6454
6488
|
query: {
|
|
6455
6489
|
text: question,
|
|
6456
6490
|
terms: queryTerms,
|
|
6457
6491
|
patterns,
|
|
6458
6492
|
},
|
|
6459
|
-
schools:
|
|
6460
|
-
kindergartens:
|
|
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
|
-
|
|
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) => ![
|
|
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
|
|