@iola_adm/iola-cli 0.1.15 → 0.1.17
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 +59 -0
- package/bin/iola.js +0 -0
- package/package.json +1 -1
- package/src/cli.js +297 -8
package/README.md
CHANGED
|
@@ -103,7 +103,13 @@ iola cache status
|
|
|
103
103
|
iola cache warm
|
|
104
104
|
iola cache clear
|
|
105
105
|
iola sync
|
|
106
|
+
iola sync status
|
|
107
|
+
iola diff schools
|
|
106
108
|
iola search "Петрова" --local
|
|
109
|
+
iola search "Петрова" --local --fts
|
|
110
|
+
iola card "школа 29"
|
|
111
|
+
iola quality
|
|
112
|
+
iola quality missing-phones
|
|
107
113
|
iola data schools --where address=Петрова --save schools-petrova
|
|
108
114
|
iola views
|
|
109
115
|
iola view schools-petrova --format csv --output schools-petrova.csv
|
|
@@ -121,6 +127,7 @@ iola version --check
|
|
|
121
127
|
iola ask "Найди школу 29"
|
|
122
128
|
iola ask "Найди школу 29" --profile codex --events --output answer.txt
|
|
123
129
|
iola ask "Найди школу 29" --schema json --no-history
|
|
130
|
+
iola ask "выгрузи школы на Петрова в csv" --profile local --tools --reasoning verify
|
|
124
131
|
iola data schools --format csv --output schools.csv
|
|
125
132
|
iola data schools --limit 10
|
|
126
133
|
iola data kindergartens --search "29"
|
|
@@ -381,7 +388,10 @@ iola data schools --cache
|
|
|
381
388
|
|
|
382
389
|
```bash
|
|
383
390
|
iola sync
|
|
391
|
+
iola sync status
|
|
392
|
+
iola diff
|
|
384
393
|
iola search "Петрова" --local
|
|
394
|
+
iola search "школа Петрова" --local --fts
|
|
385
395
|
iola data schools --local --search "лицей"
|
|
386
396
|
```
|
|
387
397
|
|
|
@@ -409,6 +419,55 @@ iola petrova
|
|
|
409
419
|
iola run "выгрузи школы на Петрова в csv"
|
|
410
420
|
```
|
|
411
421
|
|
|
422
|
+
## Локальный tool-agent для слабых моделей
|
|
423
|
+
|
|
424
|
+
Для локального профиля Ollama доступен режим `--tools`. Он сделан специально
|
|
425
|
+
для маленьких моделей, которые хуже отвечают свободным текстом, но могут быть
|
|
426
|
+
полезны как планировщик действий.
|
|
427
|
+
|
|
428
|
+
В этом режиме CLI не доверяет модели напрямую. Модель предлагает JSON-план,
|
|
429
|
+
CLI валидирует список разрешенных tools и сам выполняет действия через
|
|
430
|
+
проверенные локальные функции:
|
|
431
|
+
|
|
432
|
+
- `search_local`
|
|
433
|
+
- `get_card`
|
|
434
|
+
- `export_data`
|
|
435
|
+
- `run_report`
|
|
436
|
+
- `save_view`
|
|
437
|
+
|
|
438
|
+
Пример:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
iola ask "выгрузи школы на Петрова в csv" --profile local --tools
|
|
442
|
+
iola ask "найди детсады без телефона" --profile local --tools --reasoning verify
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Режимы:
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
--reasoning fast # один план
|
|
449
|
+
--reasoning verify # план с валидацией результата
|
|
450
|
+
--reasoning vote # несколько вариантов, выбирается валидный
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
OpenAI, OpenRouter и Codex работают как раньше. `--tools` применяется только
|
|
454
|
+
к локальному Ollama-профилю, чтобы не менять поведение внешних провайдеров.
|
|
455
|
+
|
|
456
|
+
Карточки, качество данных и изменения:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
iola card schools 1215067180
|
|
460
|
+
iola card "школа 29"
|
|
461
|
+
iola quality
|
|
462
|
+
iola quality schools
|
|
463
|
+
iola quality missing-phones
|
|
464
|
+
iola quality invalid-emails
|
|
465
|
+
iola quality duplicate-inn
|
|
466
|
+
iola sync status
|
|
467
|
+
iola diff
|
|
468
|
+
iola diff schools
|
|
469
|
+
```
|
|
470
|
+
|
|
412
471
|
## Переменные окружения
|
|
413
472
|
|
|
414
473
|
```bash
|
package/bin/iola.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -97,8 +97,11 @@ const COMMANDS = new Map([
|
|
|
97
97
|
["mcp", handleMcp],
|
|
98
98
|
["cache", handleCache],
|
|
99
99
|
["sync", handleSync],
|
|
100
|
+
["diff", handleDiff],
|
|
100
101
|
["views", handleViews],
|
|
101
102
|
["view", handleView],
|
|
103
|
+
["card", handleCard],
|
|
104
|
+
["quality", handleQuality],
|
|
102
105
|
["report", handleReport],
|
|
103
106
|
["privacy", handlePrivacy],
|
|
104
107
|
["backup", handleBackup],
|
|
@@ -163,6 +166,11 @@ Usage:
|
|
|
163
166
|
iola mcp list|status|install|remove
|
|
164
167
|
iola cache status|warm|clear
|
|
165
168
|
iola sync [--dataset schools|kindergartens]
|
|
169
|
+
iola sync status
|
|
170
|
+
iola diff [schools|kindergartens]
|
|
171
|
+
iola card schools 1215067180
|
|
172
|
+
iola card "школа 29"
|
|
173
|
+
iola quality [schools|kindergartens|missing-phones|invalid-emails|duplicate-inn]
|
|
166
174
|
iola views
|
|
167
175
|
iola view NAME [--format table|json|csv] [--output FILE]
|
|
168
176
|
iola report schools-summary|education-contacts|missing-phones|licenses
|
|
@@ -175,7 +183,7 @@ Usage:
|
|
|
175
183
|
iola config set api.mcpBaseUrl URL
|
|
176
184
|
iola config reset
|
|
177
185
|
iola update
|
|
178
|
-
iola ask TEXT [--profile NAME] [--model MODEL] [--output FILE] [--schema json|table] [--events] [--no-history]
|
|
186
|
+
iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history]
|
|
179
187
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
180
188
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
181
189
|
iola ai context TEXT [--json]
|
|
@@ -324,7 +332,7 @@ async function handleAgentLine(line, state) {
|
|
|
324
332
|
return false;
|
|
325
333
|
}
|
|
326
334
|
|
|
327
|
-
if (command === "views" || command === "view" || command === "report" || command === "privacy" || command === "backup" || command === "alias" || command === "run") {
|
|
335
|
+
if (command === "diff" || command === "card" || command === "quality" || command === "views" || command === "view" || command === "report" || command === "privacy" || command === "backup" || command === "alias" || command === "run") {
|
|
328
336
|
await COMMANDS.get(command)(args);
|
|
329
337
|
return false;
|
|
330
338
|
}
|
|
@@ -416,6 +424,7 @@ async function handleAgentLine(line, state) {
|
|
|
416
424
|
mcp: ["mcp", args],
|
|
417
425
|
cache: ["cache", args],
|
|
418
426
|
sync: ["sync", args],
|
|
427
|
+
diff: ["diff", args],
|
|
419
428
|
config: ["config", args],
|
|
420
429
|
layers: ["layers", args],
|
|
421
430
|
data: ["data", args],
|
|
@@ -449,6 +458,9 @@ function printAgentHelp() {
|
|
|
449
458
|
/mcp status
|
|
450
459
|
/cache status
|
|
451
460
|
/sync
|
|
461
|
+
/diff
|
|
462
|
+
/card школа 29
|
|
463
|
+
/quality
|
|
452
464
|
/views
|
|
453
465
|
/config get
|
|
454
466
|
/config set api.baseUrl URL
|
|
@@ -1109,6 +1121,16 @@ async function handleCache(args) {
|
|
|
1109
1121
|
}
|
|
1110
1122
|
|
|
1111
1123
|
async function handleSync(args) {
|
|
1124
|
+
const [action] = args;
|
|
1125
|
+
if (action === "status") {
|
|
1126
|
+
printTable(getSyncStatus(), [
|
|
1127
|
+
["dataset", "Слой"],
|
|
1128
|
+
["records", "Записей"],
|
|
1129
|
+
["last_sync", "Последний sync"],
|
|
1130
|
+
["status", "Статус"],
|
|
1131
|
+
]);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1112
1134
|
const options = parseOptions(args);
|
|
1113
1135
|
const datasets = options.dataset ? [options.dataset] : Object.keys(DATASETS);
|
|
1114
1136
|
const rows = [];
|
|
@@ -1123,6 +1145,44 @@ async function handleSync(args) {
|
|
|
1123
1145
|
]);
|
|
1124
1146
|
}
|
|
1125
1147
|
|
|
1148
|
+
async function handleDiff(args) {
|
|
1149
|
+
const [dataset] = args;
|
|
1150
|
+
const rows = listSyncChanges(dataset);
|
|
1151
|
+
printTable(rows, [
|
|
1152
|
+
["created_at", "Дата"],
|
|
1153
|
+
["dataset", "Слой"],
|
|
1154
|
+
["change_type", "Тип"],
|
|
1155
|
+
["record_key", "Ключ"],
|
|
1156
|
+
["summary", "Сводка"],
|
|
1157
|
+
]);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async function handleCard(args) {
|
|
1161
|
+
await ensureLocalData();
|
|
1162
|
+
const options = parseOptions(args);
|
|
1163
|
+
const query = args.join(" ").trim();
|
|
1164
|
+
if (!query) throw new Error('Пример: iola card "школа 29"');
|
|
1165
|
+
const item = findCard(query);
|
|
1166
|
+
if (!item) throw new Error(`Объект не найден: ${query}`);
|
|
1167
|
+
if (options.json) {
|
|
1168
|
+
printJson(item);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
printKeyValue(item);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async function handleQuality(args) {
|
|
1175
|
+
const [scope = "all"] = args;
|
|
1176
|
+
await ensureLocalData();
|
|
1177
|
+
const rows = runQuality(scope);
|
|
1178
|
+
printTable(rows, [
|
|
1179
|
+
["check", "Проверка"],
|
|
1180
|
+
["dataset", "Слой"],
|
|
1181
|
+
["count", "Кол-во"],
|
|
1182
|
+
["sample", "Пример"],
|
|
1183
|
+
]);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1126
1186
|
async function handleViews(args) {
|
|
1127
1187
|
const [action, name] = args;
|
|
1128
1188
|
if (action === "delete" || action === "remove") {
|
|
@@ -1803,6 +1863,16 @@ function initDatabase() {
|
|
|
1803
1863
|
message TEXT,
|
|
1804
1864
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1805
1865
|
);
|
|
1866
|
+
CREATE TABLE IF NOT EXISTS sync_changes (
|
|
1867
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1868
|
+
dataset TEXT NOT NULL,
|
|
1869
|
+
record_key TEXT NOT NULL,
|
|
1870
|
+
change_type TEXT NOT NULL,
|
|
1871
|
+
old_json TEXT,
|
|
1872
|
+
new_json TEXT,
|
|
1873
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1874
|
+
);
|
|
1875
|
+
CREATE INDEX IF NOT EXISTS idx_sync_changes_dataset_created_at ON sync_changes(dataset, created_at DESC);
|
|
1806
1876
|
CREATE TABLE IF NOT EXISTS aliases (
|
|
1807
1877
|
name TEXT PRIMARY KEY,
|
|
1808
1878
|
command TEXT NOT NULL,
|
|
@@ -2174,17 +2244,29 @@ function saveLocalRecords(dataset, items) {
|
|
|
2174
2244
|
initDatabase();
|
|
2175
2245
|
const db = openDatabase();
|
|
2176
2246
|
try {
|
|
2247
|
+
const oldRows = db.prepare("SELECT record_key, record_json FROM local_records WHERE dataset = ?").all(dataset);
|
|
2248
|
+
const oldMap = new Map(oldRows.map((row) => [row.record_key, row.record_json]));
|
|
2249
|
+
const newKeys = new Set();
|
|
2177
2250
|
db.prepare("DELETE FROM local_records WHERE dataset = ?").run(dataset);
|
|
2178
2251
|
db.prepare("DELETE FROM local_records_fts WHERE dataset = ?").run(dataset);
|
|
2179
2252
|
const insert = db.prepare("INSERT INTO local_records(dataset, record_key, record_json, searchable_text, synced_at) VALUES (?, ?, ?, ?, datetime('now'))");
|
|
2180
2253
|
const insertFts = db.prepare("INSERT INTO local_records_fts(dataset, record_key, searchable_text) VALUES (?, ?, ?)");
|
|
2254
|
+
const insertChange = db.prepare("INSERT INTO sync_changes(dataset, record_key, change_type, old_json, new_json) VALUES (?, ?, ?, ?, ?)");
|
|
2181
2255
|
for (const item of items) {
|
|
2182
2256
|
const summary = selectPublicSummary(item);
|
|
2183
2257
|
const key = String(summary.inn || item.id || `${dataset}-${Math.random()}`);
|
|
2258
|
+
newKeys.add(key);
|
|
2259
|
+
const newJson = JSON.stringify(item);
|
|
2260
|
+
const oldJson = oldMap.get(key);
|
|
2261
|
+
if (!oldJson) insertChange.run(dataset, key, "added", null, newJson);
|
|
2262
|
+
else if (oldJson !== newJson) insertChange.run(dataset, key, "changed", oldJson, newJson);
|
|
2184
2263
|
const text = JSON.stringify(summary).toLocaleLowerCase("ru-RU");
|
|
2185
|
-
insert.run(dataset, key,
|
|
2264
|
+
insert.run(dataset, key, newJson, text);
|
|
2186
2265
|
insertFts.run(dataset, key, text);
|
|
2187
2266
|
}
|
|
2267
|
+
for (const [key, oldJson] of oldMap.entries()) {
|
|
2268
|
+
if (!newKeys.has(key)) insertChange.run(dataset, key, "removed", oldJson, null);
|
|
2269
|
+
}
|
|
2188
2270
|
} finally {
|
|
2189
2271
|
db.close();
|
|
2190
2272
|
}
|
|
@@ -2213,6 +2295,14 @@ function searchLocalRecords(query, options = {}) {
|
|
|
2213
2295
|
const dataset = options.dataset || "all";
|
|
2214
2296
|
const limit = Number(options.limit || 20);
|
|
2215
2297
|
try {
|
|
2298
|
+
if (options.fts && query) {
|
|
2299
|
+
const ftsQuery = query.split(/\s+/).filter(Boolean).map((term) => `"${term.replace(/"/g, "")}"`).join(" ");
|
|
2300
|
+
const params = dataset === "all" ? [ftsQuery, limit] : [ftsQuery, dataset, limit];
|
|
2301
|
+
const sql = dataset === "all"
|
|
2302
|
+
? "SELECT r.record_json FROM local_records_fts f JOIN local_records r ON r.dataset=f.dataset AND r.record_key=f.record_key WHERE local_records_fts MATCH ? LIMIT ?"
|
|
2303
|
+
: "SELECT r.record_json FROM local_records_fts f JOIN local_records r ON r.dataset=f.dataset AND r.record_key=f.record_key WHERE local_records_fts MATCH ? AND f.dataset = ? LIMIT ?";
|
|
2304
|
+
return db.prepare(sql).all(...params).map((row) => selectPublicSummary(JSON.parse(row.record_json)));
|
|
2305
|
+
}
|
|
2216
2306
|
const params = [];
|
|
2217
2307
|
let sql = "SELECT dataset, record_json FROM local_records";
|
|
2218
2308
|
const where = [];
|
|
@@ -2233,6 +2323,76 @@ function searchLocalRecords(query, options = {}) {
|
|
|
2233
2323
|
}
|
|
2234
2324
|
}
|
|
2235
2325
|
|
|
2326
|
+
function getSyncStatus() {
|
|
2327
|
+
initDatabase();
|
|
2328
|
+
const db = openDatabase();
|
|
2329
|
+
try {
|
|
2330
|
+
return Object.keys(DATASETS).map((dataset) => {
|
|
2331
|
+
const records = db.prepare("SELECT COUNT(*) AS count FROM local_records WHERE dataset = ?").get(dataset);
|
|
2332
|
+
const run = db.prepare("SELECT status, created_at FROM sync_runs WHERE dataset = ? ORDER BY id DESC LIMIT 1").get(dataset);
|
|
2333
|
+
return { dataset, records: records?.count || 0, last_sync: run?.created_at || "-", status: run?.status || "never" };
|
|
2334
|
+
});
|
|
2335
|
+
} finally {
|
|
2336
|
+
db.close();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function listSyncChanges(dataset) {
|
|
2341
|
+
initDatabase();
|
|
2342
|
+
const db = openDatabase();
|
|
2343
|
+
try {
|
|
2344
|
+
const rows = dataset
|
|
2345
|
+
? db.prepare("SELECT * FROM sync_changes WHERE dataset = ? ORDER BY id DESC LIMIT 50").all(dataset)
|
|
2346
|
+
: db.prepare("SELECT * FROM sync_changes ORDER BY id DESC LIMIT 50").all();
|
|
2347
|
+
return rows.map((row) => ({
|
|
2348
|
+
...row,
|
|
2349
|
+
summary: summarizeChange(row),
|
|
2350
|
+
}));
|
|
2351
|
+
} finally {
|
|
2352
|
+
db.close();
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function summarizeChange(row) {
|
|
2357
|
+
const payload = row.new_json || row.old_json;
|
|
2358
|
+
if (!payload) return "-";
|
|
2359
|
+
try {
|
|
2360
|
+
const item = selectPublicSummary(JSON.parse(payload));
|
|
2361
|
+
return item.name || item.inn || "-";
|
|
2362
|
+
} catch {
|
|
2363
|
+
return "-";
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function findCard(query) {
|
|
2368
|
+
const normalized = query.toLocaleLowerCase("ru-RU");
|
|
2369
|
+
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
2370
|
+
const inn = normalized.match(/\b\d{10,12}\b/)?.[0];
|
|
2371
|
+
const number = normalized.match(/\b\d{1,3}\b/)?.[0];
|
|
2372
|
+
const rows = searchLocalRecords(inn || number || query, { dataset, limit: 20, fts: false });
|
|
2373
|
+
if (inn) return rows.find((row) => String(row.inn) === inn) || null;
|
|
2374
|
+
if (number) return rows.find((row) => String(row.name || "").includes(`№ ${number}`) || String(row.name || "").includes(`№${number}`)) || rows[0] || null;
|
|
2375
|
+
return rows[0] || null;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
function runQuality(scope) {
|
|
2379
|
+
const datasets = ["schools", "kindergartens"];
|
|
2380
|
+
const rows = [];
|
|
2381
|
+
for (const dataset of datasets) {
|
|
2382
|
+
if (scope !== "all" && scope !== dataset && !scope.includes("-")) continue;
|
|
2383
|
+
const records = searchLocalRecords("", { dataset, limit: 1000 });
|
|
2384
|
+
const missingPhones = records.filter((item) => !item.phone || item.phone === "-");
|
|
2385
|
+
const invalidEmails = records.filter((item) => item.email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(item.email));
|
|
2386
|
+
const innCounts = new Map();
|
|
2387
|
+
for (const item of records) innCounts.set(item.inn, (innCounts.get(item.inn) || 0) + 1);
|
|
2388
|
+
const duplicateInn = records.filter((item) => item.inn && innCounts.get(item.inn) > 1);
|
|
2389
|
+
if (scope === "all" || scope === dataset || scope === "missing-phones") rows.push({ check: "missing-phones", dataset, count: missingPhones.length, sample: missingPhones[0]?.name || "-" });
|
|
2390
|
+
if (scope === "all" || scope === dataset || scope === "invalid-emails") rows.push({ check: "invalid-emails", dataset, count: invalidEmails.length, sample: invalidEmails[0]?.email || "-" });
|
|
2391
|
+
if (scope === "all" || scope === dataset || scope === "duplicate-inn") rows.push({ check: "duplicate-inn", dataset, count: duplicateInn.length, sample: duplicateInn[0]?.inn || "-" });
|
|
2392
|
+
}
|
|
2393
|
+
return rows;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2236
2396
|
function saveView(name, dataset, args) {
|
|
2237
2397
|
initDatabase();
|
|
2238
2398
|
const db = openDatabase();
|
|
@@ -2466,6 +2626,9 @@ async function aiAsk(args, context = {}) {
|
|
|
2466
2626
|
|
|
2467
2627
|
const config = await loadConfig();
|
|
2468
2628
|
const providerConfig = resolveAiProfile(config, options);
|
|
2629
|
+
if (options.tools && providerConfig.provider === "ollama") {
|
|
2630
|
+
return localToolAsk(question, providerConfig, options);
|
|
2631
|
+
}
|
|
2469
2632
|
applyRuntimeConfig(providerConfig, options.config);
|
|
2470
2633
|
const dataContext = await buildDataContext(question);
|
|
2471
2634
|
emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
|
|
@@ -2529,6 +2692,132 @@ function resolveAiProfile(config, options = {}) {
|
|
|
2529
2692
|
};
|
|
2530
2693
|
}
|
|
2531
2694
|
|
|
2695
|
+
async function localToolAsk(question, providerConfig, options) {
|
|
2696
|
+
await ensureLocalData();
|
|
2697
|
+
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
2698
|
+
const validated = validateToolPlan(plan);
|
|
2699
|
+
const result = await executeToolPlan(validated);
|
|
2700
|
+
const answer = formatToolResult(result, options);
|
|
2701
|
+
|
|
2702
|
+
if (!options["no-history"] && isFeatureEnabled("sqlite-history")) {
|
|
2703
|
+
recordAskHistory({
|
|
2704
|
+
question,
|
|
2705
|
+
answer,
|
|
2706
|
+
providerConfig,
|
|
2707
|
+
dataContext: { tool_plan: validated, tool_result: result },
|
|
2708
|
+
error: "",
|
|
2709
|
+
sessionId: null,
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
emitEvent(options, "tool_plan", { plan: validated });
|
|
2714
|
+
if (options.output) await writeFile(options.output, answer, "utf8");
|
|
2715
|
+
if (options.format === "json" || options.schema === "json") {
|
|
2716
|
+
printJson({ answer, plan: validated, result });
|
|
2717
|
+
} else {
|
|
2718
|
+
console.log(answer);
|
|
2719
|
+
}
|
|
2720
|
+
return answer;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
async function buildLocalToolPlan(question, providerConfig, options) {
|
|
2724
|
+
const mode = options.reasoning || "verify";
|
|
2725
|
+
const prompt = [
|
|
2726
|
+
"Ты планировщик CLI iola. Верни только JSON.",
|
|
2727
|
+
"Доступные tools: search_local, get_card, export_data, run_report, save_view.",
|
|
2728
|
+
"Схема: {\"steps\":[{\"tool\":\"search_local\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
2729
|
+
"Для выгрузки CSV добавь export_data с format=csv и output, если пользователь назвал файл.",
|
|
2730
|
+
`Вопрос: ${question}`,
|
|
2731
|
+
].join("\n");
|
|
2732
|
+
|
|
2733
|
+
try {
|
|
2734
|
+
const raw = await callOllama(providerConfig, [{ role: "user", content: prompt }]);
|
|
2735
|
+
const parsed = parseJsonObject(raw);
|
|
2736
|
+
if (mode === "vote") {
|
|
2737
|
+
return chooseBestPlan([parsed, inferToolPlan(question)]);
|
|
2738
|
+
}
|
|
2739
|
+
return parsed;
|
|
2740
|
+
} catch {
|
|
2741
|
+
return inferToolPlan(question);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function parseJsonObject(text) {
|
|
2746
|
+
const match = String(text).match(/\{[\s\S]*\}/);
|
|
2747
|
+
if (!match) throw new Error("JSON-план не найден.");
|
|
2748
|
+
return JSON.parse(match[0]);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
function inferToolPlan(question) {
|
|
2752
|
+
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
2753
|
+
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
2754
|
+
const steps = [];
|
|
2755
|
+
if (normalized.includes("без телефона")) {
|
|
2756
|
+
steps.push({ tool: "run_report", args: { name: "missing-phones" } });
|
|
2757
|
+
} else {
|
|
2758
|
+
const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
|
|
2759
|
+
steps.push({ tool: "search_local", args: { dataset, query, limit: 20 } });
|
|
2760
|
+
}
|
|
2761
|
+
if (normalized.includes("csv") || normalized.includes("выгруз")) {
|
|
2762
|
+
steps.push({ tool: "export_data", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
|
|
2763
|
+
}
|
|
2764
|
+
return { steps };
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
function chooseBestPlan(plans) {
|
|
2768
|
+
return plans.find((plan) => {
|
|
2769
|
+
try {
|
|
2770
|
+
validateToolPlan(plan);
|
|
2771
|
+
return true;
|
|
2772
|
+
} catch {
|
|
2773
|
+
return false;
|
|
2774
|
+
}
|
|
2775
|
+
}) || plans.at(-1);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
function validateToolPlan(plan) {
|
|
2779
|
+
const allowed = new Set(["search_local", "get_card", "export_data", "run_report", "save_view"]);
|
|
2780
|
+
if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
|
|
2781
|
+
for (const step of plan.steps) {
|
|
2782
|
+
if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
2783
|
+
}
|
|
2784
|
+
return plan;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
async function executeToolPlan(plan) {
|
|
2788
|
+
let current = [];
|
|
2789
|
+
const outputs = [];
|
|
2790
|
+
for (const step of plan.steps) {
|
|
2791
|
+
if (step.tool === "search_local") {
|
|
2792
|
+
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
2793
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
2794
|
+
} else if (step.tool === "get_card") {
|
|
2795
|
+
const card = findCard(step.args?.query || "");
|
|
2796
|
+
current = card ? [card] : [];
|
|
2797
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
2798
|
+
} else if (step.tool === "run_report") {
|
|
2799
|
+
current = runQuality(step.args?.name || "all");
|
|
2800
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
2801
|
+
} else if (step.tool === "save_view") {
|
|
2802
|
+
saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
|
|
2803
|
+
outputs.push({ tool: step.tool, saved: step.args?.name });
|
|
2804
|
+
} else if (step.tool === "export_data") {
|
|
2805
|
+
const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
|
|
2806
|
+
await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
|
|
2807
|
+
outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
return { rows: current, outputs };
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function formatToolResult(result, options) {
|
|
2814
|
+
if (options.schema === "json") return JSON.stringify(result, null, 2);
|
|
2815
|
+
const exported = result.outputs.find((item) => item.output);
|
|
2816
|
+
if (exported) return `Готово. Файл сохранен: ${exported.output}. Записей: ${exported.rows}`;
|
|
2817
|
+
if (!result.rows.length) return "Данных не найдено.";
|
|
2818
|
+
return result.rows.slice(0, 10).map((row) => `${row.name || row.check}: ${row.address || row.count || ""}`).join("\n");
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2532
2821
|
function applyRuntimeConfig(target, value) {
|
|
2533
2822
|
if (!value) {
|
|
2534
2823
|
return;
|
|
@@ -2912,7 +3201,7 @@ async function listDataset(dataset, args) {
|
|
|
2912
3201
|
params.set("offset", options.offset || "0");
|
|
2913
3202
|
|
|
2914
3203
|
const data = options.local
|
|
2915
|
-
|
|
3204
|
+
? searchLocalRecords(options.search || options._.join(" ") || "", { dataset, limit: Number(options.limit || 20), fts: options.fts })
|
|
2916
3205
|
: normalizeItems(await fetchJsonMaybeCached(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`, options));
|
|
2917
3206
|
const items = data;
|
|
2918
3207
|
const filtered = applyDatasetFilters(items, options);
|
|
@@ -2968,8 +3257,8 @@ async function searchAll(args) {
|
|
|
2968
3257
|
const limit = Number(options.limit || 5);
|
|
2969
3258
|
const [schools, kindergartens] = options.local
|
|
2970
3259
|
? [
|
|
2971
|
-
searchLocalRecords(query, { dataset: "schools", limit }),
|
|
2972
|
-
searchLocalRecords(query, { dataset: "kindergartens", limit }),
|
|
3260
|
+
searchLocalRecords(query, { dataset: "schools", limit, fts: options.fts }),
|
|
3261
|
+
searchLocalRecords(query, { dataset: "kindergartens", limit, fts: options.fts }),
|
|
2973
3262
|
]
|
|
2974
3263
|
: await Promise.all([
|
|
2975
3264
|
fetchJsonMaybeCached(`${await getApiBaseUrl()}/schools?limit=100&offset=0`, options),
|
|
@@ -3018,12 +3307,12 @@ function parseOptions(args) {
|
|
|
3018
3307
|
|
|
3019
3308
|
for (let index = 0; index < args.length; index += 1) {
|
|
3020
3309
|
const arg = args[index];
|
|
3021
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache") {
|
|
3310
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--fts") {
|
|
3022
3311
|
result[arg.slice(2)] = true;
|
|
3023
3312
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
3024
3313
|
result.check = true;
|
|
3025
3314
|
result[arg.slice(2)] = true;
|
|
3026
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save") {
|
|
3315
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning") {
|
|
3027
3316
|
result[arg.slice(2)] = args[index + 1];
|
|
3028
3317
|
index += 1;
|
|
3029
3318
|
} else {
|