@iola_adm/iola-cli 0.1.84 → 0.1.86
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 +13 -5
- package/package.json +2 -3
- package/src/cli.js +448 -48
- package/src/iola_hf_runner.py +136 -0
- package/experiments/small-model-concepts/README.md +0 -34
- package/experiments/small-model-concepts/concepts/agent-consensus/README.md +0 -25
- package/experiments/small-model-concepts/concepts/hybrid/README.md +0 -23
- package/experiments/small-model-concepts/concepts/model-architecture/README.md +0 -42
- package/experiments/small-model-concepts/datasets/adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/datasets/simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/lib/common.js +0 -192
- package/experiments/small-model-concepts/lib/concepts.js +0 -210
- package/experiments/small-model-concepts/results/latest/conditional-memory-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/conditional-memory-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/council-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/council-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/early-exit-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/early-exit-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/escalation-ladder-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/escalation-ladder-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/memory-verified-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/memory-verified-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/skill-router-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/skill-router-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/sparse-escalation-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/sparse-escalation-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/strict-skill-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/strict-skill-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/summary.json +0 -313
- package/experiments/small-model-concepts/results/latest/verify-adversarial-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest/verify-simple-facts.jsonl +0 -100
- package/experiments/small-model-concepts/results/latest-summary.json +0 -313
- package/experiments/small-model-concepts/scripts/generate-datasets.js +0 -199
- package/experiments/small-model-concepts/scripts/run-evaluation.js +0 -133
- package/experiments/small-model-concepts/scripts/summarize-results.js +0 -19
package/src/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { inflateRawSync, inflateSync } from "node:zlib";
|
|
13
13
|
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
15
16
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
16
17
|
const MIN_NODE_VERSION = "22.5.0";
|
|
@@ -20,20 +21,25 @@ const LAST_GOOD_CONFIG_FILE = path.join(CONFIG_DIR, "config.last-good.json");
|
|
|
20
21
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
21
22
|
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
22
23
|
const DB_SCHEMA_VERSION = 8;
|
|
24
|
+
const IOLA_LOCAL_MODEL = "iola-router-1b";
|
|
25
|
+
const IOLA_LOCAL_OLLAMA_MODEL = "gemma3:1b";
|
|
26
|
+
const IOLA_ROUTER_HF_REPO = process.env.IOLA_ROUTER_HF_REPO || "LMSerg/iola-1b-router-2026-05-28-merged";
|
|
27
|
+
const IOLA_MODEL_DIR = path.join(CONFIG_DIR, "models", "router");
|
|
28
|
+
const IOLA_MODEL_RUNTIME_DIR = path.join(CONFIG_DIR, "model-runtime");
|
|
29
|
+
const IOLA_MODEL_RUNNER = path.resolve(__dirname, "iola_hf_runner.py");
|
|
23
30
|
const PROJECT_IOLA_DIR = path.join(process.cwd(), ".iola");
|
|
24
31
|
const PROJECT_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "config.json");
|
|
25
32
|
const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
|
|
26
33
|
const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
|
|
27
34
|
const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
|
|
28
35
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
29
|
-
const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
|
|
36
|
+
const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open"];
|
|
30
37
|
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
31
38
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
32
39
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
33
40
|
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
34
41
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
35
42
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
36
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
43
|
const BUILTIN_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
38
44
|
const USER_SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
39
45
|
const PROJECT_CONTEXT_FILE = path.join(process.cwd(), "IOLA.md");
|
|
@@ -102,9 +108,9 @@ const SKILL_BUNDLES = {
|
|
|
102
108
|
requirements: ["files mode read-only/workspace-write", "7-Zip для архивов"],
|
|
103
109
|
},
|
|
104
110
|
"local-agent": {
|
|
105
|
-
description: "Локальная модель
|
|
111
|
+
description: "Локальная модель IOLA с проверочным reasoning и локальными tools.",
|
|
106
112
|
skills: ["local-model", "open-data"],
|
|
107
|
-
requirements: ["
|
|
113
|
+
requirements: ["Python", "локальная модель"],
|
|
108
114
|
},
|
|
109
115
|
};
|
|
110
116
|
let onboardRanThisProcess = false;
|
|
@@ -115,14 +121,14 @@ const DEFAULT_AI_CONFIG = {
|
|
|
115
121
|
},
|
|
116
122
|
ai: {
|
|
117
123
|
activeProfile: "local",
|
|
118
|
-
provider: "
|
|
119
|
-
model:
|
|
120
|
-
baseUrl: "http://127.0.0.1:11434",
|
|
124
|
+
provider: "iola",
|
|
125
|
+
model: IOLA_LOCAL_MODEL,
|
|
121
126
|
profiles: {
|
|
122
127
|
local: {
|
|
123
|
-
provider: "
|
|
124
|
-
model:
|
|
125
|
-
|
|
128
|
+
provider: "iola",
|
|
129
|
+
model: IOLA_LOCAL_MODEL,
|
|
130
|
+
repo: IOLA_ROUTER_HF_REPO,
|
|
131
|
+
modelDir: IOLA_MODEL_DIR,
|
|
126
132
|
},
|
|
127
133
|
openai: {
|
|
128
134
|
provider: "openai",
|
|
@@ -146,6 +152,8 @@ const DEFAULT_AI_CONFIG = {
|
|
|
146
152
|
permissions: {
|
|
147
153
|
localTools: {
|
|
148
154
|
search_data: true,
|
|
155
|
+
search_entities: true,
|
|
156
|
+
resolve_entity_field: true,
|
|
149
157
|
get_card: true,
|
|
150
158
|
export_report: true,
|
|
151
159
|
file_read: false,
|
|
@@ -434,6 +442,8 @@ export async function main(argv) {
|
|
|
434
442
|
throw new Error(`Нужен Node.js ${MIN_NODE_VERSION} или новее. Сейчас: ${nodeStatus.current}. Запустите: iola init --upgrade-node`);
|
|
435
443
|
}
|
|
436
444
|
|
|
445
|
+
await maybeRefreshIolaModelForCommand(command, args);
|
|
446
|
+
|
|
437
447
|
const handler = COMMANDS.get(command);
|
|
438
448
|
|
|
439
449
|
if (!handler) {
|
|
@@ -448,6 +458,23 @@ export async function main(argv) {
|
|
|
448
458
|
await handler(runtime.debugFile ? [...args, "--debug-file", runtime.debugFile] : args);
|
|
449
459
|
}
|
|
450
460
|
|
|
461
|
+
async function maybeRefreshIolaModelForCommand(command, args = []) {
|
|
462
|
+
if (process.env.IOLA_SKIP_MODEL_CHECK === "1") return;
|
|
463
|
+
const aiRuntimeCommands = new Set(["ask", "agent", "chat"]);
|
|
464
|
+
const isAiCommand = command === "ai" && !["setup", "models", "key", "profile", "profiles", "doctor"].includes(args[0]);
|
|
465
|
+
if (!aiRuntimeCommands.has(command) && !isAiCommand) return;
|
|
466
|
+
const config = await loadConfig();
|
|
467
|
+
const profile = config.ai.profiles?.[getActiveProfileName(config)];
|
|
468
|
+
if (profile?.provider !== "iola" && config.ai.provider !== "iola") return;
|
|
469
|
+
await ensureIolaModelFresh({
|
|
470
|
+
repo: profile?.repo || IOLA_ROUTER_HF_REPO,
|
|
471
|
+
modelDir: profile?.modelDir || IOLA_MODEL_DIR,
|
|
472
|
+
quiet: true,
|
|
473
|
+
}).catch((error) => {
|
|
474
|
+
if (process.env.IOLA_DEBUG) console.error(error instanceof Error ? error.message : String(error));
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
451
478
|
async function showHelp() {
|
|
452
479
|
await showBanner();
|
|
453
480
|
console.log(`iola - CLI и AI-агент городского округа "Город Йошкар-Ола"
|
|
@@ -552,7 +579,7 @@ Usage:
|
|
|
552
579
|
iola update
|
|
553
580
|
iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--files] [--plan] [--trace] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
|
|
554
581
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
555
|
-
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
582
|
+
iola ai ask TEXT [--provider iola|ollama|openai|openrouter] [--model MODEL]
|
|
556
583
|
iola ai context TEXT [--json]
|
|
557
584
|
iola ai key set openai
|
|
558
585
|
iola ai key set openrouter
|
|
@@ -562,9 +589,10 @@ Usage:
|
|
|
562
589
|
iola ai profile add NAME --provider PROVIDER --model MODEL
|
|
563
590
|
iola ai profile use NAME
|
|
564
591
|
iola ai profile delete NAME
|
|
565
|
-
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
592
|
+
iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
|
|
566
593
|
iola ai doctor [--json]
|
|
567
594
|
iola ai setup
|
|
595
|
+
iola ai setup iola [--yes]
|
|
568
596
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
569
597
|
iola health [--json]
|
|
570
598
|
iola layers [--json]
|
|
@@ -682,9 +710,11 @@ async function getAiReadiness() {
|
|
|
682
710
|
hasUsableOllamaModel(),
|
|
683
711
|
hasUsableCodexAuth(),
|
|
684
712
|
]);
|
|
713
|
+
const iola = await hasUsableIolaModel();
|
|
685
714
|
const openai = Boolean(process.env.OPENAI_API_KEY || secrets.openai?.apiKey);
|
|
686
715
|
const openrouter = Boolean(process.env.OPENROUTER_API_KEY || secrets.openrouter?.apiKey);
|
|
687
716
|
const providerReady = {
|
|
717
|
+
iola,
|
|
688
718
|
ollama,
|
|
689
719
|
openai,
|
|
690
720
|
openrouter,
|
|
@@ -695,8 +725,9 @@ async function getAiReadiness() {
|
|
|
695
725
|
activeProfile: activeProfileName,
|
|
696
726
|
activeProvider: activeProfile.provider || "-",
|
|
697
727
|
activeModel: activeProfile.model || "-",
|
|
698
|
-
anyReady: Boolean(ollama || openai || openrouter || codex),
|
|
728
|
+
anyReady: Boolean(iola || ollama || openai || openrouter || codex),
|
|
699
729
|
profiles: config.ai.profiles || {},
|
|
730
|
+
iola,
|
|
700
731
|
ollama,
|
|
701
732
|
openai,
|
|
702
733
|
openrouter,
|
|
@@ -705,7 +736,7 @@ async function getAiReadiness() {
|
|
|
705
736
|
}
|
|
706
737
|
|
|
707
738
|
function getFallbackAiProfile(readiness) {
|
|
708
|
-
const priority = ["openai", "openrouter", "codex", "ollama"];
|
|
739
|
+
const priority = ["iola", "openai", "openrouter", "codex", "ollama"];
|
|
709
740
|
for (const provider of priority) {
|
|
710
741
|
if (!readiness[provider]) continue;
|
|
711
742
|
const entry = Object.entries(readiness.profiles || {}).find(([, profile]) => profile.provider === provider);
|
|
@@ -1500,6 +1531,7 @@ function buildAgentStatusLine(state) {
|
|
|
1500
1531
|
const ai = state.aiStatus;
|
|
1501
1532
|
if (!ai) return cwd;
|
|
1502
1533
|
const kind = {
|
|
1534
|
+
iola: "IOLA local",
|
|
1503
1535
|
ollama: "локальная",
|
|
1504
1536
|
openai: "API",
|
|
1505
1537
|
openrouter: "API",
|
|
@@ -1885,6 +1917,10 @@ async function upgradeNodeWithInstaller() {
|
|
|
1885
1917
|
}
|
|
1886
1918
|
|
|
1887
1919
|
async function checkConfiguredModel(config) {
|
|
1920
|
+
if (config.ai.provider === "iola") {
|
|
1921
|
+
return await hasUsableIolaModel() ? "installed" : "missing";
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1888
1924
|
if (config.ai.provider !== "ollama") {
|
|
1889
1925
|
return "external-api";
|
|
1890
1926
|
}
|
|
@@ -1934,6 +1970,7 @@ async function initCli(args = []) {
|
|
|
1934
1970
|
if (!process.stdin.isTTY || options.yes) {
|
|
1935
1971
|
console.log("");
|
|
1936
1972
|
console.log("Для настройки AI используйте:");
|
|
1973
|
+
console.log(" iola ai setup iola --yes");
|
|
1937
1974
|
console.log(" iola ai setup ollama");
|
|
1938
1975
|
console.log(" iola ai key set openai");
|
|
1939
1976
|
console.log(" iola ai setup openai --model gpt-4.1-mini");
|
|
@@ -1957,19 +1994,20 @@ async function handleAi(args) {
|
|
|
1957
1994
|
if (subcommand === "help") {
|
|
1958
1995
|
await showBanner();
|
|
1959
1996
|
console.log(`AI-команды:
|
|
1960
|
-
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
1997
|
+
iola ai ask TEXT [--provider iola|ollama|openai|openrouter] [--model MODEL]
|
|
1961
1998
|
iola ai context TEXT [--json]
|
|
1962
1999
|
iola ai key set openai
|
|
1963
2000
|
iola ai key set openrouter
|
|
1964
2001
|
iola ai key status
|
|
1965
2002
|
iola ai key delete openai|openrouter
|
|
1966
2003
|
iola ai profiles
|
|
1967
|
-
iola ai profile add NAME --provider ollama|openai|openrouter|codex --model MODEL
|
|
2004
|
+
iola ai profile add NAME --provider iola|ollama|openai|openrouter|codex --model MODEL
|
|
1968
2005
|
iola ai profile use NAME
|
|
1969
2006
|
iola ai profile delete NAME
|
|
1970
|
-
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
2007
|
+
iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
|
|
1971
2008
|
iola ai doctor [--json]
|
|
1972
2009
|
iola ai setup
|
|
2010
|
+
iola ai setup iola [--yes]
|
|
1973
2011
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
1974
2012
|
iola ai setup openai [--model MODEL]
|
|
1975
2013
|
iola ai setup openrouter [--model MODEL]
|
|
@@ -3981,6 +4019,11 @@ async function aiSetup(args) {
|
|
|
3981
4019
|
return;
|
|
3982
4020
|
}
|
|
3983
4021
|
|
|
4022
|
+
if (provider === "iola") {
|
|
4023
|
+
await setupIolaLocal(args.slice(1));
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
3984
4027
|
if (provider === "ollama") {
|
|
3985
4028
|
await setupOllama(args.slice(1));
|
|
3986
4029
|
return;
|
|
@@ -4103,8 +4146,8 @@ async function aiModels(args) {
|
|
|
4103
4146
|
const [provider] = args;
|
|
4104
4147
|
const options = parseOptions(args.slice(1));
|
|
4105
4148
|
|
|
4106
|
-
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4107
|
-
throw new Error("Провайдер обязателен: iola ai models ollama|openai|openrouter|codex");
|
|
4149
|
+
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4150
|
+
throw new Error("Провайдер обязателен: iola ai models iola|ollama|openai|openrouter|codex");
|
|
4108
4151
|
}
|
|
4109
4152
|
|
|
4110
4153
|
const models = await listAiModels(provider);
|
|
@@ -4125,6 +4168,18 @@ async function aiModels(args) {
|
|
|
4125
4168
|
}
|
|
4126
4169
|
|
|
4127
4170
|
async function listAiModels(provider) {
|
|
4171
|
+
if (provider === "iola") {
|
|
4172
|
+
const state = readConfigLayerSync(getIolaModelStateFile(IOLA_MODEL_DIR)) || {};
|
|
4173
|
+
const remote = await getRemoteIolaModelRevision().catch(() => null);
|
|
4174
|
+
return [{
|
|
4175
|
+
id: IOLA_LOCAL_MODEL,
|
|
4176
|
+
provider: "iola",
|
|
4177
|
+
note: state.revision
|
|
4178
|
+
? `installed ${state.revision.slice(0, 12)}${remote?.sha && remote.sha !== state.revision ? ", update available" : ""}`
|
|
4179
|
+
: "not installed",
|
|
4180
|
+
}];
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4128
4183
|
if (provider === "ollama") {
|
|
4129
4184
|
try {
|
|
4130
4185
|
const config = await loadConfig();
|
|
@@ -4198,9 +4253,9 @@ async function listAiModels(provider) {
|
|
|
4198
4253
|
|
|
4199
4254
|
function getRecommendedOllamaModels(notePrefix = "recommended") {
|
|
4200
4255
|
return [
|
|
4256
|
+
{ id: IOLA_LOCAL_OLLAMA_MODEL, provider: "ollama", note: `${notePrefix} IOLA default low RAM` },
|
|
4201
4257
|
{ id: "qwen3:1.7b", provider: "ollama", note: `${notePrefix} recommended low RAM` },
|
|
4202
4258
|
{ id: "qwen3:4b", provider: "ollama", note: `${notePrefix} recommended balanced` },
|
|
4203
|
-
{ id: "gemma3:1b", provider: "ollama", note: `${notePrefix} Gemma low RAM` },
|
|
4204
4259
|
{ id: "gemma3:4b", provider: "ollama", note: `${notePrefix} Gemma balanced` },
|
|
4205
4260
|
{ id: "llama3.2:3b", provider: "ollama", note: `${notePrefix} legacy fallback` },
|
|
4206
4261
|
{ id: "llama3.2:1b", provider: "ollama", note: `${notePrefix} minimal fallback only` },
|
|
@@ -4249,8 +4304,8 @@ async function addAiProfile(name, args) {
|
|
|
4249
4304
|
const options = parseOptions(args);
|
|
4250
4305
|
const provider = options.provider;
|
|
4251
4306
|
|
|
4252
|
-
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4253
|
-
throw new Error("Провайдер должен быть ollama, openai, openrouter или codex.");
|
|
4307
|
+
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
4308
|
+
throw new Error("Провайдер должен быть iola, ollama, openai, openrouter или codex.");
|
|
4254
4309
|
}
|
|
4255
4310
|
|
|
4256
4311
|
const profile = buildProfileFromOptions(provider, options);
|
|
@@ -4329,7 +4384,7 @@ async function deleteAiProfile(name) {
|
|
|
4329
4384
|
}
|
|
4330
4385
|
|
|
4331
4386
|
function buildProfileFromOptions(provider, options) {
|
|
4332
|
-
const defaults = DEFAULT_AI_CONFIG.ai.profiles[provider === "ollama" ? "local" : provider];
|
|
4387
|
+
const defaults = DEFAULT_AI_CONFIG.ai.profiles[provider === "ollama" || provider === "iola" ? "local" : provider];
|
|
4333
4388
|
const profile = {
|
|
4334
4389
|
...defaults,
|
|
4335
4390
|
provider,
|
|
@@ -4340,6 +4395,11 @@ function buildProfileFromOptions(provider, options) {
|
|
|
4340
4395
|
profile.baseUrl = options["base-url"];
|
|
4341
4396
|
}
|
|
4342
4397
|
|
|
4398
|
+
if (provider === "iola") {
|
|
4399
|
+
profile.repo = options.repo || defaults.repo || IOLA_ROUTER_HF_REPO;
|
|
4400
|
+
profile.modelDir = options["model-dir"] || defaults.modelDir || IOLA_MODEL_DIR;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4343
4403
|
if (provider === "codex") {
|
|
4344
4404
|
profile.sandbox = options.sandbox || defaults.sandbox || "read-only";
|
|
4345
4405
|
profile.approval = options.approval || defaults.approval || "never";
|
|
@@ -4363,17 +4423,18 @@ async function useAiProvider(args) {
|
|
|
4363
4423
|
|
|
4364
4424
|
const provider = providerOrProfile;
|
|
4365
4425
|
|
|
4366
|
-
if (provider !== "ollama" && provider !== "openai" && provider !== "openrouter" && provider !== "codex") {
|
|
4367
|
-
throw new Error("Провайдер должен быть ollama, openai, openrouter, codex или именем AI-профиля.");
|
|
4426
|
+
if (provider !== "iola" && provider !== "ollama" && provider !== "openai" && provider !== "openrouter" && provider !== "codex") {
|
|
4427
|
+
throw new Error("Провайдер должен быть iola, ollama, openai, openrouter, codex или именем AI-профиля.");
|
|
4368
4428
|
}
|
|
4369
4429
|
|
|
4370
4430
|
const defaultModel = {
|
|
4371
|
-
|
|
4431
|
+
iola: IOLA_LOCAL_MODEL,
|
|
4432
|
+
ollama: config.ai.provider === "ollama" ? config.ai.model : IOLA_LOCAL_OLLAMA_MODEL,
|
|
4372
4433
|
openai: config.ai.provider === "openai" ? config.ai.model : "gpt-4.1-mini",
|
|
4373
4434
|
openrouter: config.ai.provider === "openrouter" ? config.ai.model : "openai/gpt-4.1-mini",
|
|
4374
4435
|
codex: config.ai.provider === "codex" ? config.ai.model : "gpt-5.5",
|
|
4375
4436
|
}[provider];
|
|
4376
|
-
const profileName = provider === "ollama" ? "local" : provider;
|
|
4437
|
+
const profileName = provider === "ollama" || provider === "iola" ? "local" : provider;
|
|
4377
4438
|
const profile = buildProfileFromOptions(provider, { model: defaultModel });
|
|
4378
4439
|
|
|
4379
4440
|
await saveConfig({
|
|
@@ -4412,7 +4473,7 @@ async function slashModelMenu(args = []) {
|
|
|
4412
4473
|
function normalizeModelMenuTarget(value = "") {
|
|
4413
4474
|
const normalized = String(value || "").trim().toLocaleLowerCase("ru-RU");
|
|
4414
4475
|
if (!normalized) return "";
|
|
4415
|
-
if (["local", "локальная", "локально", "ollama"].includes(normalized)) return "local";
|
|
4476
|
+
if (["local", "локальная", "локально", "iola", "иола", "ollama"].includes(normalized)) return "local";
|
|
4416
4477
|
if (["api", "апи"].includes(normalized)) return "api";
|
|
4417
4478
|
if (normalized === "openai") return "openai";
|
|
4418
4479
|
if (normalized === "openrouter" || normalized === "router") return "openrouter";
|
|
@@ -4422,7 +4483,7 @@ function normalizeModelMenuTarget(value = "") {
|
|
|
4422
4483
|
|
|
4423
4484
|
async function chooseModelTarget() {
|
|
4424
4485
|
console.log("Выберите AI-подключение:");
|
|
4425
|
-
console.log(" 1. Локальная модель
|
|
4486
|
+
console.log(" 1. Локальная модель IOLA");
|
|
4426
4487
|
console.log(" 2. API (OpenAI/OpenRouter)");
|
|
4427
4488
|
console.log(" 3. Codex CLI");
|
|
4428
4489
|
console.log(" 0. Отмена");
|
|
@@ -4433,7 +4494,7 @@ async function chooseModelTarget() {
|
|
|
4433
4494
|
|
|
4434
4495
|
async function openModelTargetMenu(target) {
|
|
4435
4496
|
if (target === "local") {
|
|
4436
|
-
const model = await chooseAiModel("
|
|
4497
|
+
const model = await chooseAiModel("iola");
|
|
4437
4498
|
if (model) await switchModelTarget("local", model);
|
|
4438
4499
|
return;
|
|
4439
4500
|
}
|
|
@@ -4522,12 +4583,15 @@ async function chooseAiModel(provider) {
|
|
|
4522
4583
|
|
|
4523
4584
|
async function switchModelTarget(target, model) {
|
|
4524
4585
|
const config = await loadConfig();
|
|
4525
|
-
const provider = target === "local" ? "
|
|
4586
|
+
const provider = target === "local" ? "iola" : target;
|
|
4587
|
+
if (provider === "iola") {
|
|
4588
|
+
await ensureIolaModelFresh({ quiet: false });
|
|
4589
|
+
}
|
|
4526
4590
|
if (provider === "ollama") {
|
|
4527
4591
|
const ready = await ensureOllamaModelAvailable(model, config);
|
|
4528
4592
|
if (!ready) return;
|
|
4529
4593
|
}
|
|
4530
|
-
const profileName = provider === "ollama" ? "local" : provider;
|
|
4594
|
+
const profileName = provider === "ollama" || provider === "iola" ? "local" : provider;
|
|
4531
4595
|
const currentProfile = config.ai.profiles?.[profileName] || buildProfileFromOptions(provider, { model });
|
|
4532
4596
|
const profile = {
|
|
4533
4597
|
...currentProfile,
|
|
@@ -5911,20 +5975,22 @@ function assertKeyProvider(provider) {
|
|
|
5911
5975
|
|
|
5912
5976
|
async function chooseAiProvider() {
|
|
5913
5977
|
console.log("Выберите режим AI:");
|
|
5914
|
-
console.log("1. Локальная модель
|
|
5978
|
+
console.log("1. Локальная модель IOLA");
|
|
5915
5979
|
console.log("2. OpenAI API");
|
|
5916
5980
|
console.log("3. OpenRouter API");
|
|
5917
5981
|
console.log("4. Codex/MCP");
|
|
5982
|
+
console.log("5. Ollama");
|
|
5918
5983
|
|
|
5919
5984
|
const rl = readline.createInterface({ input, output });
|
|
5920
5985
|
try {
|
|
5921
5986
|
const answer = (await rl.question("Введите номер [1]: ")).trim() || "1";
|
|
5922
5987
|
return {
|
|
5923
|
-
1: "
|
|
5988
|
+
1: "iola",
|
|
5924
5989
|
2: "openai",
|
|
5925
5990
|
3: "openrouter",
|
|
5926
5991
|
4: "codex",
|
|
5927
|
-
|
|
5992
|
+
5: "ollama",
|
|
5993
|
+
}[answer] || "iola";
|
|
5928
5994
|
} finally {
|
|
5929
5995
|
rl.close();
|
|
5930
5996
|
}
|
|
@@ -5984,6 +6050,53 @@ async function setupOllama(args) {
|
|
|
5984
6050
|
console.log(`Готово. Локальный AI-профиль сохранен в ${CONFIG_FILE}`);
|
|
5985
6051
|
}
|
|
5986
6052
|
|
|
6053
|
+
async function setupIolaLocal(args) {
|
|
6054
|
+
const options = parseOptions(args);
|
|
6055
|
+
const repo = options.repo || IOLA_ROUTER_HF_REPO;
|
|
6056
|
+
const modelDir = options["model-dir"] || IOLA_MODEL_DIR;
|
|
6057
|
+
const profileName = options.name || "local";
|
|
6058
|
+
const optional = Boolean(options.optional);
|
|
6059
|
+
|
|
6060
|
+
if (optional && process.env.CI === "true") {
|
|
6061
|
+
return;
|
|
6062
|
+
}
|
|
6063
|
+
|
|
6064
|
+
try {
|
|
6065
|
+
await ensureIolaModelFresh({ repo, modelDir, force: true, quiet: Boolean(options.quiet) });
|
|
6066
|
+
} catch (error) {
|
|
6067
|
+
if (!optional) throw error;
|
|
6068
|
+
console.warn(`IOLA local model не установлена: ${error instanceof Error ? error.message : String(error)}`);
|
|
6069
|
+
}
|
|
6070
|
+
|
|
6071
|
+
const config = await loadConfig();
|
|
6072
|
+
await saveConfig({
|
|
6073
|
+
ai: {
|
|
6074
|
+
...config.ai,
|
|
6075
|
+
activeProfile: profileName,
|
|
6076
|
+
provider: "iola",
|
|
6077
|
+
model: IOLA_LOCAL_MODEL,
|
|
6078
|
+
profiles: {
|
|
6079
|
+
...(config.ai.profiles || {}),
|
|
6080
|
+
[profileName]: {
|
|
6081
|
+
provider: "iola",
|
|
6082
|
+
model: IOLA_LOCAL_MODEL,
|
|
6083
|
+
repo,
|
|
6084
|
+
modelDir,
|
|
6085
|
+
},
|
|
6086
|
+
},
|
|
6087
|
+
},
|
|
6088
|
+
});
|
|
6089
|
+
|
|
6090
|
+
if (options.quiet) return;
|
|
6091
|
+
console.log("");
|
|
6092
|
+
console.log("IOLA local mode готов:");
|
|
6093
|
+
console.log(` runtime: Python transformers/peft`);
|
|
6094
|
+
console.log(` model: ${IOLA_LOCAL_MODEL}`);
|
|
6095
|
+
console.log(` Hugging Face: ${repo}`);
|
|
6096
|
+
console.log(` cache: ${modelDir}`);
|
|
6097
|
+
console.log(" точные данные: https://apiiola.yasg.ru/api/v1/resolve-entity-field");
|
|
6098
|
+
}
|
|
6099
|
+
|
|
5987
6100
|
async function aiAsk(args, context = {}) {
|
|
5988
6101
|
const options = parseOptions(args);
|
|
5989
6102
|
const question = options._.join(" ").trim();
|
|
@@ -5995,9 +6108,9 @@ async function aiAsk(args, context = {}) {
|
|
|
5995
6108
|
const config = await loadConfig();
|
|
5996
6109
|
const providerConfig = await resolveUsableAiProfile(config, options);
|
|
5997
6110
|
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
5998
|
-
if (providerConfig.provider !== "ollama") await assertPermission("externalAi");
|
|
6111
|
+
if (providerConfig.provider !== "ollama" && providerConfig.provider !== "iola") await assertPermission("externalAi");
|
|
5999
6112
|
if (options["stream-json"]) options.events = true;
|
|
6000
|
-
if (options.tools && providerConfig.provider === "ollama") {
|
|
6113
|
+
if (providerConfig.provider === "iola" || (options.tools && providerConfig.provider === "ollama")) {
|
|
6001
6114
|
return localToolAsk(question, providerConfig, options);
|
|
6002
6115
|
}
|
|
6003
6116
|
applyRuntimeConfig(providerConfig, options.config);
|
|
@@ -6220,14 +6333,25 @@ function resolveAiProfile(config, options = {}) {
|
|
|
6220
6333
|
provider,
|
|
6221
6334
|
model: options.model || activeProfile.model || config.ai.model,
|
|
6222
6335
|
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
6336
|
+
repo: options.repo || activeProfile.repo,
|
|
6337
|
+
modelDir: options["model-dir"] || activeProfile.modelDir,
|
|
6223
6338
|
temperature: options.temperature || activeProfile.temperature,
|
|
6224
6339
|
};
|
|
6225
6340
|
}
|
|
6226
6341
|
|
|
6227
6342
|
async function localToolAsk(question, providerConfig, options) {
|
|
6228
6343
|
if (options["stream-json"]) options.events = true;
|
|
6344
|
+
const guarded = guardNonPublicQuestion(question);
|
|
6345
|
+
if (guarded) {
|
|
6346
|
+
if (!options.quiet) console.log(guarded);
|
|
6347
|
+
return guarded;
|
|
6348
|
+
}
|
|
6229
6349
|
await ensureLocalData();
|
|
6230
6350
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
6351
|
+
if (plan.directAnswer) {
|
|
6352
|
+
if (!options.quiet) console.log(plan.directAnswer);
|
|
6353
|
+
return plan.directAnswer;
|
|
6354
|
+
}
|
|
6231
6355
|
const validated = validateToolPlan(plan, options);
|
|
6232
6356
|
if (options.plan) {
|
|
6233
6357
|
printToolPlan(validated);
|
|
@@ -6267,6 +6391,14 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
6267
6391
|
return answer;
|
|
6268
6392
|
}
|
|
6269
6393
|
|
|
6394
|
+
function guardNonPublicQuestion(question) {
|
|
6395
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6396
|
+
if (/(зарплат|получа[ею]т|доход|домашн|паспорт|снилс|личн|персональн)/iu.test(normalized)) {
|
|
6397
|
+
return "Это поле не входит в открытые публичные данные.";
|
|
6398
|
+
}
|
|
6399
|
+
return "";
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6270
6402
|
function printToolPlan(plan) {
|
|
6271
6403
|
console.log("План выполнения:");
|
|
6272
6404
|
plan.steps.forEach((step, index) => {
|
|
@@ -6276,6 +6408,17 @@ function printToolPlan(plan) {
|
|
|
6276
6408
|
|
|
6277
6409
|
async function buildLocalToolPlan(question, providerConfig, options) {
|
|
6278
6410
|
const mode = options.reasoning || "verify";
|
|
6411
|
+
|
|
6412
|
+
if (providerConfig.provider === "iola") {
|
|
6413
|
+
await ensureIolaModelFresh({
|
|
6414
|
+
repo: providerConfig.repo || IOLA_ROUTER_HF_REPO,
|
|
6415
|
+
modelDir: providerConfig.modelDir || IOLA_MODEL_DIR,
|
|
6416
|
+
quiet: true,
|
|
6417
|
+
});
|
|
6418
|
+
const raw = await callIolaLocal(providerConfig, [{ role: "user", content: question }]);
|
|
6419
|
+
return normalizeIolaRouterPlan(raw, question, options);
|
|
6420
|
+
}
|
|
6421
|
+
|
|
6279
6422
|
const prompt = [
|
|
6280
6423
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
6281
6424
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
@@ -6298,6 +6441,27 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
6298
6441
|
}
|
|
6299
6442
|
}
|
|
6300
6443
|
|
|
6444
|
+
function normalizeIolaRouterPlan(raw, question, options = {}) {
|
|
6445
|
+
const payload = typeof raw === "string" ? parseJsonObject(raw) : raw;
|
|
6446
|
+
if (payload.action === "tool_call") {
|
|
6447
|
+
const tool = payload.tool === "get_entity_field" ? "resolve_entity_field" : payload.tool;
|
|
6448
|
+
return { steps: [{ tool, args: payload.args || {} }] };
|
|
6449
|
+
}
|
|
6450
|
+
if (payload.action === "direct_answer") {
|
|
6451
|
+
return { directAnswer: payload.answer || "" };
|
|
6452
|
+
}
|
|
6453
|
+
if (payload.action === "clarify") {
|
|
6454
|
+
return { directAnswer: payload.question || "Уточните запрос." };
|
|
6455
|
+
}
|
|
6456
|
+
if (payload.action === "refuse") {
|
|
6457
|
+
return { directAnswer: payload.reason === "field_not_public" ? "Это поле не входит в открытые публичные данные." : "Не могу выполнить этот запрос." };
|
|
6458
|
+
}
|
|
6459
|
+
if (options.reasoning === "vote") {
|
|
6460
|
+
return inferToolPlan(question, options);
|
|
6461
|
+
}
|
|
6462
|
+
throw new Error(`IOLA router вернул неподдерживаемое действие: ${payload.action || "unknown"}`);
|
|
6463
|
+
}
|
|
6464
|
+
|
|
6301
6465
|
function parseJsonObject(text) {
|
|
6302
6466
|
const match = String(text).match(/\{[\s\S]*\}/);
|
|
6303
6467
|
if (!match) throw new Error("JSON-план не найден.");
|
|
@@ -6347,6 +6511,50 @@ function validateToolPlan(plan, options = {}) {
|
|
|
6347
6511
|
return plan;
|
|
6348
6512
|
}
|
|
6349
6513
|
|
|
6514
|
+
async function searchPublicEntities(args = {}) {
|
|
6515
|
+
const payload = await postJson(`${await getApiBaseUrl()}/search-entities`, {
|
|
6516
|
+
layer: normalizeEntityLayer(args.layer),
|
|
6517
|
+
query: args.query || args.entity_name || args.name || "",
|
|
6518
|
+
limit: Number(args.limit || 10),
|
|
6519
|
+
filters: args.filters || undefined,
|
|
6520
|
+
});
|
|
6521
|
+
return normalizeItems(payload).map((item) => ({
|
|
6522
|
+
...(item.entity || item),
|
|
6523
|
+
score: item.score,
|
|
6524
|
+
layer: payload.layer || normalizeEntityLayer(args.layer),
|
|
6525
|
+
}));
|
|
6526
|
+
}
|
|
6527
|
+
|
|
6528
|
+
async function resolvePublicEntityField(args = {}) {
|
|
6529
|
+
const payload = await postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
|
|
6530
|
+
layer: normalizeEntityLayer(args.layer),
|
|
6531
|
+
entity_number: args.entity_number ?? args.number,
|
|
6532
|
+
entity_name: args.entity_name || args.name,
|
|
6533
|
+
inn: args.inn,
|
|
6534
|
+
field: normalizeEntityField(args.field),
|
|
6535
|
+
must_refute_user_value: args.must_refute_user_value,
|
|
6536
|
+
});
|
|
6537
|
+
return payload;
|
|
6538
|
+
}
|
|
6539
|
+
|
|
6540
|
+
function normalizeEntityLayer(layer) {
|
|
6541
|
+
const value = String(layer || "").toLocaleLowerCase("ru-RU");
|
|
6542
|
+
if (value === "school" || value === "schools" || value.includes("школ")) return "schools";
|
|
6543
|
+
if (value === "kindergarten" || value === "kindergartens" || value.includes("сад")) return "kindergartens";
|
|
6544
|
+
return value || "schools";
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6547
|
+
function normalizeEntityField(field) {
|
|
6548
|
+
const value = String(field || "").toLocaleLowerCase("ru-RU");
|
|
6549
|
+
if (value === "director" || value === "head" || value.includes("директор") || value.includes("руковод")) return "head";
|
|
6550
|
+
if (value === "site" || value === "url" || value === "website" || value.includes("сайт")) return "website";
|
|
6551
|
+
if (value === "mail" || value === "email" || value.includes("почт")) return "email";
|
|
6552
|
+
if (value === "phone" || value.includes("тел")) return "phone";
|
|
6553
|
+
if (value === "address" || value.includes("адрес")) return "address";
|
|
6554
|
+
if (value === "license") return "license_status";
|
|
6555
|
+
return value || "name";
|
|
6556
|
+
}
|
|
6557
|
+
|
|
6350
6558
|
function availableToolNames(options = {}) {
|
|
6351
6559
|
const names = new Set(LOCAL_TOOLS);
|
|
6352
6560
|
for (const tool of getLocalMcpToolNames()) names.add(tool);
|
|
@@ -6366,6 +6574,13 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6366
6574
|
if (step.tool === "search_data" || step.tool === "search_local") {
|
|
6367
6575
|
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
6368
6576
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6577
|
+
} else if (step.tool === "search_entities") {
|
|
6578
|
+
current = await searchPublicEntities(step.args || {});
|
|
6579
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
6580
|
+
} else if (step.tool === "resolve_entity_field") {
|
|
6581
|
+
const resolved = await resolvePublicEntityField(step.args || {});
|
|
6582
|
+
current = Array.isArray(resolved) ? resolved : [resolved];
|
|
6583
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
6369
6584
|
} else if (step.tool === "get_card") {
|
|
6370
6585
|
const card = findCard(step.args?.query || "");
|
|
6371
6586
|
current = card ? [card] : [];
|
|
@@ -6514,7 +6729,13 @@ function formatToolResult(result, options) {
|
|
|
6514
6729
|
const exported = result.outputs.find((item) => item.output);
|
|
6515
6730
|
if (exported) return `Готово. Файл сохранен: ${exported.output}. Записей: ${exported.rows}`;
|
|
6516
6731
|
if (!result.rows.length) return "Данных не найдено.";
|
|
6517
|
-
return result.rows.slice(0, 10).map((row) =>
|
|
6732
|
+
return result.rows.slice(0, 10).map((row) => {
|
|
6733
|
+
if (row.ok && row.entity && row.field) {
|
|
6734
|
+
const name = row.entity.name || row.entity.inn || "организация";
|
|
6735
|
+
return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
|
|
6736
|
+
}
|
|
6737
|
+
return `${row.name || row.check || row.inn || "строка"}: ${row.address || row.phone || row.email || row.website || row.count || ""}`;
|
|
6738
|
+
}).join("\n");
|
|
6518
6739
|
}
|
|
6519
6740
|
|
|
6520
6741
|
function applyRuntimeConfig(target, value) {
|
|
@@ -6906,6 +7127,10 @@ function buildSourceLines(dataContext) {
|
|
|
6906
7127
|
}
|
|
6907
7128
|
|
|
6908
7129
|
async function callAiProvider(config, messages) {
|
|
7130
|
+
if (config.provider === "iola") {
|
|
7131
|
+
return callIolaLocal(config, messages);
|
|
7132
|
+
}
|
|
7133
|
+
|
|
6909
7134
|
if (config.provider === "ollama") {
|
|
6910
7135
|
return callOllama(config, messages);
|
|
6911
7136
|
}
|
|
@@ -6925,6 +7150,155 @@ async function callAiProvider(config, messages) {
|
|
|
6925
7150
|
throw new Error(`Неизвестный AI-провайдер: ${config.provider}`);
|
|
6926
7151
|
}
|
|
6927
7152
|
|
|
7153
|
+
async function callIolaLocal(config, messages) {
|
|
7154
|
+
const runtime = await ensureIolaModelRuntime({ quiet: true });
|
|
7155
|
+
const repo = config.repo || IOLA_ROUTER_HF_REPO;
|
|
7156
|
+
const modelDir = config.modelDir || IOLA_MODEL_DIR;
|
|
7157
|
+
const payload = {
|
|
7158
|
+
repo,
|
|
7159
|
+
cache_dir: modelDir,
|
|
7160
|
+
messages,
|
|
7161
|
+
max_new_tokens: Number(config.maxNewTokens || 180),
|
|
7162
|
+
temperature: Number(config.temperature ?? 0),
|
|
7163
|
+
};
|
|
7164
|
+
const { stdout, stderr } = await runCommand(runtime.python, [IOLA_MODEL_RUNNER], {
|
|
7165
|
+
input: JSON.stringify(payload),
|
|
7166
|
+
env: {
|
|
7167
|
+
IOLA_ROUTER_HF_REPO: repo,
|
|
7168
|
+
IOLA_MODEL_DIR: modelDir,
|
|
7169
|
+
},
|
|
7170
|
+
});
|
|
7171
|
+
const text = stdout.trim();
|
|
7172
|
+
if (!text) {
|
|
7173
|
+
throw new Error(`IOLA local model вернула пустой ответ.${stderr ? `\n${stderr}` : ""}`);
|
|
7174
|
+
}
|
|
7175
|
+
return text;
|
|
7176
|
+
}
|
|
7177
|
+
|
|
7178
|
+
async function hasUsableIolaModel() {
|
|
7179
|
+
const state = readConfigLayerSync(getIolaModelStateFile(IOLA_MODEL_DIR));
|
|
7180
|
+
return Boolean(state?.repo && state?.revision && existsSync(IOLA_MODEL_DIR));
|
|
7181
|
+
}
|
|
7182
|
+
|
|
7183
|
+
async function ensureIolaModelFresh(options = {}) {
|
|
7184
|
+
const repo = options.repo || IOLA_ROUTER_HF_REPO;
|
|
7185
|
+
const modelDir = options.modelDir || IOLA_MODEL_DIR;
|
|
7186
|
+
await mkdir(modelDir, { recursive: true });
|
|
7187
|
+
const stateFile = getIolaModelStateFile(modelDir);
|
|
7188
|
+
const state = readConfigLayerSync(stateFile) || {};
|
|
7189
|
+
const remote = await getRemoteIolaModelRevision(repo).catch(() => null);
|
|
7190
|
+
const stale = options.force || state.repo !== repo || !state.revision || (remote?.sha && remote.sha !== state.revision);
|
|
7191
|
+
if (!stale) return state;
|
|
7192
|
+
|
|
7193
|
+
if (!options.quiet) {
|
|
7194
|
+
const reason = state.revision ? "обновляю" : "устанавливаю";
|
|
7195
|
+
console.log(`IOLA local model: ${reason} ${repo}`);
|
|
7196
|
+
console.log("Загрузка первой установки может занять несколько минут.");
|
|
7197
|
+
}
|
|
7198
|
+
|
|
7199
|
+
const runtime = await ensureIolaModelRuntime({ quiet: options.quiet });
|
|
7200
|
+
const { stdout } = await runCommand(runtime.python, [IOLA_MODEL_RUNNER, "--ensure"], {
|
|
7201
|
+
input: JSON.stringify({ repo, cache_dir: modelDir }),
|
|
7202
|
+
env: {
|
|
7203
|
+
IOLA_ROUTER_HF_REPO: repo,
|
|
7204
|
+
IOLA_MODEL_DIR: modelDir,
|
|
7205
|
+
},
|
|
7206
|
+
});
|
|
7207
|
+
const installed = parseJsonObject(stdout || "{}");
|
|
7208
|
+
const nextState = {
|
|
7209
|
+
repo,
|
|
7210
|
+
revision: installed.revision || remote?.sha || state.revision || `local-${Date.now()}`,
|
|
7211
|
+
installedAt: new Date().toISOString(),
|
|
7212
|
+
runtime: "transformers",
|
|
7213
|
+
};
|
|
7214
|
+
await mkdir(modelDir, { recursive: true });
|
|
7215
|
+
await writeFile(stateFile, `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
|
|
7216
|
+
return nextState;
|
|
7217
|
+
}
|
|
7218
|
+
|
|
7219
|
+
function getIolaModelStateFile(modelDir = IOLA_MODEL_DIR) {
|
|
7220
|
+
return path.join(modelDir, "manifest.json");
|
|
7221
|
+
}
|
|
7222
|
+
|
|
7223
|
+
async function ensureIolaModelRuntime(options = {}) {
|
|
7224
|
+
const python = await getIolaRuntimePython();
|
|
7225
|
+
if (python && await checkIolaPythonDeps(python)) return { python };
|
|
7226
|
+
|
|
7227
|
+
const basePython = await findPythonCommand();
|
|
7228
|
+
if (!basePython) {
|
|
7229
|
+
throw new Error("Python не найден. Установите Python 3.11+ и повторите: iola ai setup iola --yes");
|
|
7230
|
+
}
|
|
7231
|
+
|
|
7232
|
+
await mkdir(IOLA_MODEL_RUNTIME_DIR, { recursive: true });
|
|
7233
|
+
if (!python) {
|
|
7234
|
+
if (!options.quiet) console.log(`Создаю Python runtime: ${IOLA_MODEL_RUNTIME_DIR}`);
|
|
7235
|
+
await runCommand(basePython.command, [...basePython.args, "-m", "venv", IOLA_MODEL_RUNTIME_DIR], { inherit: !options.quiet });
|
|
7236
|
+
}
|
|
7237
|
+
|
|
7238
|
+
const runtimePython = await getIolaRuntimePython();
|
|
7239
|
+
if (!runtimePython) {
|
|
7240
|
+
throw new Error("Не удалось создать Python runtime для локальной модели.");
|
|
7241
|
+
}
|
|
7242
|
+
|
|
7243
|
+
if (!options.quiet) console.log("Устанавливаю зависимости локальной модели: torch, transformers, peft.");
|
|
7244
|
+
await runCommand(runtimePython, ["-m", "pip", "install", "--upgrade", "pip"], { inherit: !options.quiet });
|
|
7245
|
+
await runCommand(runtimePython, ["-m", "pip", "install", "torch>=2.6.0", "transformers>=4.57.0,<5.0", "peft>=0.15.0", "accelerate>=1.8.0", "huggingface_hub>=0.34.0,<1.0", "hf_xet>=1.1.0", "safetensors>=0.4.0"], { inherit: !options.quiet });
|
|
7246
|
+
|
|
7247
|
+
if (!await checkIolaPythonDeps(runtimePython)) {
|
|
7248
|
+
throw new Error("Python-зависимости локальной модели не установились.");
|
|
7249
|
+
}
|
|
7250
|
+
return { python: runtimePython };
|
|
7251
|
+
}
|
|
7252
|
+
|
|
7253
|
+
async function getIolaRuntimePython() {
|
|
7254
|
+
const candidate = process.platform === "win32"
|
|
7255
|
+
? path.join(IOLA_MODEL_RUNTIME_DIR, "Scripts", "python.exe")
|
|
7256
|
+
: path.join(IOLA_MODEL_RUNTIME_DIR, "bin", "python");
|
|
7257
|
+
return existsSync(candidate) ? candidate : null;
|
|
7258
|
+
}
|
|
7259
|
+
|
|
7260
|
+
async function checkIolaPythonDeps(python) {
|
|
7261
|
+
try {
|
|
7262
|
+
await runCommand(python, [IOLA_MODEL_RUNNER, "--check-deps"]);
|
|
7263
|
+
return true;
|
|
7264
|
+
} catch {
|
|
7265
|
+
return false;
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7269
|
+
async function findPythonCommand() {
|
|
7270
|
+
const candidates = [
|
|
7271
|
+
{ command: process.env.IOLA_PYTHON, args: [] },
|
|
7272
|
+
{ command: "python", args: [] },
|
|
7273
|
+
{ command: "python3", args: [] },
|
|
7274
|
+
{ command: "py", args: ["-3"] },
|
|
7275
|
+
].filter((item) => item.command);
|
|
7276
|
+
|
|
7277
|
+
for (const candidate of candidates) {
|
|
7278
|
+
try {
|
|
7279
|
+
await runCommand(candidate.command, [...candidate.args, "--version"]);
|
|
7280
|
+
return candidate;
|
|
7281
|
+
} catch {
|
|
7282
|
+
// Try next candidate.
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
return null;
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
async function getRemoteIolaModelRevision(repo = IOLA_ROUTER_HF_REPO) {
|
|
7289
|
+
const repoPath = String(repo).split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
7290
|
+
const response = await fetch(`https://huggingface.co/api/models/${repoPath}`, {
|
|
7291
|
+
headers: {
|
|
7292
|
+
accept: "application/json",
|
|
7293
|
+
"user-agent": "@iola_adm/iola-cli",
|
|
7294
|
+
},
|
|
7295
|
+
signal: AbortSignal.timeout(5000),
|
|
7296
|
+
});
|
|
7297
|
+
if (!response.ok) throw new Error(`Hugging Face model metadata failed: ${response.status} ${response.statusText}`);
|
|
7298
|
+
const payload = await response.json();
|
|
7299
|
+
return { sha: payload.sha || payload.lastModified || "", lastModified: payload.lastModified || "" };
|
|
7300
|
+
}
|
|
7301
|
+
|
|
6928
7302
|
async function callCodex(config, messages) {
|
|
6929
7303
|
const prompt = messages.map((message) => `${message.role.toUpperCase()}:\n${message.content}`).join("\n\n");
|
|
6930
7304
|
const outputFile = path.join(os.tmpdir(), `iola-codex-${process.pid}-${Date.now()}.txt`);
|
|
@@ -7262,6 +7636,9 @@ async function onboard(args = []) {
|
|
|
7262
7636
|
if (components.includes("workspace")) await handleWorkspace(["init"]);
|
|
7263
7637
|
if (components.includes("policy")) await handlePolicy(["use", "analyst"]);
|
|
7264
7638
|
if (components.includes("archive")) await ensureArchiveTool({ install: true });
|
|
7639
|
+
if (components.includes("iola")) {
|
|
7640
|
+
await setupIolaLocal(["--yes"]);
|
|
7641
|
+
}
|
|
7265
7642
|
if (components.includes("ollama")) {
|
|
7266
7643
|
await installOllamaIfMissing();
|
|
7267
7644
|
await setupOllama(["--yes"]);
|
|
@@ -7309,7 +7686,7 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7309
7686
|
const map = {
|
|
7310
7687
|
1: "workspace",
|
|
7311
7688
|
2: "policy",
|
|
7312
|
-
3: "
|
|
7689
|
+
3: "iola",
|
|
7313
7690
|
4: "openai",
|
|
7314
7691
|
5: "openrouter",
|
|
7315
7692
|
6: "codex",
|
|
@@ -7317,6 +7694,7 @@ async function chooseOnboardComponents(status = null) {
|
|
|
7317
7694
|
8: "archive",
|
|
7318
7695
|
9: "index",
|
|
7319
7696
|
10: "browser",
|
|
7697
|
+
11: "ollama",
|
|
7320
7698
|
};
|
|
7321
7699
|
return [...selected].map((item) => map[item] || item).filter(Boolean);
|
|
7322
7700
|
} finally {
|
|
@@ -7338,6 +7716,7 @@ async function getOnboardComponentStatus() {
|
|
|
7338
7716
|
return {
|
|
7339
7717
|
workspace: workspaceReady,
|
|
7340
7718
|
policy: policyReady,
|
|
7719
|
+
iola: Boolean(readiness.iola),
|
|
7341
7720
|
ollama: Boolean(ollamaVersion && readiness.ollama),
|
|
7342
7721
|
openai: Boolean(readiness.openai),
|
|
7343
7722
|
openrouter: Boolean(readiness.openrouter),
|
|
@@ -7353,7 +7732,7 @@ function onboardComponentRows(status) {
|
|
|
7353
7732
|
const rows = [
|
|
7354
7733
|
["1", "workspace", "workspace и контекст", "рабочая папка, IOLA.md и .iola/context.md"],
|
|
7355
7734
|
["2", "policy", "policy analyst", "разрешения и профиль аналитика"],
|
|
7356
|
-
["3", "
|
|
7735
|
+
["3", "iola", "IOLA локальная модель", "локальная модель найдена"],
|
|
7357
7736
|
["4", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
|
|
7358
7737
|
["5", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
|
|
7359
7738
|
["6", "codex", "Codex CLI", "CLI установлен и авторизация найдена"],
|
|
@@ -7361,6 +7740,7 @@ function onboardComponentRows(status) {
|
|
|
7361
7740
|
["8", "archive", "7-Zip / архивы", "архиватор найден"],
|
|
7362
7741
|
["9", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
|
|
7363
7742
|
["10", "browser", "Browser runtime", "Playwright/Chromium установлен"],
|
|
7743
|
+
["11", "ollama", "Ollama", "опциональный локальный runtime"],
|
|
7364
7744
|
];
|
|
7365
7745
|
return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
|
|
7366
7746
|
}
|
|
@@ -7369,12 +7749,13 @@ function defaultOnboardSelection(status) {
|
|
|
7369
7749
|
const defaults = [];
|
|
7370
7750
|
if (!status.workspace) defaults.push("1");
|
|
7371
7751
|
if (!status.policy) defaults.push("2");
|
|
7752
|
+
if (!status.iola) defaults.push("3");
|
|
7372
7753
|
if (!status.archive) defaults.push("8");
|
|
7373
7754
|
return defaults.length ? defaults : ["1", "2"];
|
|
7374
7755
|
}
|
|
7375
7756
|
|
|
7376
7757
|
function defaultOnboardComponents(status) {
|
|
7377
|
-
const map = { 1: "workspace", 2: "policy", 3: "
|
|
7758
|
+
const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "openai", 5: "openrouter", 6: "codex", 7: "codex-mcp", 8: "archive", 9: "index", 10: "browser", 11: "ollama" };
|
|
7378
7759
|
return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
|
|
7379
7760
|
}
|
|
7380
7761
|
|
|
@@ -7383,12 +7764,12 @@ function parseOptions(args) {
|
|
|
7383
7764
|
|
|
7384
7765
|
for (let index = 0; index < args.length; index += 1) {
|
|
7385
7766
|
const arg = args[index];
|
|
7386
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
|
|
7767
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
|
|
7387
7768
|
result[arg.slice(2)] = true;
|
|
7388
7769
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
7389
7770
|
result.check = true;
|
|
7390
7771
|
result[arg.slice(2)] = true;
|
|
7391
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || 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" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file") {
|
|
7772
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || 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" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file") {
|
|
7392
7773
|
result[arg.slice(2)] = args[index + 1];
|
|
7393
7774
|
index += 1;
|
|
7394
7775
|
} else {
|
|
@@ -8663,7 +9044,7 @@ function recordUsage({ providerConfig, question, answer, sessionId, profile }) {
|
|
|
8663
9044
|
}
|
|
8664
9045
|
|
|
8665
9046
|
function estimateCost(providerConfig, tokens) {
|
|
8666
|
-
if (!providerConfig || providerConfig.provider === "ollama" || providerConfig.provider === "codex") return 0;
|
|
9047
|
+
if (!providerConfig || providerConfig.provider === "iola" || providerConfig.provider === "ollama" || providerConfig.provider === "codex") return 0;
|
|
8667
9048
|
const perMillion = providerConfig.provider === "openrouter" ? 0.25 : 0.4;
|
|
8668
9049
|
return Math.round((tokens / 1_000_000) * perMillion * 1_000_000) / 1_000_000;
|
|
8669
9050
|
}
|
|
@@ -9340,8 +9721,8 @@ function validateConfig(config) {
|
|
|
9340
9721
|
if (!config.ai?.profiles || typeof config.ai.profiles !== "object") errors.push("ai.profiles обязателен");
|
|
9341
9722
|
if (config.ai?.activeProfile && !config.ai.profiles?.[config.ai.activeProfile]) errors.push(`ai.activeProfile не найден в profiles: ${config.ai.activeProfile}`);
|
|
9342
9723
|
for (const [name, profile] of Object.entries(config.ai?.profiles || {})) {
|
|
9343
|
-
if (!["ollama", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
|
|
9344
|
-
if (profile.provider !== "codex" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
9724
|
+
if (!["iola", "ollama", "openai", "openrouter", "codex"].includes(profile.provider)) errors.push(`ai.profiles.${name}.provider неизвестен`);
|
|
9725
|
+
if (profile.provider !== "codex" && profile.provider !== "iola" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
9345
9726
|
}
|
|
9346
9727
|
for (const tool of Object.keys(config.permissions?.localTools || {})) {
|
|
9347
9728
|
if (!ALL_TOOL_ALIASES.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
|
|
@@ -9358,7 +9739,7 @@ function configSchema() {
|
|
|
9358
9739
|
required: ["api", "ai"],
|
|
9359
9740
|
properties: {
|
|
9360
9741
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
9361
|
-
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
9742
|
+
ai: { required: ["activeProfile", "profiles"], providers: ["iola", "ollama", "openai", "openrouter", "codex"] },
|
|
9362
9743
|
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
9363
9744
|
toolsets: { available: Object.keys(TOOLSETS) },
|
|
9364
9745
|
files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
|
|
@@ -9373,7 +9754,7 @@ function getActiveProfileName(config) {
|
|
|
9373
9754
|
return config.ai.activeProfile;
|
|
9374
9755
|
}
|
|
9375
9756
|
|
|
9376
|
-
const provider = config.ai.provider === "ollama" ? "local" : config.ai.provider;
|
|
9757
|
+
const provider = config.ai.provider === "ollama" || config.ai.provider === "iola" ? "local" : config.ai.provider;
|
|
9377
9758
|
if (provider && config.ai.profiles?.[provider]) {
|
|
9378
9759
|
return provider;
|
|
9379
9760
|
}
|
|
@@ -9560,6 +9941,25 @@ async function fetchJson(url) {
|
|
|
9560
9941
|
return response.json();
|
|
9561
9942
|
}
|
|
9562
9943
|
|
|
9944
|
+
async function postJson(url, payload) {
|
|
9945
|
+
const response = await fetch(url, {
|
|
9946
|
+
method: "POST",
|
|
9947
|
+
headers: {
|
|
9948
|
+
accept: "application/json",
|
|
9949
|
+
"content-type": "application/json",
|
|
9950
|
+
"user-agent": "@iola_adm/iola-cli",
|
|
9951
|
+
},
|
|
9952
|
+
body: JSON.stringify(payload),
|
|
9953
|
+
});
|
|
9954
|
+
|
|
9955
|
+
if (!response.ok) {
|
|
9956
|
+
const text = await response.text().catch(() => "");
|
|
9957
|
+
throw new Error(`Request failed: ${response.status} ${response.statusText} (${url})${text ? `\n${text}` : ""}`);
|
|
9958
|
+
}
|
|
9959
|
+
|
|
9960
|
+
return response.json();
|
|
9961
|
+
}
|
|
9962
|
+
|
|
9563
9963
|
function parseJsonOrSse(text) {
|
|
9564
9964
|
const trimmed = String(text || "").trim();
|
|
9565
9965
|
if (!trimmed) return null;
|