@iola_adm/iola-cli 0.1.97 → 0.1.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +173 -10
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -899,8 +899,7 @@ async function startAgentRawInput() {
|
|
|
899
899
|
}
|
|
900
900
|
} finally {
|
|
901
901
|
clearAgentInputArea(state);
|
|
902
|
-
|
|
903
|
-
finishAgentTerminalLine();
|
|
902
|
+
finishAgentTerminalLine(state);
|
|
904
903
|
if (!wasRaw) input.setRawMode(false);
|
|
905
904
|
input.pause();
|
|
906
905
|
}
|
|
@@ -1425,9 +1424,21 @@ function clearAgentStatusBar(state) {
|
|
|
1425
1424
|
state.statusRows = 0;
|
|
1426
1425
|
}
|
|
1427
1426
|
|
|
1428
|
-
function finishAgentTerminalLine() {
|
|
1427
|
+
function finishAgentTerminalLine(state = null) {
|
|
1429
1428
|
if (!output.isTTY) return;
|
|
1430
|
-
output.
|
|
1429
|
+
const rows = Number(output.rows || state?.statusRows || 0);
|
|
1430
|
+
output.write("\x1b[r");
|
|
1431
|
+
if (rows >= 1) {
|
|
1432
|
+
output.write(`\x1b[${rows};1H\x1b[2K\n`);
|
|
1433
|
+
} else {
|
|
1434
|
+
output.write("\r\x1b[0K\n");
|
|
1435
|
+
}
|
|
1436
|
+
if (state) {
|
|
1437
|
+
state.statusBar = false;
|
|
1438
|
+
state.statusRows = 0;
|
|
1439
|
+
state.renderedInputLines = 0;
|
|
1440
|
+
state.renderedLines = 0;
|
|
1441
|
+
}
|
|
1431
1442
|
}
|
|
1432
1443
|
|
|
1433
1444
|
function startActivityIndicator(label = "работаю") {
|
|
@@ -6302,7 +6313,7 @@ function buildDirectDataAnswer(question, dataContext) {
|
|
|
6302
6313
|
|
|
6303
6314
|
function detectDirectDataFields(normalizedQuestion) {
|
|
6304
6315
|
const fields = [];
|
|
6305
|
-
if (/(
|
|
6316
|
+
if (/(директ|руководител|заведующ|кто возглавляет)/iu.test(normalizedQuestion)) fields.push("head");
|
|
6306
6317
|
if (/(сайт|website|url|ссылка)/iu.test(normalizedQuestion)) fields.push("website");
|
|
6307
6318
|
if (/(телефон|номер телефона|позвонить)/iu.test(normalizedQuestion)) fields.push("phone");
|
|
6308
6319
|
if (/(почт|email|e-mail|имейл|электронн)/iu.test(normalizedQuestion)) fields.push("email");
|
|
@@ -6448,6 +6459,11 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
6448
6459
|
return casualAnswer;
|
|
6449
6460
|
}
|
|
6450
6461
|
await ensureLocalData();
|
|
6462
|
+
const personRoleAnswer = buildPersonRoleDirectAnswer(question);
|
|
6463
|
+
if (personRoleAnswer) {
|
|
6464
|
+
if (!options.quiet) console.log(personRoleAnswer);
|
|
6465
|
+
return personRoleAnswer;
|
|
6466
|
+
}
|
|
6451
6467
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
6452
6468
|
if (plan.directAnswer) {
|
|
6453
6469
|
if (!options.quiet) console.log(plan.directAnswer);
|
|
@@ -6505,6 +6521,13 @@ function buildCasualDirectAnswer(question) {
|
|
|
6505
6521
|
if (/^(кто ты|что ты|какая ты модель|что ты за модель|что за модель|какая модель|назови модель|ты какая модель|ты кто)([?.!\s]*)$/iu.test(normalized)) {
|
|
6506
6522
|
return "Я IOLA, первая городская модель искусственного интеллекта Йошкар-Олы. Работаю локально в CLI и отвечаю по открытым городским данным через проверяемые слои и API.";
|
|
6507
6523
|
}
|
|
6524
|
+
if (/^(что ты умеешь|что умеешь|что можешь|чем можешь помочь|какие у тебя возможности|твои возможности)([?.!\s]*)$/iu.test(normalized)) {
|
|
6525
|
+
return [
|
|
6526
|
+
"Я помогаю работать с открытыми городскими данными Йошкар-Олы.",
|
|
6527
|
+
"Умею искать школы и детские сады, находить адреса, телефоны, сайты, email и ИНН, проверять сведения через слои данных и API, а также готовить простые списки и выгрузки.",
|
|
6528
|
+
"Если данных нет в открытом слое, я скажу об этом прямо.",
|
|
6529
|
+
].join("\n");
|
|
6530
|
+
}
|
|
6508
6531
|
if (/^(привет|здравствуй|здравствуйте|добрый день|доброе утро|добрый вечер|hi|hello|hey)([!.?\s]+(как дела|как ты|что нового)[?.!\s]*)?$/iu.test(normalized)) {
|
|
6509
6532
|
return "Привет. Работаю нормально. Могу помочь с открытыми данными Йошкар-Олы: школами, детскими садами, адресами, телефонами, сайтами и ИНН.";
|
|
6510
6533
|
}
|
|
@@ -6517,6 +6540,68 @@ function buildCasualDirectAnswer(question) {
|
|
|
6517
6540
|
return "";
|
|
6518
6541
|
}
|
|
6519
6542
|
|
|
6543
|
+
function buildPersonRoleDirectAnswer(question) {
|
|
6544
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
6545
|
+
const asksHead = /(директ|руководител|заведующ|возглавляет)/iu.test(normalized);
|
|
6546
|
+
const asksOrganization = /(какой|какая|где|чего|организац|учрежден|школ|сад|детсад|гимнази|лицей)/iu.test(normalized);
|
|
6547
|
+
if (!asksHead || !asksOrganization) return "";
|
|
6548
|
+
|
|
6549
|
+
const nameTokens = extractPersonNameTokens(question);
|
|
6550
|
+
if (nameTokens.length < 2) return "";
|
|
6551
|
+
|
|
6552
|
+
const dataset = normalized.includes("сад") || normalized.includes("детсад")
|
|
6553
|
+
? "kindergartens"
|
|
6554
|
+
: normalized.includes("школ") || normalized.includes("гимнази") || normalized.includes("лицей")
|
|
6555
|
+
? "schools"
|
|
6556
|
+
: "all";
|
|
6557
|
+
const rows = searchLocalRecords("", { dataset, limit: 10000 })
|
|
6558
|
+
.filter((row) => personTokensMatchHead(row.head, nameTokens));
|
|
6559
|
+
|
|
6560
|
+
if (rows.length === 0) {
|
|
6561
|
+
return `В открытых данных не нашел руководителя с ФИО: ${formatPersonTokens(nameTokens)}.`;
|
|
6562
|
+
}
|
|
6563
|
+
|
|
6564
|
+
const exactRows = rows.filter((row) => personTokensMatchHead(row.head, nameTokens, { strict: true }));
|
|
6565
|
+
const matches = exactRows.length > 0 ? exactRows : rows;
|
|
6566
|
+
if (matches.length === 1) {
|
|
6567
|
+
const row = matches[0];
|
|
6568
|
+
return [
|
|
6569
|
+
`${row.head} является руководителем: ${row.name}.`,
|
|
6570
|
+
row.address ? `Адрес: ${row.address}` : "",
|
|
6571
|
+
row.inn ? `ИНН: ${row.inn}` : "",
|
|
6572
|
+
"Источник: открытый слой образования.",
|
|
6573
|
+
].filter(Boolean).join("\n");
|
|
6574
|
+
}
|
|
6575
|
+
|
|
6576
|
+
return [
|
|
6577
|
+
`Нашел несколько организаций для ФИО ${formatPersonTokens(nameTokens)}:`,
|
|
6578
|
+
...matches.slice(0, 10).map((row) => `- ${row.head}: ${row.name}${row.inn ? `, ИНН ${row.inn}` : ""}`),
|
|
6579
|
+
].join("\n");
|
|
6580
|
+
}
|
|
6581
|
+
|
|
6582
|
+
function extractPersonNameTokens(question) {
|
|
6583
|
+
const stopWords = new Set([
|
|
6584
|
+
"так", "а", "и", "или", "кто", "что", "какой", "какая", "какого", "какой-то", "это",
|
|
6585
|
+
"директор", "директора", "директором", "руководитель", "руководителем", "заведующая", "заведующий",
|
|
6586
|
+
"школа", "школы", "школе", "сад", "сада", "детский", "детского", "гимназия", "лицей",
|
|
6587
|
+
"является", "возглавляет", "найди", "покажи",
|
|
6588
|
+
]);
|
|
6589
|
+
return [...String(question || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{3,}/gu)]
|
|
6590
|
+
.map((match) => match[0])
|
|
6591
|
+
.filter((token) => !stopWords.has(token));
|
|
6592
|
+
}
|
|
6593
|
+
|
|
6594
|
+
function personTokensMatchHead(head, tokens, options = {}) {
|
|
6595
|
+
const headTokens = new Set([...String(head || "").toLocaleLowerCase("ru-RU").matchAll(/\p{L}{3,}/gu)].map((match) => match[0]));
|
|
6596
|
+
if (options.strict) return tokens.every((token) => headTokens.has(token));
|
|
6597
|
+
const headText = [...headTokens].join(" ");
|
|
6598
|
+
return tokens.every((token) => headTokens.has(token) || headText.includes(token));
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
function formatPersonTokens(tokens) {
|
|
6602
|
+
return tokens.map(capitalizeFirst).join(" ");
|
|
6603
|
+
}
|
|
6604
|
+
|
|
6520
6605
|
function printToolPlan(plan) {
|
|
6521
6606
|
console.log("План выполнения:");
|
|
6522
6607
|
plan.steps.forEach((step, index) => {
|
|
@@ -6655,15 +6740,68 @@ async function searchPublicEntities(args = {}) {
|
|
|
6655
6740
|
}
|
|
6656
6741
|
|
|
6657
6742
|
async function resolvePublicEntityField(args = {}) {
|
|
6658
|
-
const
|
|
6659
|
-
|
|
6743
|
+
const endpoint = `${await getApiBaseUrl()}/resolve-entity-field`;
|
|
6744
|
+
const requestedField = normalizeEntityField(args.field);
|
|
6745
|
+
const layer = normalizeEntityLayer(args.layer);
|
|
6746
|
+
const payload = {
|
|
6747
|
+
layer,
|
|
6660
6748
|
entity_number: args.entity_number ?? args.number,
|
|
6661
6749
|
entity_name: args.entity_name || args.name,
|
|
6662
6750
|
inn: args.inn,
|
|
6663
|
-
field:
|
|
6751
|
+
field: requestedField,
|
|
6664
6752
|
must_refute_user_value: args.must_refute_user_value,
|
|
6753
|
+
};
|
|
6754
|
+
try {
|
|
6755
|
+
return await postJson(endpoint, payload);
|
|
6756
|
+
} catch (error) {
|
|
6757
|
+
const fallbackField = pickResolveFieldFallback(requestedField, error);
|
|
6758
|
+
if (fallbackField && fallbackField !== requestedField) {
|
|
6759
|
+
try {
|
|
6760
|
+
return await postJson(endpoint, { ...payload, field: fallbackField });
|
|
6761
|
+
} catch (retryError) {
|
|
6762
|
+
const resolvedBySearch = await resolvePublicEntityFieldViaSearch({ ...payload, field: fallbackField }, retryError);
|
|
6763
|
+
if (resolvedBySearch) return resolvedBySearch;
|
|
6764
|
+
throw retryError;
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
6767
|
+
const resolvedBySearch = await resolvePublicEntityFieldViaSearch(payload, error);
|
|
6768
|
+
if (resolvedBySearch) return resolvedBySearch;
|
|
6769
|
+
throw error;
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
async function resolvePublicEntityFieldViaSearch(payload, originalError) {
|
|
6774
|
+
const details = parseErrorJsonDetails(originalError);
|
|
6775
|
+
if (details?.error !== "entity_not_found") return null;
|
|
6776
|
+
if (payload.inn) return null;
|
|
6777
|
+
const query = payload.entity_name || buildEntitySearchQuery(payload.layer, payload.entity_number);
|
|
6778
|
+
if (!query) return null;
|
|
6779
|
+
const candidates = await searchPublicEntities({ layer: payload.layer, query, limit: 10 });
|
|
6780
|
+
const candidate = pickResolvedEntityCandidate(candidates, payload);
|
|
6781
|
+
if (!candidate?.inn) return null;
|
|
6782
|
+
return postJson(`${await getApiBaseUrl()}/resolve-entity-field`, {
|
|
6783
|
+
layer: payload.layer,
|
|
6784
|
+
inn: candidate.inn,
|
|
6785
|
+
field: payload.field,
|
|
6786
|
+
must_refute_user_value: payload.must_refute_user_value,
|
|
6665
6787
|
});
|
|
6666
|
-
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
function buildEntitySearchQuery(layer, number) {
|
|
6791
|
+
if (number === undefined || number === null || number === "") return "";
|
|
6792
|
+
const label = layer === "kindergartens" ? "детский сад" : "школа";
|
|
6793
|
+
return `${label} ${number}`;
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6796
|
+
function pickResolvedEntityCandidate(candidates, payload) {
|
|
6797
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
|
6798
|
+
const number = payload.entity_number === undefined || payload.entity_number === null ? "" : String(payload.entity_number);
|
|
6799
|
+
if (number) {
|
|
6800
|
+
const exact = candidates.find((item) => itemNameHasNumber(item, number));
|
|
6801
|
+
if (exact) return exact;
|
|
6802
|
+
}
|
|
6803
|
+
if (candidates.length === 1) return candidates[0];
|
|
6804
|
+
return candidates.find((item) => Number(item.score || 0) > 0) || candidates[0];
|
|
6667
6805
|
}
|
|
6668
6806
|
|
|
6669
6807
|
function normalizeEntityLayer(layer) {
|
|
@@ -6675,7 +6813,7 @@ function normalizeEntityLayer(layer) {
|
|
|
6675
6813
|
|
|
6676
6814
|
function normalizeEntityField(field) {
|
|
6677
6815
|
const value = String(field || "").toLocaleLowerCase("ru-RU");
|
|
6678
|
-
if (value === "director" || value === "head" || value.includes("
|
|
6816
|
+
if (value === "director" || value === "directors" || value === "directs" || value === "direct" || value === "head" || value === "head_name" || value.includes("директ") || value.includes("руковод")) return "director";
|
|
6679
6817
|
if (value === "site" || value === "url" || value === "website" || value.includes("сайт")) return "website";
|
|
6680
6818
|
if (value === "mail" || value === "email" || value.includes("почт")) return "email";
|
|
6681
6819
|
if (value === "phone" || value.includes("тел")) return "phone";
|
|
@@ -6684,6 +6822,31 @@ function normalizeEntityField(field) {
|
|
|
6684
6822
|
return value || "name";
|
|
6685
6823
|
}
|
|
6686
6824
|
|
|
6825
|
+
function pickResolveFieldFallback(requestedField, error) {
|
|
6826
|
+
const details = parseErrorJsonDetails(error);
|
|
6827
|
+
if (details?.error !== "field_not_public") return "";
|
|
6828
|
+
const publicFields = new Set((details.public_fields || []).map((field) => String(field)));
|
|
6829
|
+
const aliases = {
|
|
6830
|
+
director: ["director", "head_name", "head"],
|
|
6831
|
+
head: ["director", "head_name", "head"],
|
|
6832
|
+
head_name: ["director", "head_name", "head"],
|
|
6833
|
+
license_status: ["license_status", "license_number", "license_date"],
|
|
6834
|
+
}[requestedField] || [];
|
|
6835
|
+
return aliases.find((field) => field !== requestedField && publicFields.has(field)) || "";
|
|
6836
|
+
}
|
|
6837
|
+
|
|
6838
|
+
function parseErrorJsonDetails(error) {
|
|
6839
|
+
const text = error instanceof Error ? error.message : String(error || "");
|
|
6840
|
+
const match = text.match(/\{[\s\S]*\}$/);
|
|
6841
|
+
if (!match) return null;
|
|
6842
|
+
try {
|
|
6843
|
+
const parsed = JSON.parse(match[0]);
|
|
6844
|
+
return parsed.detail || parsed;
|
|
6845
|
+
} catch {
|
|
6846
|
+
return null;
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6687
6850
|
function availableToolNames(options = {}) {
|
|
6688
6851
|
const names = new Set(LOCAL_TOOLS);
|
|
6689
6852
|
for (const tool of getLocalMcpToolNames()) names.add(tool);
|