@iola_adm/iola-cli 0.1.7 → 0.1.8
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/README.md +19 -3
- package/package.json +1 -1
- package/src/cli.js +195 -7
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ iola agent
|
|
|
79
79
|
iola ai doctor
|
|
80
80
|
iola ai setup ollama
|
|
81
81
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
82
|
+
iola ai context "школа 29"
|
|
82
83
|
iola ai key set openai
|
|
83
84
|
iola ai key status
|
|
84
85
|
iola ai setup openai --model gpt-4.1-mini
|
|
@@ -115,6 +116,11 @@ iola agent
|
|
|
115
116
|
/search лицей --limit 3
|
|
116
117
|
/mcp-info
|
|
117
118
|
/ai doctor
|
|
119
|
+
/context школа 29
|
|
120
|
+
/use ollama
|
|
121
|
+
/use openai
|
|
122
|
+
/key status
|
|
123
|
+
/key set openai
|
|
118
124
|
/model
|
|
119
125
|
/provider
|
|
120
126
|
/config
|
|
@@ -150,6 +156,16 @@ iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
|
150
156
|
iola ai ask "Покажи контакты лицея"
|
|
151
157
|
```
|
|
152
158
|
|
|
159
|
+
Проверить, какие данные попадут в AI-контекст:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
iola ai context "школа 29"
|
|
163
|
+
iola ai context "1215067180" --json
|
|
164
|
+
iola ai context "улица Петрова"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Поиск контекста учитывает номера учреждений, ИНН и улицы.
|
|
168
|
+
|
|
153
169
|
Ключи OpenAI/OpenRouter сохраняются локально на компьютере пользователя:
|
|
154
170
|
|
|
155
171
|
```text
|
|
@@ -174,9 +190,9 @@ AI-ответ строится по контексту из публичного
|
|
|
174
190
|
|
|
175
191
|
## Назначение
|
|
176
192
|
|
|
177
|
-
|
|
178
|
-
подключения MCP/skill
|
|
179
|
-
|
|
193
|
+
CLI дает прямой терминальный доступ к открытым данным городского округа,
|
|
194
|
+
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter и
|
|
195
|
+
интерактивному агентному режиму.
|
|
180
196
|
|
|
181
197
|
## Переменные окружения
|
|
182
198
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -65,6 +65,7 @@ Usage:
|
|
|
65
65
|
iola banner
|
|
66
66
|
iola agent
|
|
67
67
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
68
|
+
iola ai context TEXT [--json]
|
|
68
69
|
iola ai key set openai
|
|
69
70
|
iola ai key set openrouter
|
|
70
71
|
iola ai key status
|
|
@@ -163,6 +164,21 @@ async function handleAgentLine(line, state) {
|
|
|
163
164
|
return false;
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
if (command === "context") {
|
|
168
|
+
await aiContext(args);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (command === "use") {
|
|
173
|
+
await useAiProvider(args);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (command === "key") {
|
|
178
|
+
await handleAiKey(args);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
166
182
|
if (command === "provider") {
|
|
167
183
|
await printAiConfigField("provider");
|
|
168
184
|
return false;
|
|
@@ -215,8 +231,13 @@ function printAgentHelp() {
|
|
|
215
231
|
/kindergartens get --inn 1215077421
|
|
216
232
|
/search лицей --limit 3
|
|
217
233
|
/mcp-info
|
|
234
|
+
/context школа 29
|
|
218
235
|
/ai doctor
|
|
219
236
|
/ai setup ollama
|
|
237
|
+
/use openai
|
|
238
|
+
/use ollama
|
|
239
|
+
/key status
|
|
240
|
+
/key set openai
|
|
220
241
|
/config
|
|
221
242
|
/provider
|
|
222
243
|
/model
|
|
@@ -290,6 +311,7 @@ async function handleAi(args) {
|
|
|
290
311
|
showBanner();
|
|
291
312
|
console.log(`AI-команды:
|
|
292
313
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
314
|
+
iola ai context TEXT [--json]
|
|
293
315
|
iola ai key set openai
|
|
294
316
|
iola ai key set openrouter
|
|
295
317
|
iola ai key status
|
|
@@ -309,6 +331,11 @@ async function handleAi(args) {
|
|
|
309
331
|
return;
|
|
310
332
|
}
|
|
311
333
|
|
|
334
|
+
if (subcommand === "context") {
|
|
335
|
+
await aiContext(rest);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
312
339
|
if (subcommand === "key") {
|
|
313
340
|
await handleAiKey(rest);
|
|
314
341
|
return;
|
|
@@ -403,6 +430,53 @@ async function handleAiKey(args) {
|
|
|
403
430
|
iola ai key delete openai|openrouter`);
|
|
404
431
|
}
|
|
405
432
|
|
|
433
|
+
async function useAiProvider(args) {
|
|
434
|
+
const [provider] = args;
|
|
435
|
+
|
|
436
|
+
if (provider !== "ollama" && provider !== "openai" && provider !== "openrouter") {
|
|
437
|
+
throw new Error("Провайдер должен быть ollama, openai или openrouter.");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const config = await loadConfig();
|
|
441
|
+
const defaultModel = {
|
|
442
|
+
ollama: config.ai.provider === "ollama" ? config.ai.model : "llama3.2:1b",
|
|
443
|
+
openai: config.ai.provider === "openai" ? config.ai.model : "gpt-4.1-mini",
|
|
444
|
+
openrouter: config.ai.provider === "openrouter" ? config.ai.model : "openai/gpt-4.1-mini",
|
|
445
|
+
}[provider];
|
|
446
|
+
|
|
447
|
+
await saveConfig({
|
|
448
|
+
ai: {
|
|
449
|
+
provider,
|
|
450
|
+
model: defaultModel,
|
|
451
|
+
baseUrl: provider === "ollama"
|
|
452
|
+
? "http://127.0.0.1:11434"
|
|
453
|
+
: provider === "openai"
|
|
454
|
+
? "https://api.openai.com/v1"
|
|
455
|
+
: "https://openrouter.ai/api/v1",
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
console.log(`AI-провайдер переключен: ${provider}, модель: ${defaultModel}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function aiContext(args) {
|
|
463
|
+
const options = parseOptions(args);
|
|
464
|
+
const query = options._.join(" ").trim();
|
|
465
|
+
|
|
466
|
+
if (!query) {
|
|
467
|
+
throw new Error('Текст запроса обязателен. Пример: iola ai context "школа 29"');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const context = await buildDataContext(query);
|
|
471
|
+
|
|
472
|
+
if (options.json) {
|
|
473
|
+
printJson(context);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
printContext(context);
|
|
478
|
+
}
|
|
479
|
+
|
|
406
480
|
async function setAiKey(provider) {
|
|
407
481
|
assertKeyProvider(provider);
|
|
408
482
|
|
|
@@ -550,11 +624,23 @@ async function buildDataContext(question) {
|
|
|
550
624
|
fetchJson(`${API_BASE_URL}/kindergartens?limit=100&offset=0`),
|
|
551
625
|
]);
|
|
552
626
|
const queryTerms = extractSearchTerms(question);
|
|
553
|
-
const
|
|
554
|
-
const
|
|
627
|
+
const patterns = extractStructuredPatterns(question);
|
|
628
|
+
const includeSchools = patterns.targetLayers.length === 0 || patterns.targetLayers.includes("schools");
|
|
629
|
+
const includeKindergartens = patterns.targetLayers.length === 0 || patterns.targetLayers.includes("kindergartens");
|
|
630
|
+
const schoolItems = includeSchools
|
|
631
|
+
? findRelevantItems(normalizeItems(schools), queryTerms, patterns, "schools").slice(0, 8)
|
|
632
|
+
: [];
|
|
633
|
+
const kindergartenItems = includeKindergartens
|
|
634
|
+
? findRelevantItems(normalizeItems(kindergartens), queryTerms, patterns, "kindergartens").slice(0, 8)
|
|
635
|
+
: [];
|
|
555
636
|
|
|
556
637
|
return {
|
|
557
638
|
layers: layers.data_layers || [],
|
|
639
|
+
query: {
|
|
640
|
+
text: question,
|
|
641
|
+
terms: queryTerms,
|
|
642
|
+
patterns,
|
|
643
|
+
},
|
|
558
644
|
schools: schoolItems.map(selectPublicSummary),
|
|
559
645
|
kindergartens: kindergartenItems.map(selectPublicSummary),
|
|
560
646
|
};
|
|
@@ -572,20 +658,83 @@ function extractSearchTerms(question) {
|
|
|
572
658
|
return normalized.length > 0 ? normalized : [question];
|
|
573
659
|
}
|
|
574
660
|
|
|
575
|
-
function
|
|
661
|
+
function extractStructuredPatterns(question) {
|
|
662
|
+
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
663
|
+
const numbers = [...new Set([...normalized.matchAll(/\b\d{1,3}\b/g)].map((match) => match[0]))];
|
|
664
|
+
const inns = [...new Set([...normalized.matchAll(/\b\d{10,12}\b/g)].map((match) => match[0]))];
|
|
665
|
+
const targetLayers = [];
|
|
666
|
+
if (/(^|[^а-яёa-z])(школа|школы|лицей|лицея|гимназия|гимназии)(?=$|[^а-яёa-z])/iu.test(normalized)) {
|
|
667
|
+
targetLayers.push("schools");
|
|
668
|
+
}
|
|
669
|
+
if (/(^|[^а-яёa-z])(сад|сады|детсад|детский|детские|доу|мбдоу)(?=$|[^а-яёa-z])/iu.test(normalized)) {
|
|
670
|
+
targetLayers.push("kindergartens");
|
|
671
|
+
}
|
|
672
|
+
const streetMatches = [
|
|
673
|
+
...normalized.matchAll(/(?:улица|ул\.?)\s+([а-яёa-z0-9 .-]+)/giu),
|
|
674
|
+
...normalized.matchAll(/([а-яёa-z0-9 .-]+)\s+(?:улица|ул\.?)/giu),
|
|
675
|
+
];
|
|
676
|
+
const streets = [...new Set(streetMatches.map((match) => cleanupPattern(match[1])).filter(Boolean))];
|
|
677
|
+
|
|
678
|
+
return { numbers, inns, streets, targetLayers: [...new Set(targetLayers)] };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function cleanupPattern(value) {
|
|
682
|
+
return value
|
|
683
|
+
.replace(/\b(школа|школы|сад|детский|детские|сады|лицей|гимназия|контакты|телефон|адрес|найди|покажи)\b/giu, " ")
|
|
684
|
+
.replace(/\s+/g, " ")
|
|
685
|
+
.trim();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function findRelevantItems(items, terms, patterns, layer) {
|
|
576
689
|
return items
|
|
577
690
|
.map((item) => ({
|
|
578
691
|
item,
|
|
579
|
-
score: scoreItem(item, terms),
|
|
692
|
+
score: scoreItem(item, terms, patterns, layer),
|
|
580
693
|
}))
|
|
581
694
|
.filter((entry) => entry.score > 0)
|
|
582
695
|
.sort((left, right) => right.score - left.score)
|
|
583
696
|
.map((entry) => entry.item);
|
|
584
697
|
}
|
|
585
698
|
|
|
586
|
-
function scoreItem(item, terms) {
|
|
587
|
-
const
|
|
588
|
-
|
|
699
|
+
function scoreItem(item, terms, patterns, layer) {
|
|
700
|
+
const summary = selectPublicSummary(item);
|
|
701
|
+
const text = JSON.stringify(summary).toLocaleLowerCase("ru-RU");
|
|
702
|
+
const name = String(summary.name || "").toLocaleLowerCase("ru-RU");
|
|
703
|
+
const address = String(summary.address || "").toLocaleLowerCase("ru-RU");
|
|
704
|
+
const generalTerms = terms.filter((term) => !/^\d+$/.test(term));
|
|
705
|
+
let score = generalTerms.reduce((value, term) => value + (text.includes(term.toLocaleLowerCase("ru-RU")) ? 1 : 0), 0);
|
|
706
|
+
|
|
707
|
+
for (const inn of patterns.inns) {
|
|
708
|
+
if (String(summary.inn) === inn) {
|
|
709
|
+
score += 20;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
for (const number of patterns.numbers) {
|
|
714
|
+
const numberPatterns = [
|
|
715
|
+
`№ ${number}`,
|
|
716
|
+
`№${number}`,
|
|
717
|
+
`школа ${number}`,
|
|
718
|
+
`сад ${number}`,
|
|
719
|
+
`лицей ${number}`,
|
|
720
|
+
`гимназия ${number}`,
|
|
721
|
+
];
|
|
722
|
+
|
|
723
|
+
if (numberPatterns.some((pattern) => name.includes(pattern))) {
|
|
724
|
+
score += 12;
|
|
725
|
+
if (patterns.targetLayers.length === 0 || patterns.targetLayers.includes(layer)) {
|
|
726
|
+
score += 5;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
for (const street of patterns.streets) {
|
|
732
|
+
if (street && address.includes(street)) {
|
|
733
|
+
score += 8;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return score;
|
|
589
738
|
}
|
|
590
739
|
|
|
591
740
|
function buildAiMessages(question, dataContext, history) {
|
|
@@ -1100,6 +1249,45 @@ async function printAiConfig() {
|
|
|
1100
1249
|
});
|
|
1101
1250
|
}
|
|
1102
1251
|
|
|
1252
|
+
function printContext(context) {
|
|
1253
|
+
const layerNames = context.layers.map((layer) => layer.name || layer.id || String(layer));
|
|
1254
|
+
console.log(`Запрос: ${context.query.text}`);
|
|
1255
|
+
console.log(`Слова поиска: ${context.query.terms.length > 0 ? context.query.terms.join(", ") : "-"}`);
|
|
1256
|
+
console.log(`Номера: ${context.query.patterns.numbers.length > 0 ? context.query.patterns.numbers.join(", ") : "-"}`);
|
|
1257
|
+
console.log(`ИНН: ${context.query.patterns.inns.length > 0 ? context.query.patterns.inns.join(", ") : "-"}`);
|
|
1258
|
+
console.log(`Улицы: ${context.query.patterns.streets.length > 0 ? context.query.patterns.streets.join(", ") : "-"}`);
|
|
1259
|
+
console.log(`Целевые слои: ${context.query.patterns.targetLayers.length > 0 ? context.query.patterns.targetLayers.join(", ") : "все"}`);
|
|
1260
|
+
console.log("");
|
|
1261
|
+
console.log(`Слои данных: ${layerNames.length > 0 ? layerNames.join(", ") : "-"}`);
|
|
1262
|
+
console.log("");
|
|
1263
|
+
|
|
1264
|
+
if (context.schools.length > 0) {
|
|
1265
|
+
console.log("Школы в контексте:");
|
|
1266
|
+
printTable(context.schools, [
|
|
1267
|
+
["name", "Название"],
|
|
1268
|
+
["address", "Адрес"],
|
|
1269
|
+
["phone", "Телефон"],
|
|
1270
|
+
["inn", "ИНН"],
|
|
1271
|
+
]);
|
|
1272
|
+
} else {
|
|
1273
|
+
console.log("Школы в контексте: нет совпадений");
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
console.log("");
|
|
1277
|
+
|
|
1278
|
+
if (context.kindergartens.length > 0) {
|
|
1279
|
+
console.log("Детские сады в контексте:");
|
|
1280
|
+
printTable(context.kindergartens, [
|
|
1281
|
+
["name", "Название"],
|
|
1282
|
+
["address", "Адрес"],
|
|
1283
|
+
["phone", "Телефон"],
|
|
1284
|
+
["inn", "ИНН"],
|
|
1285
|
+
]);
|
|
1286
|
+
} else {
|
|
1287
|
+
console.log("Детские сады в контексте: нет совпадений");
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1103
1291
|
async function printAiConfigField(field) {
|
|
1104
1292
|
const config = await loadConfig();
|
|
1105
1293
|
console.log(config.ai[field] || "-");
|