В последнее время появляется все больше инструментов и приложений, которые решают различные задачи при помощи ИИ. Некоторые из них, можно было решить и раньше, но не так качественно (различные решения для извлечения данных из документов), а некоторые, вероятно, можно решить только сейчас. Один из таких кейсов был и у меня - я хотел написать приложение, чтобы было удобно делить чек по фотографии.

В принципе, задачу можно решить и без ИИ - для распознавания текста уже есть OCR-технологии, дальше, по-сути, самый обычный CRUD. Но, после первых экспериментов я чуть было не оставил эту задачу насовсем - OCR хоть и работает, но качество распознавания оставляет желать лучшего. Чтобы улучшить качество, нужно применять фильтры, чтобы текст был максимально контрастным. Да и в целом - качество фотографии влияет очень сильно, и это я еще не дошел до фильтрации текста (в чеке, очевидно, не только список товаров).

Тогда я и решил попробовать для распознавания текста LLM, тем более, что этим подходом можно “убить двух зайцев” - с фильтрацией современные модели точно справятся. В принципе, решение уже было рабочее, но делиться я им не спешил - теперь нужно было платить не только за виртуальный сервер, но и за запросы к API. Тогда я и начал искать решение - хотелось бы иметь полностью статичный клиент и вообще обойтись без сервера, но тогда за распознавание должен отвечать пользователь. Тут решение тоже есть - у нас уже есть стандарт для описания навыков для ИИ-агентов, т.е. задачу можно описать в виде инструкции и отдать агенту, а на клиенте оставить только взаимодействие с пользователем.

Как мне показалось, вариант отличный, но приложение должно работать в первую очередь на телефоне, а как там с запуском ИИ-агентов? На самом деле, такая возможность есть, причем модель в этом случае работает локально - нужно приложение Edge Gallery от Google. В приложении выбираем модель для работы с агентскими сценариями и после загрузки можно начинать пользоваться. Сейчас доступны несколько предустановленных навыков, но мне было интересно попробовать написать свой - для своей задачи.

Пишем навык для приложения

В первую очередь я решил разобраться с документацией - там уже описан небольшой пример, как писать свои навыки. Навыки могут быть простыми, т.е. текстовыми или с поддержкой JS, в таком случае у разработчика есть возможность работать с окружением, доступным внутри WebView и описывать методы, которые будут доступны агенту. И это ровно то, что нужно - я хотел написать приложение вообще без сервера, так что мне нужен какой-то способ передавать результат распознавания из ответа модели в приложение, и это все-таки лучше доверить какому-нибудь алгоритму.

Структура навыка выглядит так:

my-js-skill/
├── SKILL.md
└── scripts/
    └── index.html

На самом деле не важно, с чего начинать, но нужно учесть, что агенту нужно явно указать - ему нужно вызвать инструмент run_js:

---
name: split-bill-rzdeli
description: Split the bill using the rzdeli.ru service
---

# Split bill from image (rzdeli.ru)

## Instructions

Call the `run_js` tool with the following exact parameters:
- script name: index.html
- data: A JSON string with the items from the check with format `Array<{ title: string, price: number, count: number }>` 
- If there are no items in the check, pass an empty array. 
- The item names can be multiple lines long. 
- The price field can be named "Сумма".

Тут можно заметить, что я явно указал формат данных, в котором модель должна вызывать JS-метод, и некоторые другие инструкции, которые я вывел, пока тестировал распознавание на локальной модели.

А теперь можно описать и сам метод - его нужно явно в виде асинхронного метода под именем ai_edge_gallery_get_result, именно такое имя ожидает инструмент run_js:

<!doctype html>
<html lang="en">
  <head></head>

  <body>
    <script>
      window['ai_edge_gallery_get_result'] = async (data) => {
        try {
          const url = new URL('https://rzdeli.ru/list')

          return JSON.stringify({
            result: 'Here is the interactive view.',
            webview: {
              url: url,
              aspectRatio: 1.0,
            },
          })
        } catch (e) {
          return JSON.stringify({
            error: `Failed: ${e.message}`,
          })
        }
      }
    </script>
  </body>
</html>

Тут так же важно выделить формат ответа - от него будет зависеть, как ответ отобразится для пользователя.

interface Result {
  error?: string // если в результате работы есть ошибка, достаточно вернуть только это поле с текстом ошибки
  result: string // в случае успеха возвращается текстовый ответ
  webview?: { // нужно для интерактивного превью - в чате будет небольшое окно с возможностью открыть страницу на полный экран
    url: string, // можно указать как относительный путь, так и сторонний сервис
    aspectRatio: number // по умолчанию 1.0
  }
  image?: { // очевидно, для отрисовки изображения в чате
    base64: string // строка в формате base64
  }
}

Сейчас уже можно протестировать навык, нужно лишь добавить его в приложение. Для импорта у нас есть три варианта:

  • Импорт из списка community-навыков. Вероятно, ревью проводит Google и просто так туда не попасть.
  • Импорт из исходников - можно просто загрузить все нужные файлы на устройство и импортировать навык из файловой системы. Можно пользоваться, но неудобно отлаживать.
  • Импорт по URL - самый простой вариант, но нужно учесть, что приложение не может загрузить навык напрямую из Github и требует, чтобы навык хостился на настоящем сервере.

Проблему с хостингом очень легко решить - весь репозиторий статичный и достаточно загрузить это все на Github pages, после этого приложение успешно загрузит навык.

Дополнительно отмечу, что в документации упоминается раздел “Native Skills”, который позволяет вызывать нативные API, однако в этом случае разработчику предполагается самостоятельно расширять доступный API.

Пишем клиентское приложение

На этом моменте у нас почти все готово - модель умеет распознавать текст из чека и умеет передавать его в JS-скрипт, осталось только отобразить это все пользователю.

Я решил не усложнять и написал приложение на React и shadcn, а список товаров получать из query-параметра. Для передачи списка позиций в чеке я выбрал base64, но он не умеет работать с UTF-8, поэтому пришлось применить пару хаков на кодировании и декодировании, но главное, что все работает.

Теперь нужно лищь добавить в навык метод для формирования корректной base64 строки и возвращать пользователю полную ссылку:

function utf8_to_b64(str) {
  return window.btoa(unescape(encodeURIComponent(str)));
}
Скриншот из чата с вызовом навыка

Выводы

Да, скорее всего, не получится решить любую задачу подобным образом - вынести вычисления на устройство пользователя и предоставить только полностью статичный клиент, но, скорее всего, задач, которые можно решить подобным образом, не мало. Нужно лишь учитывать чувствительность данных и мощности локальных моделей, которые сейчас не очень высокие, но, тем не менее позволяют решать и такие задачи.

Стоит отметить отдельно, что такие решения не предполагают создание приложения, которое можно продать пользователю - это всего лишь один из вариантов решения моей (или вашей) проблемы и в этом случае важно сделать решение максимально дешевым. Надеюсь, это небольшое руководство поможет решить и несколько ваших задач.