@iola_adm/iola-cli 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import readline from "node:readline/promises";
|
|
|
10
10
|
import { Readable } from "node:stream";
|
|
11
11
|
import { stdin as input, stdout as output } from "node:process";
|
|
12
12
|
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
import tls from "node:tls";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { inflateRawSync, inflateSync } from "node:zlib";
|
|
15
16
|
|
|
@@ -6695,7 +6696,7 @@ async function aiAsk(args, context = {}) {
|
|
|
6695
6696
|
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
6696
6697
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
6697
6698
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
6698
|
-
const directAnswer = buildDirectDataAnswer(question, dataContext);
|
|
6699
|
+
const directAnswer = await buildDirectDataAnswer(question, dataContext);
|
|
6699
6700
|
if (directAnswer) {
|
|
6700
6701
|
if (historyEnabled) {
|
|
6701
6702
|
recordAskHistory({ question, answer: directAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
@@ -6758,10 +6759,12 @@ async function aiAsk(args, context = {}) {
|
|
|
6758
6759
|
return answer;
|
|
6759
6760
|
}
|
|
6760
6761
|
|
|
6761
|
-
function buildDirectDataAnswer(question, dataContext) {
|
|
6762
|
+
async function buildDirectDataAnswer(question, dataContext) {
|
|
6762
6763
|
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
6763
6764
|
const requestedFields = detectDirectDataFields(normalized);
|
|
6764
6765
|
if (requestedFields.length === 0) return "";
|
|
6766
|
+
const educationAnswer = await buildDeterministicEducationAnswer(question, requestedFields);
|
|
6767
|
+
if (educationAnswer) return educationAnswer;
|
|
6765
6768
|
const rows = [
|
|
6766
6769
|
...dataContext.schools.map((item) => ({ layer: "schools", layerName: "школы", ...item })),
|
|
6767
6770
|
...dataContext.kindergartens.map((item) => ({ layer: "kindergartens", layerName: "детские сады", ...item })),
|
|
@@ -6791,6 +6794,140 @@ function detectDirectDataFields(normalizedQuestion) {
|
|
|
6791
6794
|
return [...new Set(fields)];
|
|
6792
6795
|
}
|
|
6793
6796
|
|
|
6797
|
+
async function buildDeterministicEducationAnswer(question, requestedFields) {
|
|
6798
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6799
|
+
const layer = /(сад|детсад|детск\w*\s+сад|садик)/iu.test(normalized)
|
|
6800
|
+
? "kindergartens"
|
|
6801
|
+
: /(школ|сош|лице|гимнази)/iu.test(normalized)
|
|
6802
|
+
? "schools"
|
|
6803
|
+
: "";
|
|
6804
|
+
if (!layer) return "";
|
|
6805
|
+
|
|
6806
|
+
const requests = extractEducationFactRequests(question, requestedFields, layer);
|
|
6807
|
+
if (requests.length === 0) return "";
|
|
6808
|
+
|
|
6809
|
+
const items = normalizeItems(await fetchAllApiItems(`${await getApiBaseUrl()}/${DATASETS[layer].endpoint}`))
|
|
6810
|
+
.map((item) => ({ layer, layerName: layer === "schools" ? "школы" : "детские сады", ...selectPublicSummary(item) }));
|
|
6811
|
+
const answers = [];
|
|
6812
|
+
|
|
6813
|
+
for (const request of requests) {
|
|
6814
|
+
const answer = resolveEducationFactRequest(request, items, layer);
|
|
6815
|
+
if (answer) answers.push(answer);
|
|
6816
|
+
}
|
|
6817
|
+
|
|
6818
|
+
return answers.filter(Boolean).join("\n");
|
|
6819
|
+
}
|
|
6820
|
+
|
|
6821
|
+
function extractEducationFactRequests(question, requestedFields, layer) {
|
|
6822
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6823
|
+
const segments = splitEducationQuestionSegments(question);
|
|
6824
|
+
const requests = [];
|
|
6825
|
+
|
|
6826
|
+
for (const segment of segments) {
|
|
6827
|
+
const segmentFields = detectDirectDataFields(segment.toLocaleLowerCase("ru-RU"));
|
|
6828
|
+
const fields = segmentFields.length > 0 ? segmentFields : (segments.length === 1 ? requestedFields : []);
|
|
6829
|
+
if (fields.length === 0) continue;
|
|
6830
|
+
const place = detectEducationPlace(segment) || detectEducationPlace(question);
|
|
6831
|
+
const number = extractEntityNumberFromQuestion(segment, layer)
|
|
6832
|
+
|| (segments.length === 1 ? extractEntityNumberFromQuestion(question, layer) : "");
|
|
6833
|
+
const hasEntitySignal = Boolean(number || place || /(школ|сош|лице|гимнази|сад|детсад|детск\w*\s+сад|садик)/iu.test(segment));
|
|
6834
|
+
if (!hasEntitySignal) continue;
|
|
6835
|
+
requests.push({ segment, fields, number, place });
|
|
6836
|
+
}
|
|
6837
|
+
|
|
6838
|
+
if (requests.length === 0 && requestedFields.length > 0 && /(школ|сош|лице|гимнази|сад|детсад|детск\w*\s+сад|садик)/iu.test(normalized)) {
|
|
6839
|
+
requests.push({
|
|
6840
|
+
segment: question,
|
|
6841
|
+
fields: requestedFields,
|
|
6842
|
+
number: extractEntityNumberFromQuestion(question, layer),
|
|
6843
|
+
place: detectEducationPlace(question),
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6846
|
+
|
|
6847
|
+
return requests;
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6850
|
+
function splitEducationQuestionSegments(question) {
|
|
6851
|
+
const text = String(question || "").trim();
|
|
6852
|
+
if (!text) return [];
|
|
6853
|
+
return text
|
|
6854
|
+
.split(/\s+(?:и|а также|,|;)\s+(?=(?:кто|какой|какая|адрес|телефон|инн|сайт|почт|email|директ|руковод|завед|где))/iu)
|
|
6855
|
+
.map((part) => part.trim())
|
|
6856
|
+
.filter(Boolean);
|
|
6857
|
+
}
|
|
6858
|
+
|
|
6859
|
+
function detectEducationPlace(text) {
|
|
6860
|
+
const normalized = normalizeEntityText(text || "");
|
|
6861
|
+
if (/(козьмодемьянск|козьмодемьянск[аеуом]?|козьмодемьянск\w*)/iu.test(normalized)) {
|
|
6862
|
+
return { id: "kozmodemyansk", label: "Козьмодемьянск", locative: "Козьмодемьянске", supported: false, aliases: ["козьмодемьянск", "козмодемьянск"] };
|
|
6863
|
+
}
|
|
6864
|
+
if (/(семеновк|семёновк)/iu.test(normalized)) {
|
|
6865
|
+
return { id: "semenovka", label: "Семёновка", locative: "Семёновке", supported: true, aliases: ["семеновк", "семёновк"] };
|
|
6866
|
+
}
|
|
6867
|
+
if (/(йошкар|йошкар-ола|йошкар ола)/iu.test(normalized)) {
|
|
6868
|
+
return { id: "yoshkar_ola", label: "Йошкар-Ола", locative: "Йошкар-Оле", supported: true, aliases: ["йошкар", "йошкар-ола", "йошкар ола"] };
|
|
6869
|
+
}
|
|
6870
|
+
return null;
|
|
6871
|
+
}
|
|
6872
|
+
|
|
6873
|
+
function resolveEducationFactRequest(request, items, layer) {
|
|
6874
|
+
const entityLabel = layer === "schools" ? "школу" : "детский сад";
|
|
6875
|
+
if (request.place && !request.place.supported) {
|
|
6876
|
+
return `В текущих открытых данных iola-cli есть данные городского округа Йошкар-Ола. Данных по ${request.place.locative || request.place.label} в этом слое нет, поэтому ответить по этому объекту не могу.`;
|
|
6877
|
+
}
|
|
6878
|
+
|
|
6879
|
+
let candidates = items;
|
|
6880
|
+
if (request.place) {
|
|
6881
|
+
candidates = candidates.filter((item) => itemMatchesPlace(item, request.place));
|
|
6882
|
+
}
|
|
6883
|
+
|
|
6884
|
+
if (request.number) {
|
|
6885
|
+
const exactByNumber = candidates.filter((item) => itemNameHasNumber(item, request.number));
|
|
6886
|
+
if (exactByNumber.length === 0) {
|
|
6887
|
+
if (request.place && candidates.length > 0) {
|
|
6888
|
+
return [
|
|
6889
|
+
`Точную ${entityLabel} № ${request.number} в ${request.place.locative || request.place.label} в открытом слое не нашел.`,
|
|
6890
|
+
`В ${request.place.locative || request.place.label} есть:`,
|
|
6891
|
+
...candidates.slice(0, 5).map((item) => `- ${getDirectDataItemName(item)}${item.address ? `; адрес: ${item.address}` : ""}${item.inn ? `; ИНН ${item.inn}` : ""}`),
|
|
6892
|
+
].join("\n");
|
|
6893
|
+
}
|
|
6894
|
+
return `В открытом слое не нашел ${entityLabel} № ${request.number}.`;
|
|
6895
|
+
}
|
|
6896
|
+
candidates = exactByNumber;
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6899
|
+
if (candidates.length === 0) {
|
|
6900
|
+
const placeText = request.place ? ` в ${request.place.locative || request.place.label}` : "";
|
|
6901
|
+
return `В открытом слое не нашел ${entityLabel}${placeText}.`;
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6904
|
+
if (candidates.length > 1 && !request.number) {
|
|
6905
|
+
return [
|
|
6906
|
+
`Нашел несколько подходящих записей${request.place ? ` для ${request.place.locative || request.place.label}` : ""}:`,
|
|
6907
|
+
...candidates.slice(0, 5).flatMap((item) => [
|
|
6908
|
+
`- ${getDirectDataItemName(item)}`,
|
|
6909
|
+
...request.fields.map((field) => ` ${formatDirectDataField(field, item)}`).filter(Boolean),
|
|
6910
|
+
` Источник: слой ${item.layer}, ИНН ${item.inn || "-"}.`,
|
|
6911
|
+
]),
|
|
6912
|
+
].join("\n");
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6915
|
+
const item = candidates[0];
|
|
6916
|
+
const lines = request.fields.map((field) => formatDirectDataField(field, item)).filter(Boolean);
|
|
6917
|
+
if (lines.length === 0) return "";
|
|
6918
|
+
return [
|
|
6919
|
+
...lines,
|
|
6920
|
+
`Источник: слой ${item.layer}, ${getDirectDataItemName(item)}, ИНН ${item.inn || "-"}.`,
|
|
6921
|
+
].join("\n");
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
function itemMatchesPlace(item, place) {
|
|
6925
|
+
if (!place) return true;
|
|
6926
|
+
const text = normalizeEntityText(`${item.name || ""} ${item.address || ""} ${item.legal_address || ""} ${item.fns_full_name || ""} ${item.fns_short_name || ""}`);
|
|
6927
|
+
if (place.id === "yoshkar_ola") return /йошкар|йошкар-ола|йошкар ола/u.test(text) && !/семеновк/u.test(text);
|
|
6928
|
+
return place.aliases.some((alias) => text.includes(normalizeEntityText(alias)));
|
|
6929
|
+
}
|
|
6930
|
+
|
|
6794
6931
|
function pickDirectDataItem(question, dataContext, rows) {
|
|
6795
6932
|
const patterns = dataContext.query?.patterns || extractStructuredPatterns(question);
|
|
6796
6933
|
const targetLayers = patterns.targetLayers || [];
|
|
@@ -8667,18 +8804,23 @@ async function callGigaChat(config, messages) {
|
|
|
8667
8804
|
}
|
|
8668
8805
|
|
|
8669
8806
|
const token = await getGigaChatAccessToken(config, authKey);
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8807
|
+
let response;
|
|
8808
|
+
try {
|
|
8809
|
+
response = await fetch(`${String(config.baseUrl || "https://gigachat.devices.sberbank.ru/api/v1").replace(/\/+$/, "")}/chat/completions`, {
|
|
8810
|
+
method: "POST",
|
|
8811
|
+
headers: {
|
|
8812
|
+
authorization: `Bearer ${token}`,
|
|
8813
|
+
"content-type": "application/json",
|
|
8814
|
+
},
|
|
8815
|
+
body: JSON.stringify({
|
|
8816
|
+
model: config.model || "GigaChat-2",
|
|
8817
|
+
messages,
|
|
8818
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
8819
|
+
}),
|
|
8820
|
+
});
|
|
8821
|
+
} catch (error) {
|
|
8822
|
+
throw new Error(formatProviderFetchError("GigaChat", error));
|
|
8823
|
+
}
|
|
8682
8824
|
|
|
8683
8825
|
if (!response.ok) {
|
|
8684
8826
|
const text = await response.text();
|
|
@@ -8690,17 +8832,23 @@ async function callGigaChat(config, messages) {
|
|
|
8690
8832
|
}
|
|
8691
8833
|
|
|
8692
8834
|
async function getGigaChatAccessToken(config, authKey) {
|
|
8835
|
+
enableSystemCaForGigaChat();
|
|
8693
8836
|
const secrets = await loadSecrets();
|
|
8694
8837
|
const scope = process.env.GIGACHAT_SCOPE || secrets.gigachat?.scope || config.scope || "GIGACHAT_API_PERS";
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8838
|
+
let response;
|
|
8839
|
+
try {
|
|
8840
|
+
response = await fetch(config.authUrl || "https://ngw.devices.sberbank.ru:9443/api/v2/oauth", {
|
|
8841
|
+
method: "POST",
|
|
8842
|
+
headers: {
|
|
8843
|
+
authorization: `Basic ${authKey}`,
|
|
8844
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
8845
|
+
RqUID: randomUUID(),
|
|
8846
|
+
},
|
|
8847
|
+
body: new URLSearchParams({ scope }).toString(),
|
|
8848
|
+
});
|
|
8849
|
+
} catch (error) {
|
|
8850
|
+
throw new Error(formatProviderFetchError("GigaChat OAuth", error));
|
|
8851
|
+
}
|
|
8704
8852
|
|
|
8705
8853
|
if (!response.ok) {
|
|
8706
8854
|
const text = await response.text();
|
|
@@ -8712,6 +8860,37 @@ async function getGigaChatAccessToken(config, authKey) {
|
|
|
8712
8860
|
return payload.access_token;
|
|
8713
8861
|
}
|
|
8714
8862
|
|
|
8863
|
+
let systemCaForGigaChatEnabled = false;
|
|
8864
|
+
|
|
8865
|
+
function enableSystemCaForGigaChat() {
|
|
8866
|
+
if (systemCaForGigaChatEnabled || process.env.GIGACHAT_DISABLE_SYSTEM_CA === "1") return;
|
|
8867
|
+
systemCaForGigaChatEnabled = true;
|
|
8868
|
+
|
|
8869
|
+
try {
|
|
8870
|
+
if (typeof tls.getCACertificates !== "function" || typeof tls.setDefaultCACertificates !== "function") return;
|
|
8871
|
+
const certificates = [
|
|
8872
|
+
...tls.getCACertificates("system"),
|
|
8873
|
+
...tls.getCACertificates("bundled"),
|
|
8874
|
+
...tls.getCACertificates("extra"),
|
|
8875
|
+
];
|
|
8876
|
+
if (certificates.length > 0) tls.setDefaultCACertificates([...new Set(certificates)]);
|
|
8877
|
+
} catch {
|
|
8878
|
+
// Older Node builds may not expose system CA management. The fetch error below
|
|
8879
|
+
// will include the concrete TLS/network cause and the manual workaround.
|
|
8880
|
+
}
|
|
8881
|
+
}
|
|
8882
|
+
|
|
8883
|
+
function formatProviderFetchError(provider, error) {
|
|
8884
|
+
const cause = error?.cause;
|
|
8885
|
+
const causeCode = cause?.code ? `${cause.code}: ` : "";
|
|
8886
|
+
const causeMessage = cause?.message || "";
|
|
8887
|
+
const details = `${error?.message || "fetch failed"}${causeMessage ? ` (${causeCode}${causeMessage})` : ""}`;
|
|
8888
|
+
if (/SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_GET_ISSUER_CERT|CERT_/i.test(`${cause?.code || ""} ${causeMessage}`)) {
|
|
8889
|
+
return `${provider} network error: ${details}\nNode не доверяет цепочке сертификатов провайдера. CLI пробует использовать системные сертификаты ОС автоматически; если ошибка повторяется, обновите Node.js или запустите CLI с NODE_OPTIONS=--use-system-ca.`;
|
|
8890
|
+
}
|
|
8891
|
+
return `${provider} network error: ${details}`;
|
|
8892
|
+
}
|
|
8893
|
+
|
|
8715
8894
|
function getAiNetworkMode(config = {}) {
|
|
8716
8895
|
return validateAiNetworkMode(AI_NETWORK_MODE || config.networkMode || "gateway");
|
|
8717
8896
|
}
|
package/test/smoke-test.js
CHANGED
|
@@ -84,4 +84,18 @@ if (deletePlan.willKeep.includes("npm package files")) {
|
|
|
84
84
|
throw new Error("delete dry-run should not keep npm package files");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const multiSchoolAnswer = await runCli(["ask", "Кто директор школы № 2 и адрес школы № 7?", "--profile", "yandexgpt", "--no-history"]);
|
|
88
|
+
assertIncludes(multiSchoolAnswer, "Адамова Наталья Васильевна", "multi school answer");
|
|
89
|
+
assertIncludes(multiSchoolAnswer, "улица Первомайская, дом 89", "multi school answer");
|
|
90
|
+
assertNotIncludes(multiSchoolAnswer, "улица Осипенко, дом 46\nИсточник: слой schools, МБОУ \"Средняя общеобразовательная школа № 7", "multi school answer");
|
|
91
|
+
|
|
92
|
+
const externalTownAnswer = await runCli(["ask", "Адрес школы № 1 Козьмодемьянска", "--profile", "yandexgpt", "--no-history"]);
|
|
93
|
+
assertIncludes(externalTownAnswer, "Данных по Козьмодемьянске", "external town answer");
|
|
94
|
+
assertNotIncludes(externalTownAnswer, "улица Петрова, дом 15", "external town answer");
|
|
95
|
+
|
|
96
|
+
const semenovkaAnswer = await runCli(["ask", "Адрес школы № 1 Семеновки", "--profile", "yandexgpt", "--no-history"]);
|
|
97
|
+
assertIncludes(semenovkaAnswer, "Точную школу № 1 в Семёновке", "semenovka answer");
|
|
98
|
+
assertIncludes(semenovkaAnswer, "село Семёновка", "semenovka answer");
|
|
99
|
+
assertNotIncludes(semenovkaAnswer, "улица Петрова, дом 15", "semenovka answer");
|
|
100
|
+
|
|
87
101
|
console.log("smoke tests passed");
|
|
@@ -29,40 +29,139 @@ iola ask "найди школы на Петрова"
|
|
|
29
29
|
|
|
30
30
|
### YandexGPT
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Для CLI нужны две вещи: API key и ID каталога Yandex Cloud.
|
|
33
|
+
|
|
34
|
+
Пошаговая настройка:
|
|
35
|
+
|
|
36
|
+
1. Откройте консоль Yandex Cloud: `https://console.yandex.cloud/`.
|
|
37
|
+
2. Войдите в аккаунт Яндекса.
|
|
38
|
+
3. Если Yandex Cloud попросит создать облако, создайте облако. Название может быть любым, например `iola-cli`.
|
|
39
|
+
4. Откройте каталог внутри облака. Если каталога нет, создайте каталог. Название может быть любым, например `default`.
|
|
40
|
+
5. На верхней панели страницы каталога найдите название каталога и его идентификатор. В нашем примере рядом с `default` был показан ID вида `b1g8...`.
|
|
41
|
+
6. Нажмите кнопку `Копировать` рядом с идентификатором каталога. Это значение будет `YANDEXGPT_FOLDER_ID`.
|
|
42
|
+
7. В левом меню или через поиск сервисов откройте `Identity and Access Management`.
|
|
43
|
+
8. Внутри `Identity and Access Management` откройте раздел `Сервисные аккаунты`.
|
|
44
|
+
9. Нажмите `Создать сервисный аккаунт`.
|
|
45
|
+
10. В поле `Имя` введите понятное имя, например `iola-cli`.
|
|
46
|
+
11. В поле `Описание` можно написать `API-доступ iola-cli к YandexGPT`.
|
|
47
|
+
12. В блоке `Роли в каталоге` нажмите `Добавить роль`.
|
|
48
|
+
13. В поле поиска роли введите `ai.languageModels.user`.
|
|
49
|
+
14. В найденной группе `ai.languageModels` выберите роль `user`. В форме должна появиться роль `ai.languageModels.user`.
|
|
50
|
+
15. Нажмите `Создать`.
|
|
51
|
+
16. После создания откройте созданный сервисный аккаунт `iola-cli`.
|
|
52
|
+
17. На странице сервисного аккаунта нажмите кнопку `Создать новый ключ`.
|
|
53
|
+
18. В выпадающем меню выберите `Создать API-ключ`. Не выбирайте `Создать статический ключ доступа` и не выбирайте `Создать авторизованный ключ`.
|
|
54
|
+
19. В поле `Описание` напишите, например, `iola-cli YandexGPT API key`.
|
|
55
|
+
20. В поле `Область действия` выберите `yc.ai.foundationModels.execute`. Если список пустой, начните вводить это значение в поле фильтра и выберите найденный вариант.
|
|
56
|
+
21. Поле `Срок действия` можно оставить пустым или выбрать дату окончания действия ключа по вашей политике безопасности.
|
|
57
|
+
22. Нажмите `Создать`.
|
|
58
|
+
23. Yandex Cloud покажет `Идентификатор ключа` и `Ваш секретный ключ`. Секретный ключ показывается только один раз.
|
|
59
|
+
24. Скопируйте `Ваш секретный ключ` и сохраните его локально. Это значение будет `YANDEXGPT_API_KEY`.
|
|
60
|
+
25. Сохраните ключ в CLI:
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
62
|
+
```bash
|
|
63
|
+
iola ai key set yandexgpt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
CLI попросит ввести:
|
|
37
67
|
|
|
38
|
-
|
|
68
|
+
- `YANDEXGPT_API_KEY` - API-ключ сервисного аккаунта;
|
|
69
|
+
- `YANDEXGPT_FOLDER_ID` - ID каталога Yandex Cloud.
|
|
70
|
+
|
|
71
|
+
Важно: `Идентификатор ключа` - это не API-ключ для CLI. Для CLI нужен именно `Ваш секретный ключ`. Если закрыть окно создания ключа, секретный ключ больше нельзя посмотреть. В этом случае удалите старый API-ключ и создайте новый.
|
|
72
|
+
|
|
73
|
+
После сохранения ключа выберите профиль и модель:
|
|
39
74
|
|
|
40
75
|
```bash
|
|
41
|
-
iola ai key set yandexgpt
|
|
42
76
|
iola ai setup yandexgpt --model yandexgpt-lite/latest
|
|
43
77
|
```
|
|
44
78
|
|
|
79
|
+
Через интерактивное меню:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
/model
|
|
83
|
+
2. Российские AI (YandexGPT/GigaChat)
|
|
84
|
+
1. YandexGPT API
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Доступные модели в CLI:
|
|
88
|
+
|
|
89
|
+
- `yandexgpt-lite/latest` - быстрый и более дешевый вариант;
|
|
90
|
+
- `yandexgpt/latest` - YandexGPT Pro latest;
|
|
91
|
+
- `yandexgpt/rc` - release candidate.
|
|
92
|
+
|
|
45
93
|
CLI также понимает env-переменные `YANDEXGPT_API_KEY` или `YANDEX_CLOUD_API_KEY`, а для каталога - `YANDEXGPT_FOLDER_ID` или `YANDEX_CLOUD_FOLDER_ID`.
|
|
46
94
|
|
|
47
95
|
### GigaChat
|
|
48
96
|
|
|
49
|
-
|
|
97
|
+
Для CLI нужен authorization key. Это не одноразовый OAuth access token: CLI сам получает OAuth-токен перед запросом, используя сохраненный authorization key.
|
|
98
|
+
|
|
99
|
+
Пошаговая настройка:
|
|
100
|
+
|
|
101
|
+
1. Перейдите на портал разработчиков Сбера: `https://developers.sber.ru/`.
|
|
102
|
+
2. Войдите в аккаунт. Если вход еще не выполнен, портал откроет страницу авторизации: `https://developers.sber.ru/studio/login`.
|
|
103
|
+
3. Откройте страницу GigaChat API. Прямой путь: `https://developers.sber.ru/studio/workspaces/my-space/get/gigachat-api`.
|
|
104
|
+
4. Нажмите кнопку подключения или `Попробовать`. После входа откроется форма создания проекта.
|
|
105
|
+
5. В форме `Почти готово` заполните `Название проекта`. Можно оставить предложенное название `Мой GigaChat API` или указать `iola-cli`.
|
|
106
|
+
6. В блоке `Группа проекта` нажмите `Выбрать группу`.
|
|
107
|
+
7. Если есть вариант `Без группы`, выберите его. Если создание проекта после этого недоступно, выберите `Создать группу`, задайте понятное имя, например `iola-cli`, и сохраните группу.
|
|
108
|
+
8. Нажмите `Создать проект`.
|
|
109
|
+
9. После создания проекта откроется страница GigaChat API. В верхней части будет название проекта, например `Мой GigaChat API`.
|
|
110
|
+
10. На странице проекта нажмите `Настроить API`. Если кнопки не видно, откройте раздел настроек проекта GigaChat API в левом меню.
|
|
111
|
+
11. На странице `Настройка API` найдите блок `Данные для авторизации запросов к API`.
|
|
112
|
+
12. Проверьте поле `Scope`. Для личного Freemium-доступа обычно указан `GIGACHAT_API_PERS`. Это значение понадобится CLI.
|
|
113
|
+
13. В блоке `Ключ авторизации` нажмите `Получить ключ`.
|
|
114
|
+
14. Откроется окно `Сохраните ваш Authorization Key`. Не закрывайте его, пока не сохраните данные.
|
|
115
|
+
15. Скопируйте поле `Authorization Key`. Это и есть значение `GIGACHAT_AUTH_KEY` для CLI.
|
|
116
|
+
16. При желании также сохраните `Client ID` и `Client Secret`, но CLI они не нужны, если вы уже скопировали `Authorization Key`.
|
|
117
|
+
17. Нажмите `Готово` только после сохранения ключа.
|
|
118
|
+
18. Сохраните ключ локально в CLI:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
iola ai key set gigachat
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
CLI попросит ввести:
|
|
50
125
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- тарифы: `https://developers.sber.ru/docs/ru/gigachat/tariffs`.
|
|
126
|
+
- `GIGACHAT_AUTH_KEY` - authorization key;
|
|
127
|
+
- `scope` - по умолчанию `GIGACHAT_API_PERS` для персонального доступа.
|
|
54
128
|
|
|
55
|
-
|
|
129
|
+
Важно: `Authorization Key` показывается один раз. Если закрыть окно и не сохранить ключ, его нельзя посмотреть повторно. В этом случае нажмите `Получить новый ключ` и сохраните новое значение. `Authorization Key` - это уже готовая строка для заголовка `Authorization: Basic ...`; вручную кодировать `Client ID:Client Secret` в base64 не нужно.
|
|
130
|
+
|
|
131
|
+
После сохранения ключа выберите профиль и модель:
|
|
56
132
|
|
|
57
133
|
```bash
|
|
58
|
-
iola ai key set gigachat
|
|
59
134
|
iola ai setup gigachat --model GigaChat-2
|
|
60
135
|
```
|
|
61
136
|
|
|
137
|
+
Через интерактивное меню:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
/model
|
|
141
|
+
2. Российские AI (YandexGPT/GigaChat)
|
|
142
|
+
2. GigaChat API
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Доступные модели в CLI:
|
|
146
|
+
|
|
147
|
+
- `GigaChat-2` - основная модель;
|
|
148
|
+
- `GigaChat-2-Pro` - повышенное качество;
|
|
149
|
+
- `GigaChat-2-Max` - максимальное качество;
|
|
150
|
+
- `GigaChat` - legacy/fallback.
|
|
151
|
+
|
|
62
152
|
CLI также понимает env-переменные `GIGACHAT_AUTH_KEY` или `GIGACHAT_API_KEY`. По умолчанию используется scope `GIGACHAT_API_PERS`; при необходимости его можно задать через `GIGACHAT_SCOPE`.
|
|
63
153
|
|
|
64
154
|
По тарифам: у GigaChat для физических лиц есть Freemium-лимит на токены; для больших объемов используются платные пакеты. У YandexGPT тарификация идет через Yandex Cloud по токенам и квотам аккаунта, поэтому актуальные бесплатные гранты или лимиты нужно проверять в консоли Yandex Cloud.
|
|
65
155
|
|
|
156
|
+
Официальная документация:
|
|
157
|
+
|
|
158
|
+
- Yandex Foundation Models: `https://yandex.cloud/ru/docs/foundation-models/`;
|
|
159
|
+
- Yandex AI Studio authentication: `https://yandex.cloud/ru/docs/ai-studio/api-ref/authentication`;
|
|
160
|
+
- Yandex Foundation Models pricing: `https://yandex.cloud/ru/docs/foundation-models/pricing`;
|
|
161
|
+
- GigaChat overview: `https://developers.sber.ru/docs/ru/gigachat/overview`;
|
|
162
|
+
- GigaChat OAuth token: `https://developers.sber.ru/docs/ru/gigachat/api/reference/rest/post-token`;
|
|
163
|
+
- GigaChat tariffs: `https://developers.sber.ru/docs/ru/gigachat/tariffs`.
|
|
164
|
+
|
|
66
165
|
## OpenAI
|
|
67
166
|
|
|
68
167
|
Получение ключа OpenAI Platform:
|