@ykurilov/agent-chat-image-library 0.0.0-stage → 0.56.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # @ykurilov/agent-chat-image-library
2
+
3
+ ## 0.56.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1eadba9: Библиотека готовых картинок: новый пакет `@ykurilov/agent-chat-image-library` с инструментами `find_images` и
8
+ `show_image` поверх каталога приложения. Картинка показывается по ширине ответа с подписью, по нажатию — окно просмотра.
9
+
10
+ - `images` у `AgentChat`, `ChatWorkspace`, `AgentPanel` принимает и список клиентов: `images={[imagesClient, imageLibraryClient]}`.
11
+ Одиночный клиент работает как раньше.
12
+ - Договор скина версии 26: у `GeneratedImages` необязательные `wide` и `caption`. Тандем их рисует. Скин версии 25
13
+ работает без правок: картинка будет плиткой ближайшей обычной пропорции (1:1, 3:2, 2:3, 16:9, 9:16) без подписи.
14
+
15
+ Подробно и как подключить — `docs/image-library.md`.
16
+
17
+ Отказ `show_image`, `show_video` и недоступный каталог в `find_images`, `find_videos` теперь ошибка инструмента (`ImageLibraryError`, `VideoLibraryError` с полем `kind`).
18
+ В ленте строка «ошибка» вместо зелёной галочки, модель получает тот же текст. Код, который читал `{ отказ }` из
19
+ результата `show_video` или `find_videos`, должен смотреть на `state: "output-error"` у части сообщения.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [1eadba9]
24
+ - @ykurilov/agent-chat-react@0.56.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yury Kurilov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # @ykurilov/agent-chat-image-library
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Готовые картинки от агента: инструменты `find_images` и `show_image` поверх каталога приложения. В ленте — картинка по
4
+ ширине ответа с подписью, по нажатию — окно просмотра. Ничего не генерирует. Отдельное расширение: приложению без
5
+ библиотеки картинок пакет не нужен.
6
+
7
+ Серверу без экрана React не нужен: `react` и `@ykurilov/agent-chat-react` — необязательные зависимости (peer). Для
8
+ `@ykurilov/agent-chat-image-library/react` оба должны стоять в приложении.
9
+
10
+ ## Сервер
11
+
12
+ ```ts
13
+ import { createImageLibrary } from "@ykurilov/agent-chat-image-library";
14
+
15
+ const library = createImageLibrary({
16
+ images: [{ id: "cell", title: "Строение клетки", alt: "Схема клетки: ядро, мембрана, митохондрии", url: "/library/cell.png", width: 1200, height: 800 }],
17
+ // или images: () => readCatalog() — каталог меняется без перезапуска
18
+ // или source: { search(query, { ownerId, chatId }), get(id, { ownerId, chatId }) } — для большой базы
19
+ // allowedHosts: ["cdn.school.ru"] — если картинки лежат не на своём сайте
20
+ });
21
+
22
+ createChatHandler({
23
+ …,
24
+ agent: { tools: (model) => ({ ...appTools(model), ...library.tools(model) }) },
25
+ });
26
+ ```
27
+
28
+ - Модель ищет через `find_images` (до 8 результатов) и показывает через `show_image` по id. Id не из каталога — ошибка
29
+ инструмента `ImageLibraryError`: в ленте строка «ошибка», модель получает тот же текст.
30
+ - После показа модель видит только «показано: название». Адрес и размеры остаются в сообщении для экрана.
31
+ - Описания инструментов постоянные: кэш модели не зависит от каталога.
32
+
33
+ ## Экран
34
+
35
+ ```tsx
36
+ import { imageLibraryClient } from "@ykurilov/agent-chat-image-library/react";
37
+
38
+ <AgentChat … images={imageLibraryClient} />
39
+ // вместе с генерацией картинок: images={[imagesClient, imageLibraryClient]}
40
+ // картинки с CDN: createImageLibraryClient({ allowedHosts: ["cdn.school.ru"] })
41
+ ```
42
+
43
+ Скин с договором версии 26 рисует картинку по ширине ответа с подписью; в Тандеме это есть. Скин версии 25 показывает её
44
+ плиткой ближайшей обычной пропорции (1:1, 3:2, 2:3, 16:9, 9:16) без подписи.
45
+
46
+ Подробно — [docs/image-library.md](../../docs/image-library.md).
@@ -0,0 +1,19 @@
1
+ // src/image-address.ts
2
+ var ROOT_CHECK_ORIGIN = "https://x.invalid";
3
+ function isImageAddress(value, allowedHosts) {
4
+ if (typeof value !== "string" || value === "" || /[\\\s\u0000-\u001f\u007f]|%2f|%5c/i.test(value)) return false;
5
+ try {
6
+ if (value.startsWith("/")) return new URL(value, ROOT_CHECK_ORIGIN).origin === ROOT_CHECK_ORIGIN;
7
+ const url = new URL(value);
8
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") return false;
9
+ const host = url.hostname.toLowerCase();
10
+ return allowedHosts.some((allowed) => allowed.toLowerCase() === host);
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export {
17
+ isImageAddress
18
+ };
19
+ //# sourceMappingURL=chunk-EXIGUWEQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/image-address.ts"],"sourcesContent":["// Адрес картинки из каталога: путь от корня своего сайта или https на домене, который приложение разрешило явно.\n// Проверка общая для сервера и экрана: подделанная запись в базе не уведёт картинку на чужой хост.\n\nconst ROOT_CHECK_ORIGIN = \"https://x.invalid\";\n\n/**\n * `/…` — свой сайт: браузер читает `/\\evil`, `/<таб>/evil` как `//evil`, поэтому путь разбирается как адрес и обязан\n * остаться на своём хосте; `\\`, пробелы, управляющие знаки и закодированные косые не допускаются. `https://…` —\n * только на хосте из `allowedHosts` (сравнение без регистра, без поддоменов), без логина и пароля в адресе.\n */\nexport function isImageAddress(value: unknown, allowedHosts: readonly string[]): value is string {\n if (typeof value !== \"string\" || value === \"\" || /[\\\\\\s\\u0000-\\u001f\\u007f]|%2f|%5c/i.test(value)) return false;\n try {\n if (value.startsWith(\"/\")) return new URL(value, ROOT_CHECK_ORIGIN).origin === ROOT_CHECK_ORIGIN;\n const url = new URL(value);\n if (url.protocol !== \"https:\" || url.username !== \"\" || url.password !== \"\") return false;\n const host = url.hostname.toLowerCase();\n return allowedHosts.some((allowed) => allowed.toLowerCase() === host);\n } catch {\n return false;\n }\n}\n"],"mappings":";AAGA,IAAM,oBAAoB;AAOnB,SAAS,eAAe,OAAgB,cAAkD;AAC/F,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM,qCAAqC,KAAK,KAAK,EAAG,QAAO;AAC1G,MAAI;AACF,QAAI,MAAM,WAAW,GAAG,EAAG,QAAO,IAAI,IAAI,OAAO,iBAAiB,EAAE,WAAW;AAC/E,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,QAAI,IAAI,aAAa,YAAY,IAAI,aAAa,MAAM,IAAI,aAAa,GAAI,QAAO;AACpF,UAAM,OAAO,IAAI,SAAS,YAAY;AACtC,WAAO,aAAa,KAAK,CAAC,YAAY,QAAQ,YAAY,MAAM,IAAI;AAAA,EACtE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ import {
2
+ isImageAddress
3
+ } from "./chunk-EXIGUWEQ.js";
4
+
5
+ // src/create-image-library.ts
6
+ import { tool } from "ai";
7
+ import { z } from "zod";
8
+
9
+ // src/image-catalog.ts
10
+ var MAX_ID_LENGTH = 200;
11
+ var MAX_FOUND = 8;
12
+ var isText = (value) => typeof value === "string" && value.trim() !== "";
13
+ var isSize = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
14
+ var isTextList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
15
+ function checkImageEntry(entry, allowedHosts) {
16
+ if (typeof entry?.id !== "string" || entry.id === "" || entry.id.length > MAX_ID_LENGTH) return "id \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u043E\u0439 \u043E\u0442 1 \u0434\u043E 200 \u0437\u043D\u0430\u043A\u043E\u0432";
17
+ if (!isText(entry.title)) return "\u043F\u0443\u0441\u0442\u043E\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435";
18
+ if (!isText(entry.alt)) return "\u043F\u0443\u0441\u0442\u043E\u0439 alt \u2014 \u043E\u043F\u0438\u0448\u0438\u0442\u0435, \u0447\u0442\u043E \u043D\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0435";
19
+ if (!isImageAddress(entry.url, allowedHosts)) return "\u0430\u0434\u0440\u0435\u0441 \u043D\u0435 \u0441\u0432\u043E\u0435\u0433\u043E \u0441\u0430\u0439\u0442\u0430 (\xAB/\u2026\xBB) \u0438 \u043D\u0435 https \u043D\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D\u043D\u043E\u043C \u0434\u043E\u043C\u0435\u043D\u0435 (allowedHosts)";
20
+ if (!isSize(entry.width)) return "\u0448\u0438\u0440\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C \u043F\u0438\u043A\u0441\u0435\u043B\u0435\u0439 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F";
21
+ if (!isSize(entry.height)) return "\u0432\u044B\u0441\u043E\u0442\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C \u043F\u0438\u043A\u0441\u0435\u043B\u0435\u0439 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F";
22
+ if (entry.description !== void 0 && typeof entry.description !== "string") return "\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0441\u0442\u0440\u043E\u043A\u0430";
23
+ if (entry.concepts !== void 0 && !isTextList(entry.concepts)) return "\u043F\u043E\u043D\u044F\u0442\u0438\u044F \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u0441\u043F\u0438\u0441\u043A\u043E\u043C \u0441\u0442\u0440\u043E\u043A";
24
+ if (entry.tags !== void 0 && !isTextList(entry.tags)) return "\u0442\u0435\u0433\u0438 \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u0441\u043F\u0438\u0441\u043A\u043E\u043C \u0441\u0442\u0440\u043E\u043A";
25
+ return void 0;
26
+ }
27
+ var nameOf = (entry, index) => typeof entry?.id === "string" && entry.id !== "" ? `\xAB${entry.id.slice(0, MAX_ID_LENGTH)}\xBB` : `\u2116${index + 1}`;
28
+ function checkImageCatalog(images, allowedHosts) {
29
+ const seen = /* @__PURE__ */ new Set();
30
+ images.forEach((image, index) => {
31
+ const reason = checkImageEntry(image, allowedHosts);
32
+ if (reason) throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A: \u0437\u0430\u043F\u0438\u0441\u044C ${nameOf(image, index)} \u2014 ${reason}.`);
33
+ if (seen.has(image.id)) throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A: id ${nameOf(image, index)} \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442\u0441\u044F.`);
34
+ seen.add(image.id);
35
+ });
36
+ }
37
+ var serviceWords = new Set(
38
+ "\u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u0440\u0438\u0441\u0443\u043D\u043E\u043A \u0440\u0438\u0441\u0443\u043D\u043A\u0430 \u0440\u0438\u0441\u0443\u043D\u043A\u0438 \u0441\u0445\u0435\u043C\u0430 \u0441\u0445\u0435\u043C\u0443 \u0441\u0445\u0435\u043C\u044B \u0444\u043E\u0442\u043E \u043F\u0440\u043E \u043A\u0430\u043A \u0447\u0442\u043E \u044D\u0442\u043E \u0434\u043B\u044F \u0438\u043B\u0438 \u043D\u0430 \u043F\u043E \u0438\u0437 \u043E\u0442 \u043F\u043E\u043A\u0430\u0436\u0438".split(" ")
39
+ );
40
+ var words = (text) => text.toLowerCase().replaceAll("\u0451", "\u0435").split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 2);
41
+ var stem = (word) => word.length > 5 ? word.slice(0, 5) : word.length >= 4 ? word.slice(0, -1) : word;
42
+ var weights = { title: 3, concepts: 3, tags: 2, description: 1 };
43
+ function score(image, queryStems) {
44
+ const fields = {
45
+ title: words(image.title),
46
+ concepts: (image.concepts ?? []).flatMap(words),
47
+ tags: (image.tags ?? []).flatMap(words),
48
+ description: [...words(image.description ?? ""), ...words(image.alt)]
49
+ };
50
+ let total = 0;
51
+ for (const queryStem of queryStems) {
52
+ for (const field of Object.keys(weights)) {
53
+ if (fields[field].some((word) => word.startsWith(queryStem))) total += weights[field];
54
+ }
55
+ }
56
+ return total;
57
+ }
58
+ function searchImages(images, query) {
59
+ const all = words(query);
60
+ const meaningful = all.filter((word) => !serviceWords.has(word));
61
+ const queryStems = [...new Set((meaningful.length > 0 ? meaningful : all).map(stem))];
62
+ if (queryStems.length === 0) return [];
63
+ return images.map((image, index) => ({ image, index, score: score(image, queryStems) })).filter((found) => found.score > 0).sort((a, b) => b.score - a.score || a.index - b.index).slice(0, MAX_FOUND).map((found) => found.image);
64
+ }
65
+
66
+ // src/image-library-error.ts
67
+ var ImageLibraryError = class extends Error {
68
+ kind;
69
+ constructor(kind, message) {
70
+ super(message);
71
+ this.name = "ImageLibraryError";
72
+ this.kind = kind;
73
+ }
74
+ };
75
+
76
+ // src/create-image-library.ts
77
+ var MAX_DESCRIPTION = 300;
78
+ var NOT_IN_CATALOG = "\u0422\u0430\u043A\u043E\u0439 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 \u043D\u0435\u0442 \u0432 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0435. \u041D\u0430\u0439\u0434\u0438 \u0435\u0451 \u0447\u0435\u0440\u0435\u0437 find_images \u0438 \u043F\u0435\u0440\u0435\u0434\u0430\u0439 id \u043E\u0442\u0442\u0443\u0434\u0430.";
79
+ var CATALOG_UNAVAILABLE = "\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D. \u0421\u043A\u0430\u0436\u0438 \u043E\u0431 \u044D\u0442\u043E\u043C \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u0438 \u043D\u0435 \u043F\u0440\u043E\u0431\u0443\u0439 \u0441\u043D\u043E\u0432\u0430 \u0432 \u044D\u0442\u043E\u043C \u043E\u0442\u0432\u0435\u0442\u0435.";
80
+ var UNAVAILABLE = /* @__PURE__ */ Symbol("\u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D");
81
+ var NOTHING_FOUND = "\u041D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u043B\u043E\u0441\u044C. \u041F\u043E\u043F\u0440\u043E\u0431\u0443\u0439 \u0434\u0440\u0443\u0433\u0438\u0435 \u0441\u043B\u043E\u0432\u0430: \u043F\u043E\u043D\u044F\u0442\u0438\u0435 \u0438\u043B\u0438 \u0442\u0435\u043C\u0443, \u0430 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0446\u0435\u043B\u0438\u043A\u043E\u043C.";
82
+ var findImagesDescription = "\u0418\u0449\u0435\u0442 \u0433\u043E\u0442\u043E\u0432\u044B\u0435 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 \u0432 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0435 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F: \u0441\u0445\u0435\u043C\u044B, \u0438\u043B\u043B\u044E\u0441\u0442\u0440\u0430\u0446\u0438\u0438, \u0440\u0438\u0441\u0443\u043D\u043A\u0438 \u043A \u043F\u043E\u043D\u044F\u0442\u0438\u044F\u043C. \u041E\u0442\u0434\u0430\u0451\u0442 \u0434\u043E 8 \u043F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0438\u0445 \u0441 id, \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435\u043C, \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435\u043C, \u043F\u043E\u043D\u044F\u0442\u0438\u044F\u043C\u0438 \u0438 \u0442\u0435\u0433\u0430\u043C\u0438. \u0412\u044B\u0437\u044B\u0432\u0430\u0439, \u043A\u043E\u0433\u0434\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043F\u043E\u043C\u043E\u0436\u0435\u0442 \u043E\u0431\u044A\u044F\u0441\u043D\u0438\u0442\u044C, \u0438\u043B\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043F\u0440\u043E\u0441\u0438\u0442 \u0435\u0451, \u0438 \u043F\u0435\u0440\u0435\u0434 show_image.";
83
+ var showImageDescription = "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443 \u0438\u0437 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 \u043F\u0440\u044F\u043C\u043E \u0432 \u043E\u0442\u0432\u0435\u0442\u0435, \u0441 \u043F\u043E\u0434\u043F\u0438\u0441\u044C\u044E. \u041F\u0435\u0440\u0435\u0434\u0430\u0439 id \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430 find_images \u2014 \u0434\u0440\u0443\u0433\u0438\u0435 id \u0438 \u0430\u0434\u0440\u0435\u0441\u0430 \u043D\u0435 \u043F\u043E\u0434\u043E\u0439\u0434\u0443\u0442. \u0421\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443 \u0432 \u043E\u0442\u0432\u0435\u0442 \u043D\u0435 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0439: \u043E\u043D\u0430 \u0443\u0436\u0435 \u043D\u0430 \u044D\u043A\u0440\u0430\u043D\u0435. \u041E\u0431\u044A\u044F\u0441\u043D\u044F\u0439 \u0441\u043B\u043E\u0432\u0430\u043C\u0438, \u0447\u0442\u043E \u043D\u0430 \u043D\u0435\u0439.";
84
+ var findImagesInput = z.object({
85
+ query: z.string().min(1).max(200).describe("\u0421\u043B\u043E\u0432\u0430 \u0434\u043B\u044F \u043F\u043E\u0438\u0441\u043A\u0430: \u043F\u043E\u043D\u044F\u0442\u0438\u0435 \u0438\u043B\u0438 \u0442\u0435\u043C\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438.")
86
+ });
87
+ var showImageInput = z.object({
88
+ id: z.string().min(1).max(200).describe("id \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 \u0438\u0437 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430 find_images.")
89
+ });
90
+ function forModel(image) {
91
+ const description = image.description?.trim();
92
+ return {
93
+ id: image.id,
94
+ \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: image.title,
95
+ ...description && { \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435: description.length > MAX_DESCRIPTION ? `${description.slice(0, MAX_DESCRIPTION - 1)}\u2026` : description },
96
+ ...image.concepts?.length && { \u043F\u043E\u043D\u044F\u0442\u0438\u044F: image.concepts },
97
+ ...image.tags?.length && { \u0442\u0435\u0433\u0438: image.tags }
98
+ };
99
+ }
100
+ var shown = (image) => ({
101
+ \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430: { id: image.id, \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: image.title, \u043F\u043E\u0434\u043F\u0438\u0441\u044C: image.alt, \u0430\u0434\u0440\u0435\u0441: image.url, \u0448\u0438\u0440\u0438\u043D\u0430: image.width, \u0432\u044B\u0441\u043E\u0442\u0430: image.height }
102
+ });
103
+ var CATALOG_TIMEOUT_MS = 5e3;
104
+ var MAX_LOGGED = 500;
105
+ var frozenCopy = (image) => Object.freeze({
106
+ ...image,
107
+ ...image.concepts && { concepts: Object.freeze([...image.concepts]) },
108
+ ...image.tags && { tags: Object.freeze([...image.tags]) }
109
+ });
110
+ async function withinTimeout(work) {
111
+ let timer;
112
+ const late = new Promise((_, reject) => {
113
+ timer = setTimeout(() => reject(new Error(`\u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B\u0430 \u0437\u0430 ${CATALOG_TIMEOUT_MS / 1e3} \u0441`)), CATALOG_TIMEOUT_MS);
114
+ });
115
+ try {
116
+ return await Promise.race([Promise.resolve().then(work), late]);
117
+ } finally {
118
+ clearTimeout(timer);
119
+ }
120
+ }
121
+ var shortId = (id) => typeof id === "string" ? id.slice(0, 200) : "(\u0431\u0435\u0437 id)";
122
+ function createImageLibrary(options) {
123
+ const logError = options.logError ?? ((message) => console.error(message));
124
+ const allowedHosts = [...options.allowedHosts ?? []];
125
+ const given = Array.isArray(options.images) ? options.images : void 0;
126
+ if (given) checkImageCatalog(given, allowedHosts);
127
+ const fixed = given && Object.freeze(given.map(frozenCopy));
128
+ const read = typeof options.images === "function" ? options.images : void 0;
129
+ const logged = /* @__PURE__ */ new Set();
130
+ const logOnce = (message) => {
131
+ if (logged.has(message)) return;
132
+ if (logged.size >= MAX_LOGGED) logged.clear();
133
+ logged.add(message);
134
+ logError(message);
135
+ };
136
+ const usable = (image) => {
137
+ if (image === void 0 || image === null) return false;
138
+ const reason = checkImageEntry(image, allowedHosts);
139
+ if (reason) logOnce(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \xAB${shortId(image?.id)}\xBB \u0438\u0437 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u0430: ${reason}.`);
140
+ return !reason;
141
+ };
142
+ const unavailable = (what, reason) => {
143
+ logError(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F: ${what} \u2014 ${reason instanceof Error ? reason.message : String(reason)}.`);
144
+ return UNAVAILABLE;
145
+ };
146
+ async function current() {
147
+ if (fixed) return fixed;
148
+ let list;
149
+ try {
150
+ list = await withinTimeout(read);
151
+ } catch (error) {
152
+ return unavailable("images() \u0443\u043F\u0430\u043B\u0430", error);
153
+ }
154
+ if (!Array.isArray(list)) return unavailable("images() \u0432\u0435\u0440\u043D\u0443\u043B\u0430 \u043D\u0435 \u0441\u043F\u0438\u0441\u043E\u043A", list === null ? "null" : typeof list);
155
+ const seen = /* @__PURE__ */ new Set();
156
+ return list.filter((image) => {
157
+ if (!usable(image)) return false;
158
+ if (seen.has(image.id)) {
159
+ logOnce(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \xAB${shortId(image.id)}\xBB \u0432 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0435 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442\u0441\u044F: \u0432\u0437\u044F\u0442\u0430 \u043F\u0435\u0440\u0432\u0430\u044F.`);
160
+ return false;
161
+ }
162
+ seen.add(image.id);
163
+ return true;
164
+ });
165
+ }
166
+ async function find(query, context) {
167
+ if (!options.source) {
168
+ const catalog = await current();
169
+ return catalog === UNAVAILABLE ? catalog : searchImages(catalog, query);
170
+ }
171
+ let found;
172
+ try {
173
+ found = await withinTimeout(() => options.source.search(query, context));
174
+ } catch (error) {
175
+ return unavailable("search \u0443\u043F\u0430\u043B", error);
176
+ }
177
+ if (!Array.isArray(found)) return unavailable("search \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435 \u0441\u043F\u0438\u0441\u043E\u043A", found === null ? "null" : typeof found);
178
+ return found.filter(usable).slice(0, MAX_FOUND);
179
+ }
180
+ async function get(id, context) {
181
+ if (!options.source) {
182
+ const catalog = await current();
183
+ return catalog === UNAVAILABLE ? catalog : catalog.find((image2) => image2.id === id);
184
+ }
185
+ let image;
186
+ try {
187
+ image = await withinTimeout(() => options.source.get(id, context));
188
+ } catch (error) {
189
+ return unavailable("get \u0443\u043F\u0430\u043B", error);
190
+ }
191
+ return usable(image) && image.id === id ? image : void 0;
192
+ }
193
+ const findImages = (context) => tool({
194
+ description: findImagesDescription,
195
+ inputSchema: findImagesInput,
196
+ execute: async ({ query }) => {
197
+ const result = await find(query, context);
198
+ if (result === UNAVAILABLE) throw new ImageLibraryError("catalog_unavailable", CATALOG_UNAVAILABLE);
199
+ const found = result.map(forModel);
200
+ return found.length > 0 ? { \u043D\u0430\u0439\u0434\u0435\u043D\u043E: found } : { \u043D\u0430\u0439\u0434\u0435\u043D\u043E: [], \u0441\u043E\u0432\u0435\u0442: NOTHING_FOUND };
201
+ }
202
+ });
203
+ const showImage = (context) => tool({
204
+ description: showImageDescription,
205
+ inputSchema: showImageInput,
206
+ execute: async ({ id }) => {
207
+ const image = await get(id, context);
208
+ if (image === UNAVAILABLE) throw new ImageLibraryError("catalog_unavailable", CATALOG_UNAVAILABLE);
209
+ if (!image) throw new ImageLibraryError("not_in_catalog", NOT_IN_CATALOG);
210
+ return shown(image);
211
+ },
212
+ // Модели — коротко, что показано; экрану — весь результат.
213
+ toModelOutput: ({ output }) => output && "\u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430" in output ? { type: "json", value: { \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043E: output.\u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430.\u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 } } : { type: "json", value: { \u043E\u0442\u043A\u0430\u0437: output?.\u043E\u0442\u043A\u0430\u0437 ?? NOT_IN_CATALOG } }
214
+ });
215
+ return {
216
+ tools(model) {
217
+ if (!model.ownerId || !model.chatId) return {};
218
+ const context = { ownerId: model.ownerId, chatId: model.chatId };
219
+ return { find_images: findImages(context), show_image: showImage(context) };
220
+ }
221
+ };
222
+ }
223
+ export {
224
+ ImageLibraryError,
225
+ checkImageCatalog,
226
+ checkImageEntry,
227
+ createImageLibrary,
228
+ isImageAddress,
229
+ searchImages
230
+ };
231
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/create-image-library.ts","../src/image-catalog.ts","../src/image-library-error.ts"],"sourcesContent":["import { tool } from \"ai\";\nimport type { ToolSet } from \"ai\";\nimport { z } from \"zod\";\nimport { MAX_FOUND, checkImageCatalog, checkImageEntry, searchImages } from \"./image-catalog\";\nimport type { ImageEntry } from \"./image-catalog\";\nimport { ImageLibraryError } from \"./image-library-error\";\n\n/** Кто спрашивает: по нему функции приложения могут учитывать права человека. */\nexport interface ImageContext {\n ownerId: string;\n chatId: string;\n}\n\n/** Каталог живыми запросами — для большой базы или своего поиска. Записи проверяются на каждом вызове. */\nexport interface ImageSource {\n search(query: string, context: ImageContext): ImageEntry[] | Promise<ImageEntry[]>;\n get(id: string, context: ImageContext): ImageEntry | undefined | Promise<ImageEntry | undefined>;\n}\n\nexport type CreateImageLibraryOptions = (\n | {\n /**\n * Массив — постоянный каталог, проверяется при создании. Функция — каталог, который меняется без перезапуска:\n * вызывается на каждом поиске и показе, новые записи видны сразу.\n */\n images: readonly ImageEntry[] | (() => readonly ImageEntry[] | Promise<readonly ImageEntry[]>);\n source?: never;\n }\n | { source: ImageSource; images?: never }\n) & {\n /** Домены, с которых можно брать картинки по `https://…` (`[\"cdn.school.ru\"]`). Пути `/…` своего сайта годятся всегда. */\n allowedHosts?: readonly string[];\n /** Куда писать о негодных записях и сбоях каталога. По умолчанию — `console.error`. */\n logError?: (message: string) => void;\n};\n\n/** Выбранная модель хода: ядро передаёт в `agent.tools(model)` и того, чей ход, и чат. */\nexport interface ImageLibraryModel {\n ownerId?: string;\n chatId?: string;\n}\n\nexport interface ImageLibrary {\n /** `{ find_images, show_image }` для `agent.tools`; без `ownerId` и `chatId` — пустой набор. */\n tools(model: ImageLibraryModel): ToolSet;\n}\n\n/**\n * Результат `show_image`: хранится в сообщении и целиком нужен показу — даже если картинку потом убрали из каталога.\n * Отказ — `ImageLibraryError`; `{ отказ }` бывает только в сообщениях, сохранённых до 0.56.\n */\nexport type ShownImage =\n | { картинка: { id: string; название: string; подпись: string; адрес: string; ширина: number; высота: number } }\n | { отказ: string };\n\nconst MAX_DESCRIPTION = 300;\nconst NOT_IN_CATALOG = \"Такой картинки нет в каталоге. Найди её через find_images и передай id оттуда.\";\nconst CATALOG_UNAVAILABLE = \"Каталог картинок сейчас недоступен. Скажи об этом человеку и не пробуй снова в этом ответе.\";\nconst UNAVAILABLE: unique symbol = Symbol(\"каталог недоступен\");\nconst NOTHING_FOUND = \"Ничего не нашлось. Попробуй другие слова: понятие или тему, а не название целиком.\";\n\n// Описания и схемы постоянные при любом каталоге: начало запроса к модели не меняется, кэш модели цел.\nconst findImagesDescription =\n \"Ищет готовые картинки в каталоге приложения: схемы, иллюстрации, рисунки к понятиям. Отдаёт до 8 подходящих с id, \" +\n \"названием, описанием, понятиями и тегами. Вызывай, когда картинка поможет объяснить, или человек просит её, и перед show_image.\";\nconst showImageDescription =\n \"Показывает человеку картинку из каталога прямо в ответе, с подписью. Передай id из результата find_images — другие id \" +\n \"и адреса не подойдут. Ссылку на картинку в ответ не вставляй: она уже на экране. Объясняй словами, что на ней.\";\n\nconst findImagesInput = z.object({\n query: z.string().min(1).max(200).describe(\"Слова для поиска: понятие или тема картинки.\"),\n});\nconst showImageInput = z.object({\n id: z.string().min(1).max(200).describe(\"id картинки из результата find_images.\"),\n});\n\nfunction forModel(image: ImageEntry) {\n const description = image.description?.trim();\n return {\n id: image.id,\n название: image.title,\n ...(description && { описание: description.length > MAX_DESCRIPTION ? `${description.slice(0, MAX_DESCRIPTION - 1)}…` : description }),\n ...(image.concepts?.length && { понятия: image.concepts }),\n ...(image.tags?.length && { теги: image.tags }),\n };\n}\n\nconst shown = (image: ImageEntry): ShownImage => ({\n картинка: { id: image.id, название: image.title, подпись: image.alt, адрес: image.url, ширина: image.width, высота: image.height },\n});\n\n/** Сколько ждать функцию каталога приложения: дольше — «каталог недоступен», ход не висит. */\nconst CATALOG_TIMEOUT_MS = 5_000;\n/** Сколько разных бед каталога помнить, чтобы не писать их снова; дальше память начинается заново. */\nconst MAX_LOGGED = 500;\n\n/** Копия записи, которую приложение уже не поменяет: каталог-массив проверен при создании и таким остаётся. */\nconst frozenCopy = (image: ImageEntry): ImageEntry =>\n Object.freeze({\n ...image,\n ...(image.concepts && { concepts: Object.freeze([...image.concepts]) as string[] }),\n ...(image.tags && { tags: Object.freeze([...image.tags]) as string[] }),\n });\n\n/** Ответ функции приложения или исключение «не ответила за 5 с». */\nasync function withinTimeout<T>(work: () => T | Promise<T>): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const late = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(`не ответила за ${CATALOG_TIMEOUT_MS / 1000} с`)), CATALOG_TIMEOUT_MS);\n });\n try {\n return await Promise.race([Promise.resolve().then(work), late]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nconst shortId = (id: unknown) => (typeof id === \"string\" ? id.slice(0, 200) : \"(без id)\");\n\n/** Инструменты библиотеки картинок поверх каталога приложения: модель ищет и показывает, сервер принимает только id из каталога. */\nexport function createImageLibrary(options: CreateImageLibraryOptions): ImageLibrary {\n const logError = options.logError ?? ((message: string) => console.error(message));\n const allowedHosts = [...(options.allowedHosts ?? [])];\n const given = Array.isArray(options.images) ? (options.images as readonly ImageEntry[]) : undefined;\n if (given) checkImageCatalog(given, allowedHosts);\n const fixed = given && Object.freeze(given.map(frozenCopy));\n const read = typeof options.images === \"function\" ? options.images : undefined;\n\n /** Одна и та же беда каталога-функции пишется в журнал один раз, а не на каждом вызове. */\n const logged = new Set<string>();\n const logOnce = (message: string) => {\n if (logged.has(message)) return;\n if (logged.size >= MAX_LOGGED) logged.clear();\n logged.add(message);\n logError(message);\n };\n\n /** Негодная запись пропускается; в журнал — только id и причина. */\n const usable = (image: ImageEntry | null | undefined): image is ImageEntry => {\n if (image === undefined || image === null) return false;\n const reason = checkImageEntry(image, allowedHosts);\n if (reason) logOnce(`Картинка «${shortId(image?.id)}» из каталога приложения пропущена: ${reason}.`);\n return !reason;\n };\n\n /** Сбой функции приложения — в журнал сервера; модели только нейтральное «недоступен», без текста исключения. */\n const unavailable = (what: string, reason: unknown): typeof UNAVAILABLE => {\n logError(`Каталог картинок приложения: ${what} — ${reason instanceof Error ? reason.message : String(reason)}.`);\n return UNAVAILABLE;\n };\n\n /** Каталог-функция: перечитывается на каждом вызове; негодные записи и повторы id отброшены. */\n async function current(): Promise<readonly ImageEntry[] | typeof UNAVAILABLE> {\n if (fixed) return fixed;\n let list: unknown;\n try {\n list = await withinTimeout(read!);\n } catch (error) {\n return unavailable(\"images() упала\", error);\n }\n if (!Array.isArray(list)) return unavailable(\"images() вернула не список\", list === null ? \"null\" : typeof list);\n const seen = new Set<string>();\n return (list as ImageEntry[]).filter((image) => {\n if (!usable(image)) return false;\n if (seen.has(image.id)) {\n logOnce(`Картинка «${shortId(image.id)}» в каталоге приложения повторяется: взята первая.`);\n return false;\n }\n seen.add(image.id);\n return true;\n });\n }\n\n async function find(query: string, context: ImageContext): Promise<ImageEntry[] | typeof UNAVAILABLE> {\n if (!options.source) {\n const catalog = await current();\n return catalog === UNAVAILABLE ? catalog : searchImages(catalog, query);\n }\n let found: unknown;\n try {\n found = await withinTimeout(() => options.source!.search(query, context));\n } catch (error) {\n return unavailable(\"search упал\", error);\n }\n if (!Array.isArray(found)) return unavailable(\"search вернул не список\", found === null ? \"null\" : typeof found);\n return (found as ImageEntry[]).filter(usable).slice(0, MAX_FOUND);\n }\n\n async function get(id: string, context: ImageContext): Promise<ImageEntry | undefined | typeof UNAVAILABLE> {\n if (!options.source) {\n const catalog = await current();\n return catalog === UNAVAILABLE ? catalog : catalog.find((image) => image.id === id);\n }\n let image: ImageEntry | null | undefined;\n try {\n image = await withinTimeout(() => options.source!.get(id, context));\n } catch (error) {\n return unavailable(\"get упал\", error);\n }\n return usable(image) && image.id === id ? image : undefined;\n }\n\n const findImages = (context: ImageContext) =>\n tool({\n description: findImagesDescription,\n inputSchema: findImagesInput,\n execute: async ({ query }) => {\n const result = await find(query, context);\n if (result === UNAVAILABLE) throw new ImageLibraryError(\"catalog_unavailable\", CATALOG_UNAVAILABLE);\n const found = result.map(forModel);\n return found.length > 0 ? { найдено: found } : { найдено: [], совет: NOTHING_FOUND };\n },\n });\n\n const showImage = (context: ImageContext) =>\n tool({\n description: showImageDescription,\n inputSchema: showImageInput,\n execute: async ({ id }): Promise<ShownImage> => {\n const image = await get(id, context);\n if (image === UNAVAILABLE) throw new ImageLibraryError(\"catalog_unavailable\", CATALOG_UNAVAILABLE);\n if (!image) throw new ImageLibraryError(\"not_in_catalog\", NOT_IN_CATALOG);\n return shown(image);\n },\n // Модели — коротко, что показано; экрану — весь результат.\n toModelOutput: ({ output }) =>\n output && \"картинка\" in output\n ? { type: \"json\", value: { показано: output.картинка.название } }\n : { type: \"json\", value: { отказ: output?.отказ ?? NOT_IN_CATALOG } },\n });\n\n return {\n tools(model): ToolSet {\n if (!model.ownerId || !model.chatId) return {};\n const context = { ownerId: model.ownerId, chatId: model.chatId };\n return { find_images: findImages(context), show_image: showImage(context) };\n },\n };\n}\n","import { isImageAddress } from \"./image-address\";\n\n/** Запись каталога приложения: что приложение знает о картинке. Сами файлы лежат у приложения. */\nexport interface ImageEntry {\n /** 1–200 знаков; модель передаёт его в `show_image`. */\n id: string;\n /** Короткое название — видимая подпись под картинкой. */\n title: string;\n /** Что изображено — для тех, кто не видит картинку (экранный диктор), и в окне просмотра. */\n alt: string;\n /** Путь от корня своего сайта `/…` или `https://…` на домене из `allowedHosts`. */\n url: string;\n /** Размеры в пикселях, целые больше нуля: место под картинку держится до загрузки. */\n width: number;\n height: number;\n /** Для поиска: о чём картинка. Модель видит первые 300 знаков. */\n description?: string;\n /** Понятия, которые картинка объясняет: по ним ищет `find_images`. */\n concepts?: string[];\n tags?: string[];\n}\n\nconst MAX_ID_LENGTH = 200;\nexport const MAX_FOUND = 8;\n\nconst isText = (value: unknown): value is string => typeof value === \"string\" && value.trim() !== \"\";\nconst isSize = (value: unknown) => typeof value === \"number\" && Number.isInteger(value) && value > 0;\nconst isTextList = (value: unknown) => Array.isArray(value) && value.every((item) => typeof item === \"string\");\n\n/** Что не так с записью; `undefined` — запись годная. Текст без содержимого записи, кроме id. */\nexport function checkImageEntry(entry: ImageEntry, allowedHosts: readonly string[]): string | undefined {\n if (typeof entry?.id !== \"string\" || entry.id === \"\" || entry.id.length > MAX_ID_LENGTH) return \"id должен быть строкой от 1 до 200 знаков\";\n if (!isText(entry.title)) return \"пустое название\";\n if (!isText(entry.alt)) return \"пустой alt — опишите, что на картинке\";\n if (!isImageAddress(entry.url, allowedHosts)) return \"адрес не своего сайта («/…») и не https на разрешённом домене (allowedHosts)\";\n if (!isSize(entry.width)) return \"ширина должна быть целым числом пикселей больше нуля\";\n if (!isSize(entry.height)) return \"высота должна быть целым числом пикселей больше нуля\";\n if (entry.description !== undefined && typeof entry.description !== \"string\") return \"описание не строка\";\n if (entry.concepts !== undefined && !isTextList(entry.concepts)) return \"понятия должны быть списком строк\";\n if (entry.tags !== undefined && !isTextList(entry.tags)) return \"теги должны быть списком строк\";\n return undefined;\n}\n\nconst nameOf = (entry: ImageEntry, index: number) => (typeof entry?.id === \"string\" && entry.id !== \"\" ? `«${entry.id.slice(0, MAX_ID_LENGTH)}»` : `№${index + 1}`);\n\n/** Каталог-массив проверяется целиком при создании: ошибка разработчика видна сразу, с id записи. */\nexport function checkImageCatalog(images: readonly ImageEntry[], allowedHosts: readonly string[]): void {\n const seen = new Set<string>();\n images.forEach((image, index) => {\n const reason = checkImageEntry(image, allowedHosts);\n if (reason) throw new Error(`Каталог картинок: запись ${nameOf(image, index)} — ${reason}.`);\n if (seen.has(image.id)) throw new Error(`Каталог картинок: id ${nameOf(image, index)} повторяется.`);\n seen.add(image.id);\n });\n}\n\n// Встроенный поиск по словам: без регистра, «ё» как «е», по началам слов — так «клетки» находит «клетка».\nconst serviceWords = new Set(\n \"картинка картинки картинку картинок изображение изображения рисунок рисунка рисунки схема схему схемы фото про как что это для или на по из от покажи\".split(\" \"),\n);\n\nconst words = (text: string) =>\n text\n .toLowerCase()\n .replaceAll(\"ё\", \"е\")\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter((word) => word.length >= 2);\n\n/** Начало слова, по которому сравниваем: окончания падежей отбрасываются. */\nconst stem = (word: string) => (word.length > 5 ? word.slice(0, 5) : word.length >= 4 ? word.slice(0, -1) : word);\n\nconst weights = { title: 3, concepts: 3, tags: 2, description: 1 } as const;\ntype Field = keyof typeof weights;\n\nfunction score(image: ImageEntry, queryStems: string[]): number {\n const fields: Record<Field, string[]> = {\n title: words(image.title),\n concepts: (image.concepts ?? []).flatMap(words),\n tags: (image.tags ?? []).flatMap(words),\n description: [...words(image.description ?? \"\"), ...words(image.alt)],\n };\n let total = 0;\n for (const queryStem of queryStems) {\n for (const field of Object.keys(weights) as Field[]) {\n if (fields[field].some((word) => word.startsWith(queryStem))) total += weights[field];\n }\n }\n return total;\n}\n\n/** До восьми подходящих: название и понятия весят больше тегов, теги — больше описания; при равенстве — порядок каталога. */\nexport function searchImages(images: readonly ImageEntry[], query: string): ImageEntry[] {\n const all = words(query);\n const meaningful = all.filter((word) => !serviceWords.has(word));\n const queryStems = [...new Set((meaningful.length > 0 ? meaningful : all).map(stem))];\n if (queryStems.length === 0) return [];\n return images\n .map((image, index) => ({ image, index, score: score(image, queryStems) }))\n .filter((found) => found.score > 0)\n .sort((a, b) => b.score - a.score || a.index - b.index)\n .slice(0, MAX_FOUND)\n .map((found) => found.image);\n}\n","export type ImageLibraryErrorKind = \"not_in_catalog\" | \"catalog_unavailable\";\n\n/**\n * Отказ `show_image` и `find_images`. Брошенная ошибка — неудача вызова: лента показывает её текст в строке инструмента, модель получает\n * тот же текст. Поэтому в нём нет адресов и текста исключений приложения.\n */\nexport class ImageLibraryError extends Error {\n readonly kind: ImageLibraryErrorKind;\n\n constructor(kind: ImageLibraryErrorKind, message: string) {\n super(message);\n this.name = \"ImageLibraryError\";\n this.kind = kind;\n }\n}\n"],"mappings":"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","names":["image"]}
@@ -0,0 +1,51 @@
1
+ import {
2
+ isImageAddress
3
+ } from "../chunk-EXIGUWEQ.js";
4
+
5
+ // src/react/image-library-client.ts
6
+ var TOOL_NAME = "show_image";
7
+ var isText = (value) => typeof value === "string" && value.trim() !== "";
8
+ var isSize = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
9
+ var KNOWN_RATIOS = [
10
+ ["1:1", 1],
11
+ ["3:2", 3 / 2],
12
+ ["2:3", 2 / 3],
13
+ ["16:9", 16 / 9],
14
+ ["9:16", 9 / 16]
15
+ ];
16
+ function nearestAspectRatio(width, height) {
17
+ const ratio = Math.log(width / height);
18
+ let best = KNOWN_RATIOS[0];
19
+ for (const known of KNOWN_RATIOS) if (Math.abs(Math.log(known[1]) - ratio) < Math.abs(Math.log(best[1]) - ratio)) best = known;
20
+ return best[0];
21
+ }
22
+ var pending = { ordered: 1, aspectRatio: "3:2", wide: true, items: [], stopped: false };
23
+ function createImageLibraryClient(options = {}) {
24
+ const allowedHosts = [...options.allowedHosts ?? []];
25
+ return {
26
+ toolName: TOOL_NAME,
27
+ describe({ part, openImage }) {
28
+ if (part.state === "input-streaming" || part.state === "input-available") return pending;
29
+ if (part.state !== "output-available") return void 0;
30
+ const output = part.output;
31
+ const image = typeof output === "object" && output !== null ? output["\u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430"] : void 0;
32
+ if (typeof image !== "object" || image === null) return void 0;
33
+ const { \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: title, \u043F\u043E\u0434\u043F\u0438\u0441\u044C: alt, \u0430\u0434\u0440\u0435\u0441: src, \u0448\u0438\u0440\u0438\u043D\u0430: width, \u0432\u044B\u0441\u043E\u0442\u0430: height } = image;
34
+ if (!isText(title) || !isText(alt) || !isImageAddress(src, allowedHosts) || !isSize(width) || !isSize(height)) return void 0;
35
+ const key = part.toolCallId;
36
+ return {
37
+ ordered: 1,
38
+ aspectRatio: nearestAspectRatio(width, height),
39
+ wide: true,
40
+ items: [{ state: "ready", key, src, alt, caption: title, width, height, onOpen: (trigger) => openImage(key, trigger) }],
41
+ stopped: false
42
+ };
43
+ }
44
+ };
45
+ }
46
+ var imageLibraryClient = createImageLibraryClient();
47
+ export {
48
+ createImageLibraryClient,
49
+ imageLibraryClient
50
+ };
51
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/image-library-client.ts"],"sourcesContent":["import type { GeneratedImagesView, ImagesClient } from \"@ykurilov/agent-chat-react\";\nimport { isImageAddress } from \"../image-address\";\n\nconst TOOL_NAME = \"show_image\";\n\nconst isText = (value: unknown): value is string => typeof value === \"string\" && value.trim() !== \"\";\nconst isSize = (value: unknown): value is number => typeof value === \"number\" && Number.isInteger(value) && value > 0;\n\n/**\n * Пропорции, которые знает каждый скин с `GeneratedImages` (те же, что у `generate_images`). Скин версии 25 не знает\n * `wide` и рисует картинку плиткой этой пропорции; незнакомую строку («1200:800») он обрезал бы квадратом.\n */\nconst KNOWN_RATIOS = [\n [\"1:1\", 1],\n [\"3:2\", 3 / 2],\n [\"2:3\", 2 / 3],\n [\"16:9\", 16 / 9],\n [\"9:16\", 9 / 16],\n] as const;\n\n/** Ближайшая знакомая пропорция по отношению сторон (сравнение в логарифмах: 2:1 и 1:2 одинаково далеки от 1:1). */\nfunction nearestAspectRatio(width: number, height: number): string {\n const ratio = Math.log(width / height);\n let best: (typeof KNOWN_RATIOS)[number] = KNOWN_RATIOS[0];\n for (const known of KNOWN_RATIOS) if (Math.abs(Math.log(known[1]) - ratio) < Math.abs(Math.log(best[1]) - ratio)) best = known;\n return best[0];\n}\n\n/** Пока вызов идёт — пустой вид: показ мгновенный, заглушка «картинка создаётся» тут неверна. */\nconst pending: GeneratedImagesView = { ordered: 1, aspectRatio: \"3:2\", wide: true, items: [], stopped: false };\n\nexport interface ImageLibraryClientOptions {\n /** Те же домены, что `allowedHosts` на сервере: картинка с другого домена не показывается. */\n allowedHosts?: readonly string[];\n}\n\n/**\n * Картинки библиотеки на экране: вызов `show_image` → картинка по ширине ответа со своей пропорцией и подписью-названием;\n * по нажатию — окно просмотра. Отказ, ошибка, «Стоп» и подделка в базе — `undefined` (строка инструмента).\n */\nexport function createImageLibraryClient(options: ImageLibraryClientOptions = {}): ImagesClient {\n const allowedHosts = [...(options.allowedHosts ?? [])];\n return {\n toolName: TOOL_NAME,\n describe({ part, openImage }) {\n if (part.state === \"input-streaming\" || part.state === \"input-available\") return pending;\n if (part.state !== \"output-available\") return undefined;\n const output = part.output as Record<string, unknown> | null;\n const image = typeof output === \"object\" && output !== null ? (output[\"картинка\"] as Record<string, unknown> | undefined) : undefined;\n if (typeof image !== \"object\" || image === null) return undefined;\n const { название: title, подпись: alt, адрес: src, ширина: width, высота: height } = image;\n if (!isText(title) || !isText(alt) || !isImageAddress(src, allowedHosts) || !isSize(width) || !isSize(height)) return undefined;\n const key = part.toolCallId;\n return {\n ordered: 1,\n aspectRatio: nearestAspectRatio(width, height),\n wide: true,\n items: [{ state: \"ready\", key, src, alt, caption: title, width, height, onOpen: (trigger) => openImage(key, trigger) }],\n stopped: false,\n };\n },\n };\n}\n\n/** Клиент для картинок только со своего сайта (`/…`). Есть CDN — `createImageLibraryClient({ allowedHosts })`. */\nexport const imageLibraryClient: ImagesClient = createImageLibraryClient();\n"],"mappings":";;;;;AAGA,IAAM,YAAY;AAElB,IAAM,SAAS,CAAC,UAAoC,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM;AAClG,IAAM,SAAS,CAAC,UAAoC,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,QAAQ;AAMpH,IAAM,eAAe;AAAA,EACnB,CAAC,OAAO,CAAC;AAAA,EACT,CAAC,OAAO,IAAI,CAAC;AAAA,EACb,CAAC,OAAO,IAAI,CAAC;AAAA,EACb,CAAC,QAAQ,KAAK,CAAC;AAAA,EACf,CAAC,QAAQ,IAAI,EAAE;AACjB;AAGA,SAAS,mBAAmB,OAAe,QAAwB;AACjE,QAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM;AACrC,MAAI,OAAsC,aAAa,CAAC;AACxD,aAAW,SAAS,aAAc,KAAI,KAAK,IAAI,KAAK,IAAI,MAAM,CAAC,CAAC,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,IAAI,KAAK,EAAG,QAAO;AACzH,SAAO,KAAK,CAAC;AACf;AAGA,IAAM,UAA+B,EAAE,SAAS,GAAG,aAAa,OAAO,MAAM,MAAM,OAAO,CAAC,GAAG,SAAS,MAAM;AAWtG,SAAS,yBAAyB,UAAqC,CAAC,GAAiB;AAC9F,QAAM,eAAe,CAAC,GAAI,QAAQ,gBAAgB,CAAC,CAAE;AACrD,SAAO;AAAA,IACL,UAAU;AAAA,IACV,SAAS,EAAE,MAAM,UAAU,GAAG;AAC5B,UAAI,KAAK,UAAU,qBAAqB,KAAK,UAAU,kBAAmB,QAAO;AACjF,UAAI,KAAK,UAAU,mBAAoB,QAAO;AAC9C,YAAM,SAAS,KAAK;AACpB,YAAM,QAAQ,OAAO,WAAW,YAAY,WAAW,OAAQ,OAAO,kDAAU,IAA4C;AAC5H,UAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,YAAM,EAAE,kDAAU,OAAO,4CAAS,KAAK,gCAAO,KAAK,sCAAQ,OAAO,sCAAQ,OAAO,IAAI;AACrF,UAAI,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,eAAe,KAAK,YAAY,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,MAAM,EAAG,QAAO;AACtH,YAAM,MAAM,KAAK;AACjB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,aAAa,mBAAmB,OAAO,MAAM;AAAA,QAC7C,MAAM;AAAA,QACN,OAAO,CAAC,EAAE,OAAO,SAAS,KAAK,KAAK,KAAK,SAAS,OAAO,OAAO,QAAQ,QAAQ,CAAC,YAAY,UAAU,KAAK,OAAO,EAAE,CAAC;AAAA,QACtH,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAGO,IAAM,qBAAmC,yBAAyB;","names":[]}
@@ -0,0 +1,55 @@
1
+ import type { ToolSet } from "ai";
2
+ import type { ImageEntry } from "./image-catalog.js";
3
+ /** Кто спрашивает: по нему функции приложения могут учитывать права человека. */
4
+ export interface ImageContext {
5
+ ownerId: string;
6
+ chatId: string;
7
+ }
8
+ /** Каталог живыми запросами — для большой базы или своего поиска. Записи проверяются на каждом вызове. */
9
+ export interface ImageSource {
10
+ search(query: string, context: ImageContext): ImageEntry[] | Promise<ImageEntry[]>;
11
+ get(id: string, context: ImageContext): ImageEntry | undefined | Promise<ImageEntry | undefined>;
12
+ }
13
+ export type CreateImageLibraryOptions = ({
14
+ /**
15
+ * Массив — постоянный каталог, проверяется при создании. Функция — каталог, который меняется без перезапуска:
16
+ * вызывается на каждом поиске и показе, новые записи видны сразу.
17
+ */
18
+ images: readonly ImageEntry[] | (() => readonly ImageEntry[] | Promise<readonly ImageEntry[]>);
19
+ source?: never;
20
+ } | {
21
+ source: ImageSource;
22
+ images?: never;
23
+ }) & {
24
+ /** Домены, с которых можно брать картинки по `https://…` (`["cdn.school.ru"]`). Пути `/…` своего сайта годятся всегда. */
25
+ allowedHosts?: readonly string[];
26
+ /** Куда писать о негодных записях и сбоях каталога. По умолчанию — `console.error`. */
27
+ logError?: (message: string) => void;
28
+ };
29
+ /** Выбранная модель хода: ядро передаёт в `agent.tools(model)` и того, чей ход, и чат. */
30
+ export interface ImageLibraryModel {
31
+ ownerId?: string;
32
+ chatId?: string;
33
+ }
34
+ export interface ImageLibrary {
35
+ /** `{ find_images, show_image }` для `agent.tools`; без `ownerId` и `chatId` — пустой набор. */
36
+ tools(model: ImageLibraryModel): ToolSet;
37
+ }
38
+ /**
39
+ * Результат `show_image`: хранится в сообщении и целиком нужен показу — даже если картинку потом убрали из каталога.
40
+ * Отказ — `ImageLibraryError`; `{ отказ }` бывает только в сообщениях, сохранённых до 0.56.
41
+ */
42
+ export type ShownImage = {
43
+ картинка: {
44
+ id: string;
45
+ название: string;
46
+ подпись: string;
47
+ адрес: string;
48
+ ширина: number;
49
+ высота: number;
50
+ };
51
+ } | {
52
+ отказ: string;
53
+ };
54
+ /** Инструменты библиотеки картинок поверх каталога приложения: модель ищет и показывает, сервер принимает только id из каталога. */
55
+ export declare function createImageLibrary(options: CreateImageLibraryOptions): ImageLibrary;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `/…` — свой сайт: браузер читает `/\evil`, `/<таб>/evil` как `//evil`, поэтому путь разбирается как адрес и обязан
3
+ * остаться на своём хосте; `\`, пробелы, управляющие знаки и закодированные косые не допускаются. `https://…` —
4
+ * только на хосте из `allowedHosts` (сравнение без регистра, без поддоменов), без логина и пароля в адресе.
5
+ */
6
+ export declare function isImageAddress(value: unknown, allowedHosts: readonly string[]): value is string;
@@ -0,0 +1,26 @@
1
+ /** Запись каталога приложения: что приложение знает о картинке. Сами файлы лежат у приложения. */
2
+ export interface ImageEntry {
3
+ /** 1–200 знаков; модель передаёт его в `show_image`. */
4
+ id: string;
5
+ /** Короткое название — видимая подпись под картинкой. */
6
+ title: string;
7
+ /** Что изображено — для тех, кто не видит картинку (экранный диктор), и в окне просмотра. */
8
+ alt: string;
9
+ /** Путь от корня своего сайта `/…` или `https://…` на домене из `allowedHosts`. */
10
+ url: string;
11
+ /** Размеры в пикселях, целые больше нуля: место под картинку держится до загрузки. */
12
+ width: number;
13
+ height: number;
14
+ /** Для поиска: о чём картинка. Модель видит первые 300 знаков. */
15
+ description?: string;
16
+ /** Понятия, которые картинка объясняет: по ним ищет `find_images`. */
17
+ concepts?: string[];
18
+ tags?: string[];
19
+ }
20
+ export declare const MAX_FOUND = 8;
21
+ /** Что не так с записью; `undefined` — запись годная. Текст без содержимого записи, кроме id. */
22
+ export declare function checkImageEntry(entry: ImageEntry, allowedHosts: readonly string[]): string | undefined;
23
+ /** Каталог-массив проверяется целиком при создании: ошибка разработчика видна сразу, с id записи. */
24
+ export declare function checkImageCatalog(images: readonly ImageEntry[], allowedHosts: readonly string[]): void;
25
+ /** До восьми подходящих: название и понятия весят больше тегов, теги — больше описания; при равенстве — порядок каталога. */
26
+ export declare function searchImages(images: readonly ImageEntry[], query: string): ImageEntry[];
@@ -0,0 +1,9 @@
1
+ export type ImageLibraryErrorKind = "not_in_catalog" | "catalog_unavailable";
2
+ /**
3
+ * Отказ `show_image` и `find_images`. Брошенная ошибка — неудача вызова: лента показывает её текст в строке инструмента, модель получает
4
+ * тот же текст. Поэтому в нём нет адресов и текста исключений приложения.
5
+ */
6
+ export declare class ImageLibraryError extends Error {
7
+ readonly kind: ImageLibraryErrorKind;
8
+ constructor(kind: ImageLibraryErrorKind, message: string);
9
+ }
@@ -0,0 +1,7 @@
1
+ export { createImageLibrary } from "./create-image-library.js";
2
+ export type { CreateImageLibraryOptions, ImageContext, ImageLibrary, ImageLibraryModel, ImageSource, ShownImage } from "./create-image-library.js";
3
+ export { checkImageCatalog, checkImageEntry, searchImages } from "./image-catalog.js";
4
+ export type { ImageEntry } from "./image-catalog.js";
5
+ export { ImageLibraryError } from "./image-library-error.js";
6
+ export type { ImageLibraryErrorKind } from "./image-library-error.js";
7
+ export { isImageAddress } from "./image-address.js";
@@ -0,0 +1,12 @@
1
+ import type { ImagesClient } from "@ykurilov/agent-chat-react";
2
+ export interface ImageLibraryClientOptions {
3
+ /** Те же домены, что `allowedHosts` на сервере: картинка с другого домена не показывается. */
4
+ allowedHosts?: readonly string[];
5
+ }
6
+ /**
7
+ * Картинки библиотеки на экране: вызов `show_image` → картинка по ширине ответа со своей пропорцией и подписью-названием;
8
+ * по нажатию — окно просмотра. Отказ, ошибка, «Стоп» и подделка в базе — `undefined` (строка инструмента).
9
+ */
10
+ export declare function createImageLibraryClient(options?: ImageLibraryClientOptions): ImagesClient;
11
+ /** Клиент для картинок только со своего сайта (`/…`). Есть CDN — `createImageLibraryClient({ allowedHosts })`. */
12
+ export declare const imageLibraryClient: ImagesClient;
@@ -0,0 +1,3 @@
1
+ export { createImageLibraryClient, imageLibraryClient } from "./image-library-client.js";
2
+ export type { ImageLibraryClientOptions } from "./image-library-client.js";
3
+ export type { ImagesClient } from "@ykurilov/agent-chat-react";
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@ykurilov/agent-chat-image-library",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.56.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "CHANGELOG.md"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "@ykurilov/agent-chat-source": "./src/index.ts",
15
+ "types": "./dist/types/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./react": {
19
+ "@ykurilov/agent-chat-source": "./src/react/index.ts",
20
+ "types": "./dist/types/react/index.d.ts",
21
+ "import": "./dist/react/index.js"
22
+ }
23
+ },
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org",
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/ykurilov/agent-chat-sdk.git",
34
+ "directory": "packages/image-library"
35
+ },
36
+ "dependencies": {
37
+ "ai": "7.0.122",
38
+ "zod": "^4.6.5"
39
+ },
40
+ "peerDependencies": {
41
+ "react": "^19.3.0",
42
+ "@ykurilov/agent-chat-react": "^0.56.0"
43
+ },
44
+ "devDependencies": {
45
+ "@testing-library/react": "^16.3.3",
46
+ "@types/react": "^19.3.0",
47
+ "jsdom": "^30.1.1",
48
+ "react": "^19.3.0",
49
+ "@ykurilov/agent-chat-react": "0.56.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "react": {
53
+ "optional": true
54
+ },
55
+ "@ykurilov/agent-chat-react": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "scripts": {
60
+ "build": "node ../../scripts/build-package.mjs",
61
+ "typecheck": "tsc -p tsconfig.json"
62
+ }
6
63
  }