Как создать Android-приложение чат-бота с генеративным ИИ Google

В Google недавно представили платформу генеративного ИИ с большими языковыми моделями, в которых применяются методы AlphaGo. Gemini  —  это искусственный интеллект, которым из существующих источников генерируется новый контент или данные. Этими технологиями генерируются реалистичные и связные тексты, изображения, аудио, видео и другие мультимедиа с получением на выходе полностью синтетических, но правдоподобных результатов.

Генеративным ИИ Google используется глубокое обучение, то есть подмножество машинного обучения, с его способностью обрабатывать огромные объемы данных и обучаться на них. Эта передовая технология позволяет моделям ИИ делать обоснованные прогнозы и создавать новый контент, опираясь на обширные знания и закономерности, почерпнутые из данных, на которых эти модели обучены.

Одно из ключевых преимуществ этих базовых моделей  —  способность с минимальным обучением адаптироваться к конкретным вариантам применения, в которых для эффективной адаптации результатов требуется лишь небольшое количество примеров данных. Это делает генеративный ИИ универсальным и мощным инструментом для создания контента.

Поэтому в Gemini имеется множество способов совершенствования того или иного продукта. Создадим приложение чат-бота с ИИ для Android, использовав SDK-пакеты чата Compose от Stream и генеративного ИИ Google для Android.

Прежде чем открывать проект в Android Studio, клонируем на локальное устройство Android-репозиторий Gemini:

git clone https://github.com/skydoves/gemini-android.git

Перед созданием проекта настроим секретные свойства.

Настраиваем секретные свойства с API-ключами

В Gemini Android для безопасной настройки API применяется secrets-gradle-plugin, которым гарантируются безопасное управление конфиденциальной информацией, нераскрытие ее в публичных репозиториях.

Прежде чем настраивать API-ключи, создаем файл secrets.properties и включаем в него такие свойства:

STREAM_API_KEY=
GEMINI_API_KEY=

Теперь, чтобы создать на локальном компьютере Gemini Android, получаем API-ключи:

  1. Дашборда Stream  —  чтобы воспользоваться SDK-пакетом чата Stream в реальном времени. Учетная запись заводится через удобную регистрацию с помощью учетной записи GitHub. API-ключ Stream получаем, просто следуя этой инструкции.
  2. Google Cloud. Чтобы получить доступ к SDK-пакету генеративного ИИ Google, API-ключ Google Cloud приобретается в Google AI Studio. Получить API-ключ по учетной записи Google очень просто.

Подробные инструкции смотрите в разделе How to build the project («Как создать проект») в репозитории GitHub.

Теперь все готово к созданию проекта Gemini Android.

Создаем Gemini Android

Вот результат создания проекта:

По примеру Gemini Android создадим теперь свой проект. Рассмотрим ключевые особенности и аспекты реализации проекта Gemini Android, которые пригодятся при разработке собственного приложения.

Добавляем зависимости Gradle

Чтобы реализовать функционал чат-бота с ИИ, сначала добавим в файлы build.gradle.kts на уровне модулей и приложения такие зависимости:

// «build.gradle.kts» модуля приложения
dependencies {
implementation("io.getstream:stream-chat-android-compose:6.0.12") // SDK-пакет чата Compose от Stream
implementation("com.google.ai.client.generativeai:generativeai:0.1.2") // SDK-пакет генеративного ИИ
}

Stream SDK  —  это Open Source проект, так что весь исходный код, истории коммитов и релизы выложены на GitHub.

Примечание: ознакомиться с чатом Stream для Android можно также в руководстве.

Модульность

Gemini Android проектируется с применением многомодульной стратегии, благодаря этому совершенствуется процесс разработки приложений. У такого подхода имеется ряд преимуществ:

  1. Переиспользуемость. За счет эффективного разбиения кода на модули упрощается совместное его использование и ограничивается доступ к коду из других модулей. Так обеспечивается то, что общая функциональность легко доступна в различных частях приложения без дублирования.
  2. Параллельное создание. При такой стратегии каждый модуль собирается параллельно, общее время сборки значительно сокращается, а процесс разработки становится эффективнее, особенно в крупных проектах.
  3. Децентрализованный фокус командной работы. Такой подход позволяет различным командам разработчиков сконцентрироваться на закрепленных за ними модулях, работать более независимо и эффективно. В итоге повышается качество кода, ускоряются циклы разработки.

Базовая система Gemini Android состоит из таких модулей:

Модули в этой архитектуре подразделяются на функциональные и базовые, у каждого из них свой набор обязанностей. Функциональные модули мы подробно изучим после того, как рассмотрим один за другим базовые.

Базовые модули для логики предметной области

Базовые модули дальше делятся на две категории: модули предметной области и модули пользовательского интерфейса.

Сначала подробно рассмотрим базовые модули предметной области.

  • core-model: здесь содержатся объекты значений. Этой структурой представлены модели, полученные из сети или других базовых модулей.
  • core-database: в этот модуль включены база данных Room, сущности, а также компоненты объекта доступа к данным DAO. Модуль занимается сохранением данных из удаленных источников в локальную базу данных, обеспечивая эффективное управление данными и их выборку.
  • core-datastore: здесь хранятся пользовательские предпочтения и настройки. Этот модуль играет определенную роль в хранении и выборке конкретной информации о пользователе, например взаимодействовал ли он со всплывающим окном.
  • core-network: в этом модуле предоставляются комплексные сетевые решения для доступа к удаленным ресурсам и выборки их данных.
  • core-data: этот модуль занимается реализацией всей логики предметной области. Модель, база данных, хранилище данных и сетевые модули организуются им в структуре репозитория  —  едином источнике истины для логики предметной области приложения, обеспечивающем согласованность и надежность при обработке данных и манипулировании ими. Поэтому извлечение или запрашивание данных осуществляется уровнем представления с помощью модуля core-data при соответствии контракту интерфейса репозитория. Это в рамках архитектуры приложения четкий и структурированный способ доступа к данным.

Базовые модули для пользовательского интерфейса

Вот базовые модули пользовательского интерфейса.

  • core-navigation: здесь предлагаются решения по беспроблемной навигации с легкими, интуитивно понятными переходами между различными функциональными экранами. В модуле обеспечивается плавная, без каких-либо сложностей навигация между всеми компонентами.
  • core-designsystem: в этом независимом модуле объединены все типовые компоненты проектирования, применяемые различными расширенными модулями, например функциональными. Таким образом в приложении повышается переиспользуемость. С точки зрения дизайна пользовательского интерфейса этот подход, с централизацией всех элементов дизайна и обеспечением согласованности во всем приложении, фактически является единым источником истины.

Такова модульная структура проекта Gemini Android, в зависимости от конкретных задач и условий применяются различные ее стратегии.

От основ перейдем к настройке тем.

Тематическое оформление

В Jetpack Compose имеются органичные подходы для поддержания единообразного внешнего вида приложения с помощью предопределенных тем в качестве источника истины.

В SDK-пакете чата Compose от Stream имеется решение ChatTheme по тематическому оформлению и приведению к единому стилю всех компонентов пользовательского интерфейса, обеспечивающее единообразный, согласованный внешний вид компонентов Stream Compose.

ChatTheme применяется в проекте Gemini Android для настройки внешнего вида компонентов пользовательского интерфейса Stream, как в этом примере:

@Composable
fun GeminiComposeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val streamColors = if (darkTheme) {
StreamColors.defaultDarkColors().copy(
appBackground = BACKGROUND900,
primaryAccent = STREAM_PRIMARY,
ownMessagesBackground = STREAM_PRIMARY
)
} else {
StreamColors.defaultColors().copy(
primaryAccent = STREAM_PRIMARY,
ownMessagesBackground = STREAM_PRIMARY_LIGHT
)
}

ChatTheme(
colors = streamColors,
reactionIconFactory = GeminiReactionFactory(),
content = content
)
}

Таким образом унифицированные стили применяются во всех компонентах Stream Compose в Composable GeminiComposeTheme:

GeminiComposeTheme {

ChannelsScreen()

..
}

Реализуем функционал списка каналов

В проекте Gemini Android функционал канала  —  это начальный экран, который видят пользователи при запуске приложения. С SDK-пакетом чата Stream для Compose упрощается реализация списка каналов, как это показано компонентом пользовательского интерфейса Compose Channelscreen:

ChannelsScreen(
isShowingHeader = false,
onItemClick = { channel ->
composeNavigator.navigate(
GeminiScreens.Messages.createRoute(
channelId = channel.cid,
)
)
}
)

Благодаря компоненту ChannelsScreen настраиваются различные элементы, такие как название, заголовок, функция поиска, а также добавляются прослушиватели к этим компонентам. Кроме того, лямбда-параметром onItemClick упрощается переход к экрану списка сообщений.

Присоединение к каналам по умолчанию

При создании проекта экран списка каналов будет пуст: к ним еще нужно присоединиться. Поэтому следующий этап  —  создание каналов и присоединение к ним, специально для чатов с ИИ.

Сначала классом ChannelService из сети через Gist извлекаются предопределенные модели каналов Gemini:

interface ChannelService {

@GET("GeminiModel.json")
suspend fun geminiChannels(): ApiResponse<List<GeminiChannel>>
}

Сетевой ответ инкапсулируется в ApiResponse. Это часть библиотеки Sandwich в Kotlin для упрощения обработки сетевых ответов при разработке Android.

Преимущества моделирования сетевых ответов разобраны в разделе Modeling Retrofit Responses With Sealed Classes and Coroutines («Моделирование модифицированных ответов запечатанными классами и корутинами»).

Перейдя к классу ChannelRepositoryImpl, обнаруживаем функцию joinDefaultChannels:

internal class ChannelRepositoryImpl @Inject constructor(
private val chatClient: ChatClient,
private val service: ChannelService,
private val geminiDao: GeminiDao,
) : ChannelRepository {

override suspend fun joinDefaultChannels(user: User): ApiResponse<List<GeminiChannel>> {
val response = service.geminiChannels()
.suspendOnSuccess {
data.forEach { geminiChannel ->
val channelClient = chatClient.channel(geminiChannel.id)
channelClient.create(
memberIds = listOf(geminiUser.id, user.id),
extraData = mapOf(
"name" to geminiChannel.name,
"image" to "https://avatars.githubusercontent.com/u/8597527?s=200&v=4.png",
)
).await().onSuccessSuspend {
geminiDao.insertGeminiChannel(geminiChannel.toEntity())
}
}
}
return response
}

В этом фрагменте кода функцией joinDefaultChannels вызовом service.geminiChannels() из сети извлекаются предопределенные модели каналов Gemini. После этого создаются каналы чата Stream: перебирается список экземпляров каналов Gemini, и каждый канал генерируется функцией chatClient.create. Подробнее о создании каналов чата Stream  —  в документации.

Кроме того, этой функцией в базу данных также вставляются элементы канала Gemini. Этот этап важен для будущего использования, особенно в экране сообщений: так обеспечивается, что соответствующие данные при необходимости всегда доступны.

Остался класс ChannelViewModel, это уровень представления для управления состояниями и событиями пользовательского интерфейса. Вот как им осуществляется присоединение к предопределенным каналам Stream, настроенным в ChannelRepositoryImpl:

@HiltViewModel
class ChannelViewModel @Inject constructor(
private val repository: ChannelRepository
) : ViewModel() {

private val userFlow = repository.streamUserFlow()

private val channelEvent: MutableSharedFlow<ChannelEvent> = publishedFlow()
internal val channelUiState: SharedFlow<ChannelUiState> =
combine(channelEvent, userFlow) { event, user ->
event to user
}.flatMapLatest { pair ->
val (event, user) = pair
when (event) {
is ChannelEvent.JoinDefaultChannels -> {
val response = repository.joinDefaultChannels(user = user)
if (response.isSuccess) {
flowOf(ChannelUiState.JoinSuccess)
} else {
flowOf(ChannelUiState.Error(response.messageOrNull))
}
}

..
}.asStateFlow(ChannelUiState.Idle)
}

Поскольку Gemini Android создается с помощью Jetpack Compose, в общей его архитектуре используются преимущества Compose: однонаправленный поток событий и данных.

Вот как этот подход обработки событий и данных согласуется с принципами Compose для оптимизированной и эффективной структуры приложения:

После получения событий от компонентов пользовательского интерфейса во ViewModel через репозитории запускается логика предметной области. Данные, полученные в этих взаимодействиях, затем наблюдаются в виде наблюдаемого потока. Далее этот поток данных передается обратно компонентам пользовательского интерфейса в виде состояния пользовательского интерфейса.

Завершив все описанные выше процессы, увидим экран каналов с предопределенными каналами:

Реализуем функционал чата с ИИ

Создадим функционал чата с ИИ. SDK-пакетом Gemini Android предоставляется доступ к моделям Gemini Pro, размещенным на серверах Google, и на основе входных данных: текста или изображений пользователей  —  выдаются текстовые ответы.

Фактически имеется два типа моделей.

  • gemini-pro: идеальная для сценариев с вводом исключительно текстовых подсказок. Ответы генерируются с помощью suspend-функции GenerativeModel.generateContent.
  • gemini-pro-vision: мультимодальная модель, которой в качестве входных данных принимаются текст и изображения. Когда подсказка  —  это сочетание текста и изображений, ответы генерируются suspend-функцией GenerativeModel.generateContent.

В принципе, эти две большие языковые модели аналогичны продвинутым инструментам автозаполнения. Если разрабатывать такой функционал этими моделями, навыки и опыт в области машинного обучения не понадобятся, значительно сократятся затраты на приобретением ML-знаний и наем ML-специалистов для конкретных сценариев. Подробнее об этом  —  в руководстве по концепциям больших языковых моделей Google.

Теперь создадим GenerativeModel, компонент для генерирования ответов на основе генеративного ИИ Google с заданными подсказками ввода.

Создаем «GenerativeModel»

GenerativeModel инициализируется указанием параметров modelName и apiKey:

val generativeModel = GenerativeModel(
modelName = "gemini-pro",
apiKey = BuildConfig.GEMINI_API_KEY,
)

А процесс генерации контента настраивается указанием параметра generationConfig:

val generativeModel = GenerativeModel(
..
generationConfig = generationConfig {
this.temperature = 0.75f
this.candidateCount = 1
this.topK = 30
this.topP = 0.5f
this.maxOutputTokens = 300
}
)

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

Настраиваются такие параметры конфигурации:

  • Максимальное количество токенов выходных данных. Токен приблизительно равен четырем символам. Этим параметром определяется верхний предел количества токенов, генерируемых ответом. Например, при ограничении в 100 токенов выдается примерно 60–80 слов.
  • Температура. Этот параметр сказывается на случайности выбора токена. Температура пониже подходит для подсказок, требующих более детерминированных или конкретных ответов. Температура повыше, напротив, способствует более вариативным или недетерминированным результатам.
  • TopK. Параметр topK, равный 1, означает: для следующего токена моделью из ее словаря выбирается наиболее вероятный токен, это называется жадным декодированием. A уже topK, равный 3, позволяет модели выбрать следующий токен из трех наиболее вероятных вариантов, исходя из параметра температуры.
  • TopP. Этот параметр позволяет выбирать токен, начиная с наиболее вероятного и продолжая до тех пор, пока сумма вероятностей не достигает порогового значения topP. Например, если токены A, B и C имеют вероятности 0,3, 0,2 и 0,1 соответственно, а значение topP равно 0,5, моделью с помощью параметра температуры для следующего токена выбирается A или B. C из рассмотрения исключается.
  • Количество кандидатов. Этим параметром указывается максимальное число уникальных генерируемых ответов. Количество кандидатов, равное 2, означает: моделью предоставляется два различных варианта ответа.

Чтобы адаптировать генеративную модель под конкретные задачи, значения каждого параметра гибко настраиваются. Подробнее о больших языковых моделях и их настраиваемых параметрах  —  в руководстве по концепциям больших языковых моделей.

Создаем экран Composable «GeminiChat»

Прежде чем генерировать ответы Gemini, настроим интерфейс чата. В Stream SDK имеется удобный компонент MessagesScreen с разными преднастроенными компонентами: верхней панелью, списком сообщений, полем ввода и другими. Это элементы для легкой настройки под задачи приложения.

Мы же создали GeminiChat с MessageList, MessageComposer и MessageListHeader для настройки конкретной функциональности, соответствующей требованиям модели генеративного ИИ. Чтобы понять, как именно выполнена такая настройка, рекомендуем изучить компонент GeminiChat на GitHub.

Чат с Gemini посредством текстовых сообщений

Теперь сгенерируем ответы с помощью генеративного ИИ. Сначала из данного GenerativeModel создадим экземпляр Chat:

val generativeModel = GenerativeModel(..)
val chat = generativeModel.startChat()

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

Создав экземпляр Chat, отправляем с помощью suspend-функции generativeChat.sendMessage() сообщение:

@HiltViewModel
class ChatViewModel @Inject constructor(
repository: ChatRepository,
chatClient: ChatClient,
) : ViewModel() {

private suspend fun sendTextMessage(text: String): String? {
val response = generativeChat.sendMessage(text)
val responseText = response.text
if (responseText != null) {
channelClient.sendMessage(
message = Message(
id = UUID.randomUUID().toString(),
cid = channelClient.cid,
text = responseText,
extraData = mutableMapOf(STREAM_CHANNEL_GEMINI_FLAG to true)
)
).await()
}
return responseText
}
..
}

Функцией sendMessage возвращается GenerateContentResponse с различными деталями сгенерированного ответа. Доступ к основному телу ответа получается через свойство text. Сообщение, полученное из извлеченного ответа, перенаправляется на канал Stream, чем обеспечивается соответствующая синхронизация ответа.

Интегрировав функцию с состояниями пользовательского интерфейса и выполнив проект, получаем результат:

Подробнее о процессе генерирования текста  —  в функции ChatViewModel.sendTextMessage.

Формирование рассуждений с Gemini по растровым изображениям

Реализуем формирование рассуждений по фото, то есть способность системы ИИ анализировать и интерпретировать визуальные данные изображений с полным пониманием содержимого изображения и контекста, включая выявление закономерностей, объектов, лиц, сцен и, возможно, эмоций или действий, запечатленных на фото.

Формирование рассуждений по фото осуществляется в различных сферах: компьютерном зрении, обработке изображений, машинном обучении. Поддерживается широкий спектр применения: классификация изображений, обнаружение объектов, распознавание лиц, автоматическое тегирование фото.

Технологией генеративного ИИ Google предлагаются возможности, облегчающие формирование рассуждений по фото. Доступ к этой функциональности получается посредством функции GenerativeModel.generateContent:

val content = content {
image(bitmap)
text(prompt)
}
val response = generativeModel.generateContent(content)

В Gemini Android рассуждения формируются по нескольким изображениям, для совершенствования анализа подсказки корректируются:

private suspend fun photoReasoning(message: Message, bitmaps: List<Bitmap>): String? {
val text = message.text
val prompt = "Look at the image(s), and then answer the following question: $text"
val content = content {
for (bitmap in bitmaps) {
image(bitmap)
}
text(prompt)
}
val response = generativeModel.generateContent(content)
val responseText = response.text
if (responseText != null) {
channelClient.sendMessage(
message = Message(
id = UUID.randomUUID().toString(),
cid = channelClient.cid,
text = responseText,
extraData = mutableMapOf(STREAM_CHANNEL_GEMINI_FLAG to true)
)
).await()
}
return responseText
}

Функцией photoReasoning создается экземпляр Content, содержащий тексты и изображения для последующего вопроса, и генерируется ответ с заданным экземпляром Content. Реализовав эту функцию и объединив с элементами пользовательского интерфейса, вы увидите результат:

Подробнее о процессе формирования рассуждений по фото  —  в функции ChatViewModel.photoReasoning.

Заключение

Мы изучили комплексную архитектуру Gemini Android, принципы генеративного ИИ Google, SDK-пакет Gemini и реализацию систем чат-ботов ИИ с возможностями генерирования текста и анализа фото. Генеративный ИИ применяется в самых разных направлениях и способен значительно усовершенствовать пользовательское взаимодействие в приложении.

Новые идеи и примеры доступны в репозиториях GitHub:

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Jaewoong Eum: Build an AI Chat Android App With Google’s Generative AI

Предыдущая статьяСоздание приложения-чата с LangChain, большими языковыми моделями и Streamlit для взаимодействия со сложной базой данных SQL. Часть 2
Следующая статьяТоп-10 антипаттернов при использовании микросервисов