@ykurilov/agent-chat-videos 0.55.0 → 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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # @ykurilov/agent-chat-videos
2
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
25
+
3
26
  ## 0.55.0
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -22,7 +22,8 @@ createChatHandler({
22
22
  });
23
23
  ```
24
24
 
25
- - Модель ищет через `find_videos` (до 8 результатов, с главами) и показывает через `show_video` по id. Чужой id — отказ.
25
+ - Модель ищет через `find_videos` (до 8 результатов, с главами) и показывает через `show_video` по id. Чужой id — ошибка инструмента `VideoLibraryError`:
26
+ в ленте строка «ошибка», модель получает тот же текст.
26
27
  - Можно начать с нужной секунды (`startSeconds`), на карточке — «с 3:20».
27
28
  - Описания инструментов постоянные: кэш модели не зависит от каталога.
28
29
 
package/dist/index.js CHANGED
@@ -74,6 +74,16 @@ function searchVideos(videos, query) {
74
74
  return videos.map((video, index) => ({ video, index, score: score(video, queryStems) })).filter((found) => found.score > 0).sort((a, b) => b.score - a.score || a.index - b.index).slice(0, MAX_FOUND).map((found) => found.video);
75
75
  }
76
76
 
77
+ // src/video-library-error.ts
78
+ var VideoLibraryError = class extends Error {
79
+ kind;
80
+ constructor(kind, message) {
81
+ super(message);
82
+ this.name = "VideoLibraryError";
83
+ this.kind = kind;
84
+ }
85
+ };
86
+
77
87
  // src/create-video-library.ts
78
88
  var MAX_DESCRIPTION = 300;
79
89
  var NOT_IN_CATALOG = "\u0422\u0430\u043A\u043E\u0433\u043E \u0432\u0438\u0434\u0435\u043E \u043D\u0435\u0442 \u0432 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0435. \u041D\u0430\u0439\u0434\u0438 \u0435\u0433\u043E \u0447\u0435\u0440\u0435\u0437 find_videos \u0438 \u043F\u0435\u0440\u0435\u0434\u0430\u0439 id \u043E\u0442\u0442\u0443\u0434\u0430.";
@@ -157,7 +167,7 @@ function createVideoLibrary(options) {
157
167
  inputSchema: findVideosInput,
158
168
  execute: async ({ query }) => {
159
169
  const result = await find(query, context);
160
- if (result === CATALOG_UNAVAILABLE_MARK) return { \u043E\u0442\u043A\u0430\u0437: CATALOG_UNAVAILABLE };
170
+ if (result === CATALOG_UNAVAILABLE_MARK) throw new VideoLibraryError("catalog_unavailable", CATALOG_UNAVAILABLE);
161
171
  const found = result.map(forModel);
162
172
  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 };
163
173
  }
@@ -167,10 +177,10 @@ function createVideoLibrary(options) {
167
177
  inputSchema: showVideoInput,
168
178
  execute: async ({ id, startSeconds }) => {
169
179
  const video = await get(id, context);
170
- if (video === CATALOG_UNAVAILABLE_MARK) return { \u043E\u0442\u043A\u0430\u0437: CATALOG_UNAVAILABLE };
171
- if (!video) return { \u043E\u0442\u043A\u0430\u0437: NOT_IN_CATALOG };
180
+ if (video === CATALOG_UNAVAILABLE_MARK) throw new VideoLibraryError("catalog_unavailable", CATALOG_UNAVAILABLE);
181
+ if (!video) throw new VideoLibraryError("not_in_catalog", NOT_IN_CATALOG);
172
182
  if (startSeconds !== void 0 && video.durationSeconds !== void 0 && startSeconds >= video.durationSeconds) {
173
- return { \u043E\u0442\u043A\u0430\u0437: `\u041D\u0430\u0447\u0430\u043B\u043E ${timeLabel(startSeconds)} \u0437\u0430 \u043A\u043E\u043D\u0446\u043E\u043C \u0432\u0438\u0434\u0435\u043E: \u043E\u043D\u043E \u0434\u043B\u0438\u0442\u0441\u044F ${timeLabel(video.durationSeconds)}.` };
183
+ throw new VideoLibraryError("start_after_end", `\u041D\u0430\u0447\u0430\u043B\u043E ${timeLabel(startSeconds)} \u0437\u0430 \u043A\u043E\u043D\u0446\u043E\u043C \u0432\u0438\u0434\u0435\u043E: \u043E\u043D\u043E \u0434\u043B\u0438\u0442\u0441\u044F ${timeLabel(video.durationSeconds)}.`);
174
184
  }
175
185
  return shown(video, startSeconds);
176
186
  },
@@ -192,6 +202,7 @@ function createVideoLibrary(options) {
192
202
  };
193
203
  }
194
204
  export {
205
+ VideoLibraryError,
195
206
  createVideoLibrary,
196
207
  embedUrl,
197
208
  isVideoAddress,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/create-video-library.ts","../src/video-catalog.ts"],"sourcesContent":["import { tool } from \"ai\";\nimport type { ToolSet } from \"ai\";\nimport { z } from \"zod\";\nimport { timeLabel } from \"./time-label\";\nimport { parseVideoUrl } from \"./video-address\";\nimport type { VideoProvider } from \"./video-address\";\nimport { MAX_FOUND, checkVideoCatalog, checkVideoEntry, searchVideos } from \"./video-catalog\";\nimport type { VideoEntry } from \"./video-catalog\";\n\n/** Кто спрашивает: по нему функции приложения могут учитывать права человека. */\nexport interface VideoContext {\n ownerId: string;\n chatId: string;\n}\n\n/** Каталог функциями — для большой базы или своего поиска. Записи проверяются на каждом вызове. */\nexport interface VideoSource {\n search(query: string, context: VideoContext): VideoEntry[] | Promise<VideoEntry[]>;\n get(id: string, context: VideoContext): VideoEntry | undefined | Promise<VideoEntry | undefined>;\n}\n\nexport type CreateVideoLibraryOptions = ({ videos: VideoEntry[]; source?: never } | { source: VideoSource; videos?: never }) & {\n /** Куда писать о негодных записях из функций приложения. По умолчанию — `console.error`. */\n logError?: (message: string) => void;\n};\n\n/** Выбранная модель хода: ядро передаёт в `agent.tools(model)` и того, чей ход, и чат. */\nexport interface VideoLibraryModel {\n ownerId?: string;\n chatId?: string;\n}\n\nexport interface VideoLibrary {\n /** `{ find_videos, show_video }` для `agent.tools`; без `ownerId` и `chatId` — пустой набор. */\n tools(model: VideoLibraryModel): ToolSet;\n}\n\n/** Результат `show_video`: хранится в сообщении и целиком нужен карточке — даже если видео потом убрали из каталога. */\nexport type ShownVideo =\n | {\n видео: { id: string; название: string; площадка: VideoProvider; код: string; длительность?: number; обложка?: string; начало?: number };\n }\n | { отказ: string };\n\nconst MAX_DESCRIPTION = 300;\nconst NOT_IN_CATALOG = \"Такого видео нет в каталоге. Найди его через find_videos и передай id оттуда.\";\nconst CATALOG_UNAVAILABLE = \"Каталог видео сейчас недоступен. Скажи об этом человеку и не пробуй снова в этом ответе.\";\nconst CATALOG_UNAVAILABLE_MARK: unique symbol = Symbol(\"каталог недоступен\");\nconst NOTHING_FOUND = \"Ничего не нашлось. Попробуй другие слова: тему или ключевое понятие, а не название целиком.\";\n\n// Описания и схемы постоянные при любом каталоге: начало запроса к модели не меняется, кэш модели цел (SC-003).\nconst findVideosDescription =\n \"Ищет видео в каталоге приложения: уроки, лекции, записи. Отдаёт до 8 подходящих с id, названием, описанием, \" +\n \"длительностью и главами. Вызывай, когда человек просит видео, урок или запись по теме, и перед show_video.\";\nconst showVideoDescription =\n \"Показывает человеку видео из каталога карточкой с плеером. Передай id из результата find_videos — другие id не подойдут. \" +\n \"startSeconds — с какой секунды начать, если нужна конкретная глава (возьми «секунда» из глав). \" +\n \"Ссылку на видео в ответ не вставляй: карточка уже на экране. Назови видео одной фразой.\";\n\nconst findVideosInput = z.object({\n query: z.string().min(1).max(200).describe(\"Слова для поиска: тема или понятие, о котором видео.\"),\n});\nconst showVideoInput = z.object({\n id: z.string().min(1).max(200).describe(\"id видео из результата find_videos.\"),\n startSeconds: z.number().int().min(0).optional().describe(\"С какой секунды начать. Не указывай, чтобы начать с начала.\"),\n});\n\nfunction forModel(video: VideoEntry) {\n const description = video.description?.trim();\n return {\n id: video.id,\n название: video.title,\n ...(description && { описание: description.length > MAX_DESCRIPTION ? `${description.slice(0, MAX_DESCRIPTION - 1)}…` : description }),\n ...(video.durationSeconds !== undefined && { длительность: timeLabel(video.durationSeconds) }),\n ...(video.chapters?.length && {\n главы: video.chapters.map((chapter) => ({ название: chapter.title, начало: timeLabel(chapter.startSeconds), секунда: chapter.startSeconds })),\n }),\n };\n}\n\nfunction shown(video: VideoEntry, startSeconds: number | undefined): ShownVideo {\n const address = parseVideoUrl(video.url)!;\n return {\n видео: {\n id: video.id,\n название: video.title,\n площадка: address.provider,\n код: address.code,\n ...(video.durationSeconds !== undefined && { длительность: video.durationSeconds }),\n ...(video.poster !== undefined && { обложка: video.poster }),\n ...(startSeconds && { начало: startSeconds }),\n },\n };\n}\n\nconst shortId = (id: unknown) => (typeof id === \"string\" ? id.slice(0, 200) : \"(без id)\");\n\n/** Инструменты видео поверх каталога приложения: модель ищет и показывает, сервер принимает только id из каталога. */\nexport function createVideoLibrary(options: CreateVideoLibraryOptions): VideoLibrary {\n const logError = options.logError ?? ((message: string) => console.error(message));\n const catalog = options.videos ? [...options.videos] : undefined;\n if (catalog) checkVideoCatalog(catalog);\n const byId = new Map(catalog?.map((video) => [video.id, video]));\n\n /** Негодная запись из базы приложения пропускается; в журнал — только id и причина. */\n const usable = (video: VideoEntry | null | undefined): video is VideoEntry => {\n if (video === undefined || video === null) return false;\n const reason = checkVideoEntry(video);\n if (reason) logError(`Видео «${shortId(video?.id)}» из каталога приложения пропущено: ${reason}.`);\n return !reason;\n };\n\n /** Сбой функции приложения — в журнал сервера; модели только нейтральное «недоступен», без текста исключения. */\n const unavailable = (what: string, reason: unknown): typeof CATALOG_UNAVAILABLE_MARK => {\n logError(`Каталог видео приложения: ${what} — ${reason instanceof Error ? reason.message : String(reason)}.`);\n return CATALOG_UNAVAILABLE_MARK;\n };\n\n async function find(query: string, context: VideoContext): Promise<VideoEntry[] | typeof CATALOG_UNAVAILABLE_MARK> {\n if (catalog) return searchVideos(catalog, query);\n let found: unknown;\n try {\n found = await 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 VideoEntry[]).filter(usable).slice(0, MAX_FOUND);\n }\n\n async function get(id: string, context: VideoContext): Promise<VideoEntry | undefined | typeof CATALOG_UNAVAILABLE_MARK> {\n if (catalog) return byId.get(id);\n let video: VideoEntry | null | undefined;\n try {\n video = await options.source!.get(id, context);\n } catch (error) {\n return unavailable(\"get упал\", error);\n }\n return usable(video) && video.id === id ? video : undefined;\n }\n\n const findVideos = (context: VideoContext) =>\n tool({\n description: findVideosDescription,\n inputSchema: findVideosInput,\n execute: async ({ query }) => {\n const result = await find(query, context);\n if (result === CATALOG_UNAVAILABLE_MARK) return { отказ: CATALOG_UNAVAILABLE };\n const found = result.map(forModel);\n return found.length > 0 ? { найдено: found } : { найдено: [], совет: NOTHING_FOUND };\n },\n });\n\n const showVideo = (context: VideoContext) =>\n tool({\n description: showVideoDescription,\n inputSchema: showVideoInput,\n execute: async ({ id, startSeconds }): Promise<ShownVideo> => {\n const video = await get(id, context);\n if (video === CATALOG_UNAVAILABLE_MARK) return { отказ: CATALOG_UNAVAILABLE };\n if (!video) return { отказ: NOT_IN_CATALOG };\n if (startSeconds !== undefined && video.durationSeconds !== undefined && startSeconds >= video.durationSeconds) {\n return { отказ: `Начало ${timeLabel(startSeconds)} за концом видео: оно длится ${timeLabel(video.durationSeconds)}.` };\n }\n return shown(video, startSeconds);\n },\n // Модели — коротко, что показано; карточке — весь результат.\n toModelOutput: ({ output }) => {\n if (output && \"видео\" in output) {\n const { название, начало } = output.видео;\n return { type: \"json\", value: { показано: название, ...(начало && { с: timeLabel(начало) }) } };\n }\n return { type: \"json\", value: { отказ: output?.отказ ?? NOT_IN_CATALOG } };\n },\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_videos: findVideos(context), show_video: showVideo(context) };\n },\n };\n}\n","import { isPosterAddress, parseVideoUrl } from \"./video-address\";\n\n/** Запись каталога приложения: что приложение знает о видео. */\nexport interface VideoEntry {\n /** 1–200 знаков; модель передаёт его в `show_video`. */\n id: string;\n title: string;\n /** Ссылка на YouTube или Kinescope. */\n url: string;\n description?: string;\n /** Целое больше нуля. */\n durationSeconds?: number;\n /** `https://…` или путь от корня сайта `/…`. Нет — скин рисует заглушку, без запросов к площадке. */\n poster?: string;\n chapters?: Array<{ title: string; startSeconds: number }>;\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() !== \"\";\n\n/** Что не так с записью; `undefined` — запись годная. Текст без содержимого записи, кроме id. */\nexport function checkVideoEntry(entry: VideoEntry): 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 (typeof entry.url !== \"string\" || !parseVideoUrl(entry.url)) return \"ссылка не на YouTube и не на Kinescope\";\n if (entry.description !== undefined && typeof entry.description !== \"string\") return \"описание не строка\";\n const duration = entry.durationSeconds;\n if (duration !== undefined && (!Number.isInteger(duration) || duration <= 0)) return \"длительность должна быть целым числом секунд больше нуля\";\n if (entry.poster !== undefined && !isPosterAddress(entry.poster)) {\n return \"обложка должна быть адресом https:// или путём от корня сайта «/…»\";\n }\n if (entry.chapters !== undefined) {\n if (!Array.isArray(entry.chapters)) return \"главы должны быть списком\";\n for (const chapter of entry.chapters) {\n if (!isText(chapter?.title)) return \"глава без названия\";\n const start = chapter.startSeconds;\n if (!Number.isInteger(start) || start < 0) return \"начало главы должно быть целым числом секунд от нуля\";\n if (duration !== undefined && start >= duration) return \"глава начинается за концом видео\";\n }\n }\n return undefined;\n}\n\n/** Каталог-массив проверяется целиком при создании: ошибка разработчика видна сразу, с id записи. */\nexport function checkVideoCatalog(videos: readonly VideoEntry[]): void {\n const seen = new Set<string>();\n videos.forEach((video, index) => {\n const reason = checkVideoEntry(video);\n const name = typeof video?.id === \"string\" && video.id !== \"\" ? `«${video.id}»` : `№${index + 1}`;\n if (reason) throw new Error(`Каталог видео: запись ${name} — ${reason}.`);\n if (seen.has(video.id)) throw new Error(`Каталог видео: id ${name} повторяется.`);\n seen.add(video.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, chapters: 2, description: 1 } as const;\n\nfunction score(video: VideoEntry, queryStems: string[]): number {\n const fields = {\n title: words(video.title),\n chapters: (video.chapters ?? []).flatMap((chapter) => words(chapter.title)),\n description: words(video.description ?? \"\"),\n };\n let total = 0;\n for (const queryStem of queryStems) {\n for (const field of [\"title\", \"chapters\", \"description\"] as const) {\n if (fields[field].some((word) => word.startsWith(queryStem))) total += weights[field];\n }\n }\n return total;\n}\n\n/** До восьми подходящих: название весит больше глав, главы — больше описания; при равенстве — порядок каталога. */\nexport function searchVideos(videos: readonly VideoEntry[], query: string): VideoEntry[] {\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 videos\n .map((video, index) => ({ video, index, score: score(video, 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.video);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/create-video-library.ts","../src/video-catalog.ts","../src/video-library-error.ts"],"sourcesContent":["import { tool } from \"ai\";\nimport type { ToolSet } from \"ai\";\nimport { z } from \"zod\";\nimport { timeLabel } from \"./time-label\";\nimport { parseVideoUrl } from \"./video-address\";\nimport type { VideoProvider } from \"./video-address\";\nimport { MAX_FOUND, checkVideoCatalog, checkVideoEntry, searchVideos } from \"./video-catalog\";\nimport type { VideoEntry } from \"./video-catalog\";\nimport { VideoLibraryError } from \"./video-library-error\";\n\n/** Кто спрашивает: по нему функции приложения могут учитывать права человека. */\nexport interface VideoContext {\n ownerId: string;\n chatId: string;\n}\n\n/** Каталог функциями — для большой базы или своего поиска. Записи проверяются на каждом вызове. */\nexport interface VideoSource {\n search(query: string, context: VideoContext): VideoEntry[] | Promise<VideoEntry[]>;\n get(id: string, context: VideoContext): VideoEntry | undefined | Promise<VideoEntry | undefined>;\n}\n\nexport type CreateVideoLibraryOptions = ({ videos: VideoEntry[]; source?: never } | { source: VideoSource; videos?: never }) & {\n /** Куда писать о негодных записях из функций приложения. По умолчанию — `console.error`. */\n logError?: (message: string) => void;\n};\n\n/** Выбранная модель хода: ядро передаёт в `agent.tools(model)` и того, чей ход, и чат. */\nexport interface VideoLibraryModel {\n ownerId?: string;\n chatId?: string;\n}\n\nexport interface VideoLibrary {\n /** `{ find_videos, show_video }` для `agent.tools`; без `ownerId` и `chatId` — пустой набор. */\n tools(model: VideoLibraryModel): ToolSet;\n}\n\n/**\n * Результат `show_video`: хранится в сообщении и целиком нужен карточке — даже если видео потом убрали из каталога.\n * Отказ — `VideoLibraryError`; `{ отказ }` бывает только в сообщениях, сохранённых до 0.56.\n */\nexport type ShownVideo =\n | {\n видео: { id: string; название: string; площадка: VideoProvider; код: string; длительность?: number; обложка?: string; начало?: number };\n }\n | { отказ: string };\n\nconst MAX_DESCRIPTION = 300;\nconst NOT_IN_CATALOG = \"Такого видео нет в каталоге. Найди его через find_videos и передай id оттуда.\";\nconst CATALOG_UNAVAILABLE = \"Каталог видео сейчас недоступен. Скажи об этом человеку и не пробуй снова в этом ответе.\";\nconst CATALOG_UNAVAILABLE_MARK: unique symbol = Symbol(\"каталог недоступен\");\nconst NOTHING_FOUND = \"Ничего не нашлось. Попробуй другие слова: тему или ключевое понятие, а не название целиком.\";\n\n// Описания и схемы постоянные при любом каталоге: начало запроса к модели не меняется, кэш модели цел (SC-003).\nconst findVideosDescription =\n \"Ищет видео в каталоге приложения: уроки, лекции, записи. Отдаёт до 8 подходящих с id, названием, описанием, \" +\n \"длительностью и главами. Вызывай, когда человек просит видео, урок или запись по теме, и перед show_video.\";\nconst showVideoDescription =\n \"Показывает человеку видео из каталога карточкой с плеером. Передай id из результата find_videos — другие id не подойдут. \" +\n \"startSeconds — с какой секунды начать, если нужна конкретная глава (возьми «секунда» из глав). \" +\n \"Ссылку на видео в ответ не вставляй: карточка уже на экране. Назови видео одной фразой.\";\n\nconst findVideosInput = z.object({\n query: z.string().min(1).max(200).describe(\"Слова для поиска: тема или понятие, о котором видео.\"),\n});\nconst showVideoInput = z.object({\n id: z.string().min(1).max(200).describe(\"id видео из результата find_videos.\"),\n startSeconds: z.number().int().min(0).optional().describe(\"С какой секунды начать. Не указывай, чтобы начать с начала.\"),\n});\n\nfunction forModel(video: VideoEntry) {\n const description = video.description?.trim();\n return {\n id: video.id,\n название: video.title,\n ...(description && { описание: description.length > MAX_DESCRIPTION ? `${description.slice(0, MAX_DESCRIPTION - 1)}…` : description }),\n ...(video.durationSeconds !== undefined && { длительность: timeLabel(video.durationSeconds) }),\n ...(video.chapters?.length && {\n главы: video.chapters.map((chapter) => ({ название: chapter.title, начало: timeLabel(chapter.startSeconds), секунда: chapter.startSeconds })),\n }),\n };\n}\n\nfunction shown(video: VideoEntry, startSeconds: number | undefined): ShownVideo {\n const address = parseVideoUrl(video.url)!;\n return {\n видео: {\n id: video.id,\n название: video.title,\n площадка: address.provider,\n код: address.code,\n ...(video.durationSeconds !== undefined && { длительность: video.durationSeconds }),\n ...(video.poster !== undefined && { обложка: video.poster }),\n ...(startSeconds && { начало: startSeconds }),\n },\n };\n}\n\nconst shortId = (id: unknown) => (typeof id === \"string\" ? id.slice(0, 200) : \"(без id)\");\n\n/** Инструменты видео поверх каталога приложения: модель ищет и показывает, сервер принимает только id из каталога. */\nexport function createVideoLibrary(options: CreateVideoLibraryOptions): VideoLibrary {\n const logError = options.logError ?? ((message: string) => console.error(message));\n const catalog = options.videos ? [...options.videos] : undefined;\n if (catalog) checkVideoCatalog(catalog);\n const byId = new Map(catalog?.map((video) => [video.id, video]));\n\n /** Негодная запись из базы приложения пропускается; в журнал — только id и причина. */\n const usable = (video: VideoEntry | null | undefined): video is VideoEntry => {\n if (video === undefined || video === null) return false;\n const reason = checkVideoEntry(video);\n if (reason) logError(`Видео «${shortId(video?.id)}» из каталога приложения пропущено: ${reason}.`);\n return !reason;\n };\n\n /** Сбой функции приложения — в журнал сервера; модели только нейтральное «недоступен», без текста исключения. */\n const unavailable = (what: string, reason: unknown): typeof CATALOG_UNAVAILABLE_MARK => {\n logError(`Каталог видео приложения: ${what} — ${reason instanceof Error ? reason.message : String(reason)}.`);\n return CATALOG_UNAVAILABLE_MARK;\n };\n\n async function find(query: string, context: VideoContext): Promise<VideoEntry[] | typeof CATALOG_UNAVAILABLE_MARK> {\n if (catalog) return searchVideos(catalog, query);\n let found: unknown;\n try {\n found = await 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 VideoEntry[]).filter(usable).slice(0, MAX_FOUND);\n }\n\n async function get(id: string, context: VideoContext): Promise<VideoEntry | undefined | typeof CATALOG_UNAVAILABLE_MARK> {\n if (catalog) return byId.get(id);\n let video: VideoEntry | null | undefined;\n try {\n video = await options.source!.get(id, context);\n } catch (error) {\n return unavailable(\"get упал\", error);\n }\n return usable(video) && video.id === id ? video : undefined;\n }\n\n const findVideos = (context: VideoContext) =>\n tool({\n description: findVideosDescription,\n inputSchema: findVideosInput,\n execute: async ({ query }) => {\n const result = await find(query, context);\n if (result === CATALOG_UNAVAILABLE_MARK) throw new VideoLibraryError(\"catalog_unavailable\", CATALOG_UNAVAILABLE);\n const found = result.map(forModel);\n return found.length > 0 ? { найдено: found } : { найдено: [], совет: NOTHING_FOUND };\n },\n });\n\n const showVideo = (context: VideoContext) =>\n tool({\n description: showVideoDescription,\n inputSchema: showVideoInput,\n execute: async ({ id, startSeconds }): Promise<ShownVideo> => {\n const video = await get(id, context);\n if (video === CATALOG_UNAVAILABLE_MARK) throw new VideoLibraryError(\"catalog_unavailable\", CATALOG_UNAVAILABLE);\n if (!video) throw new VideoLibraryError(\"not_in_catalog\", NOT_IN_CATALOG);\n if (startSeconds !== undefined && video.durationSeconds !== undefined && startSeconds >= video.durationSeconds) {\n throw new VideoLibraryError(\"start_after_end\", `Начало ${timeLabel(startSeconds)} за концом видео: оно длится ${timeLabel(video.durationSeconds)}.`);\n }\n return shown(video, startSeconds);\n },\n // Модели — коротко, что показано; карточке — весь результат.\n toModelOutput: ({ output }) => {\n if (output && \"видео\" in output) {\n const { название, начало } = output.видео;\n return { type: \"json\", value: { показано: название, ...(начало && { с: timeLabel(начало) }) } };\n }\n return { type: \"json\", value: { отказ: output?.отказ ?? NOT_IN_CATALOG } };\n },\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_videos: findVideos(context), show_video: showVideo(context) };\n },\n };\n}\n","import { isPosterAddress, parseVideoUrl } from \"./video-address\";\n\n/** Запись каталога приложения: что приложение знает о видео. */\nexport interface VideoEntry {\n /** 1–200 знаков; модель передаёт его в `show_video`. */\n id: string;\n title: string;\n /** Ссылка на YouTube или Kinescope. */\n url: string;\n description?: string;\n /** Целое больше нуля. */\n durationSeconds?: number;\n /** `https://…` или путь от корня сайта `/…`. Нет — скин рисует заглушку, без запросов к площадке. */\n poster?: string;\n chapters?: Array<{ title: string; startSeconds: number }>;\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() !== \"\";\n\n/** Что не так с записью; `undefined` — запись годная. Текст без содержимого записи, кроме id. */\nexport function checkVideoEntry(entry: VideoEntry): 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 (typeof entry.url !== \"string\" || !parseVideoUrl(entry.url)) return \"ссылка не на YouTube и не на Kinescope\";\n if (entry.description !== undefined && typeof entry.description !== \"string\") return \"описание не строка\";\n const duration = entry.durationSeconds;\n if (duration !== undefined && (!Number.isInteger(duration) || duration <= 0)) return \"длительность должна быть целым числом секунд больше нуля\";\n if (entry.poster !== undefined && !isPosterAddress(entry.poster)) {\n return \"обложка должна быть адресом https:// или путём от корня сайта «/…»\";\n }\n if (entry.chapters !== undefined) {\n if (!Array.isArray(entry.chapters)) return \"главы должны быть списком\";\n for (const chapter of entry.chapters) {\n if (!isText(chapter?.title)) return \"глава без названия\";\n const start = chapter.startSeconds;\n if (!Number.isInteger(start) || start < 0) return \"начало главы должно быть целым числом секунд от нуля\";\n if (duration !== undefined && start >= duration) return \"глава начинается за концом видео\";\n }\n }\n return undefined;\n}\n\n/** Каталог-массив проверяется целиком при создании: ошибка разработчика видна сразу, с id записи. */\nexport function checkVideoCatalog(videos: readonly VideoEntry[]): void {\n const seen = new Set<string>();\n videos.forEach((video, index) => {\n const reason = checkVideoEntry(video);\n const name = typeof video?.id === \"string\" && video.id !== \"\" ? `«${video.id}»` : `№${index + 1}`;\n if (reason) throw new Error(`Каталог видео: запись ${name} — ${reason}.`);\n if (seen.has(video.id)) throw new Error(`Каталог видео: id ${name} повторяется.`);\n seen.add(video.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, chapters: 2, description: 1 } as const;\n\nfunction score(video: VideoEntry, queryStems: string[]): number {\n const fields = {\n title: words(video.title),\n chapters: (video.chapters ?? []).flatMap((chapter) => words(chapter.title)),\n description: words(video.description ?? \"\"),\n };\n let total = 0;\n for (const queryStem of queryStems) {\n for (const field of [\"title\", \"chapters\", \"description\"] as const) {\n if (fields[field].some((word) => word.startsWith(queryStem))) total += weights[field];\n }\n }\n return total;\n}\n\n/** До восьми подходящих: название весит больше глав, главы — больше описания; при равенстве — порядок каталога. */\nexport function searchVideos(videos: readonly VideoEntry[], query: string): VideoEntry[] {\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 videos\n .map((video, index) => ({ video, index, score: score(video, 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.video);\n}\n","export type VideoLibraryErrorKind = \"not_in_catalog\" | \"catalog_unavailable\" | \"start_after_end\";\n\n/**\n * Отказ `show_video` и `find_videos`. Брошенная ошибка — неудача вызова: лента показывает её текст в строке инструмента, модель получает\n * тот же текст. Поэтому в нём нет ссылок и текста исключений приложения.\n */\nexport class VideoLibraryError extends Error {\n readonly kind: VideoLibraryErrorKind;\n\n constructor(kind: VideoLibraryErrorKind, message: string) {\n super(message);\n this.name = \"VideoLibraryError\";\n this.kind = kind;\n }\n}\n"],"mappings":"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","names":[]}
@@ -30,7 +30,10 @@ export interface VideoLibrary {
30
30
  /** `{ find_videos, show_video }` для `agent.tools`; без `ownerId` и `chatId` — пустой набор. */
31
31
  tools(model: VideoLibraryModel): ToolSet;
32
32
  }
33
- /** Результат `show_video`: хранится в сообщении и целиком нужен карточке — даже если видео потом убрали из каталога. */
33
+ /**
34
+ * Результат `show_video`: хранится в сообщении и целиком нужен карточке — даже если видео потом убрали из каталога.
35
+ * Отказ — `VideoLibraryError`; `{ отказ }` бывает только в сообщениях, сохранённых до 0.56.
36
+ */
34
37
  export type ShownVideo = {
35
38
  видео: {
36
39
  id: string;
@@ -1,5 +1,7 @@
1
1
  export { createVideoLibrary } from "./create-video-library.js";
2
2
  export type { CreateVideoLibraryOptions, ShownVideo, VideoContext, VideoLibrary, VideoLibraryModel, VideoSource } from "./create-video-library.js";
3
+ export { VideoLibraryError } from "./video-library-error.js";
4
+ export type { VideoLibraryErrorKind } from "./video-library-error.js";
3
5
  export { searchVideos } from "./video-catalog.js";
4
6
  export type { VideoEntry } from "./video-catalog.js";
5
7
  export { embedUrl, isVideoAddress, parseVideoUrl, watchUrl } from "./video-address.js";
@@ -0,0 +1,9 @@
1
+ export type VideoLibraryErrorKind = "not_in_catalog" | "catalog_unavailable" | "start_after_end";
2
+ /**
3
+ * Отказ `show_video` и `find_videos`. Брошенная ошибка — неудача вызова: лента показывает её текст в строке инструмента, модель получает
4
+ * тот же текст. Поэтому в нём нет ссылок и текста исключений приложения.
5
+ */
6
+ export declare class VideoLibraryError extends Error {
7
+ readonly kind: VideoLibraryErrorKind;
8
+ constructor(kind: VideoLibraryErrorKind, message: string);
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykurilov/agent-chat-videos",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -39,14 +39,14 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^19.3.0",
42
- "@ykurilov/agent-chat-react": "^0.55.0"
42
+ "@ykurilov/agent-chat-react": "^0.56.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@testing-library/react": "^16.3.3",
46
46
  "@types/react": "^19.3.0",
47
47
  "jsdom": "^30.1.1",
48
48
  "react": "^19.3.0",
49
- "@ykurilov/agent-chat-react": "0.55.0"
49
+ "@ykurilov/agent-chat-react": "0.56.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "react": {