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

Сегодня мы:

  • Научимся устанавливать LLama3 на локальную машину.

  • Научимся бесплатно запускать LLama3 через платформу GROQ.

  • Разберемся с преимуществами и недостатками первого и второго способа развертывания LLama3.

  • Напишем полноценного Telegram бота с использованием aiogram3, который сможет работать как с локальной версией LLAMA3, так и через сервис GROQ (технически он сможет работать с любой подключенной нейросетью).

  • Запустим Telegram бота на VPS сервере (опционально).

Подготовка

Для того чтобы следовать этому руководству, вам потребуется:

  • VPS сервер (важно, чтобы с европейским или США API).

  • База данных PostgreSQL (о том, как запустить её за пару минут, я писал [ТУТ]).

  • Базовые знания по написанию ботов через aiogram3 (в моём профиле на Хабре вы найдёте множество публикаций, в которых подробно рассмотрена тема создания Telegram ботов).

  • Установленный Docker на локальной машине и на VPS сервере (если вы новичок, установите Docker Desktop).

Надеюсь, что подготовку вы уже выполнили.

Развертывание локальной версии нейросети LLAMA с использованием Docker

Откройте консоль и выполните следующую команду:

docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

Эта команда развернет локальный образ LLAMA, который будет работать исключительно на вашем процессоре. Также существует вариант использования Nvidia GPU, с инструкциями можно ознакомиться [здесь].

Для запуска самой модели выполните команду:

docker exec -it ollama ollama run llama3:8b

Эта команда загрузит и запустит языковую модель LLAMA3:8b (4.7GB). Доступна также более крупная версия LLama3, 70b (40GB). Вы можете запускать и другие модели, список которых доступен [здесь].

Чтобы запустить другую модель, используйте команду:

docker exec -it ollama ollama run model_name:tag
Запустил, пошла загрузка и можно вести диалог.
Запустил, пошла загрузка и можно вести диалог.

Для выхода из диалога с LLM отправьте команду /bye, затем последовательно выполните CTRL+P и CTRL+Q, чтобы выйти из интерактивного режима.

Обратите внимание. Так вы просто свернете контейнер, но он все равно будет запущен. Если вы хотите удалить контейнер, то воспользуйтесь командами:

docker stop ollama
docker rm ollama
docker rmi ollama/ollama

Обратите внимание. После остановки (удаления) контейнера ollama локальное взаимодействие с ним будет невозможным!

Важно: перед использованием локальных ИИ выполните загрузку самой модели. Самый простой способ – запустить диалог с моделью, как описано выше, так вы и загрузку выполните, и проверите, что всё корректно работает.

Использование GROQ для взаимодействия с LLAMA3

Альтернативой локальному запуску LLAMA3 является использование сервиса GROQ. Для этого:

  1. Зайдите в консоль GROQ.

  2. Зарегистрируйтесь и выполните авторизацию.

  3. Перейдите в раздел KEYS.

  4. Нажмите на «Create API Key».

  5. Скопируйте созданный ключ и сохраните его в надёжном месте.

Этих простых действий будет достаточно для дальнейшего использования GROQ в качестве посредника между вашим Python проектом и LLama3.

Преимущества и недостатки использования LLama3 в локальном виде или через GROQ

Локальный запуск LLAMA3

Преимущества:

  1. Контроль над данными и конфиденциальность:

    • Полный контроль над данными, так как они не передаются через сторонние серверы.

  2. Настраиваемость:

    • Возможность полной настройки системы и оптимизации под специфические задачи.

  3. Единовременные затраты:

    • Высокие начальные затраты на оборудование, но отсутствие постоянных расходов на аренду облачных ресурсов.

  4. Отсутствие зависимости от интернета:

    • Модель может работать автономно.

Недостатки:

  1. Высокие начальные затраты:

    • Значительные затраты на покупку и установку оборудования.

  2. Техническое обслуживание:

    • Необходимость регулярного обслуживания и обновления оборудования.

  3. Ограниченная масштабируемость:

    • Масштабирование требует физического обновления или добавления новых машин.

Использование платформы GROQ

Преимущества:

  1. Масштабируемость:

    • Легкость масштабирования вычислительных ресурсов без необходимости физического обновления оборудования.

  2. Обновления и поддержка:

    • Доступ к последним обновлениям и поддержке от команды GROQ.

  3. Экономия времени:

    • Быстрое развертывание и настройка.

  4. Гибкость в оплате:

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

Недостатки:

  1. Возможные расходы:

    • В будущем платформа станет платной.

  2. Зависимость от интернета:

    • Требуется постоянное интернет-соединение.

  3. Меньший контроль над данными:

    • Данные передаются через сторонние серверы.

  4. Ограниченния платформы (текущие и будущие):

    • Сама платформа уже сейчас устанавливает лимиты и ограничения. К примеру есть лимиты по запросам к модели (скрин ниже).

    • Ещё пример - на данный момент нельзя пользоваться данной платформой с РФ IP адресов. При чем, вы не только не сможете без VPN зайти на сайт, но ещё и VPN должен стоять на вашем компьютере (сервере, если IP РФ), если вы в РФ и если вы будете использовать GROQ в своих проектах.

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

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

Теперь давайте рассмотрим программное использование LLama3 с локальным запуском и запуском через платформу GROQ.

Я подготовил две демо-версии, изучив которые, вы поймёте, как происходит программное взаимодействие с LLAMA3 через Python.

Демо локального использования

from openai import OpenAI

client = OpenAI(
    base_url='http://localhost:11434/v1',
    api_key='ollama',
)

dialog_history = []

while True:
    user_input = input("Введите ваше сообщение ('stop' для завершения): ")

    if user_input.lower() == "stop":
        break

    # Добавляем сообщение пользователя в историю диалога
    dialog_history.append({
        "role": "user",
        "content": user_input,
    })

    response = client.chat.completions.create(
        model="llama3:8b",
        messages=dialog_history,
    )

    # Извлекаем содержимое ответа
    response_content = response.choices[0].message.content
    print("Ответ модели:", response_content)

    # Добавляем ответ модели в историю диалога
    dialog_history.append({
        "role": "assistant",
        "content": response_content,
    })

Давайте разберемся с этим кодом.

Импорт и настройка клиента

from openai import OpenAI

client = OpenAI(
    base_url='http://localhost:11434/v1',
    api_key='ollama',
)

Здесь мы импортируем библиотеку OpenAI и создаем экземпляр клиента OpenAI, указывая базовый URL для API (локальный сервер на порту 11434) и API-ключ (в данном случае 'ollama').

Такую возможность мы получаем после того как локально развернули Docker контейнер Ollama. Далее, выполняя запросы о которых поговорим далее, мы имитируем диалог с ботом через консоль (описывал выше).

Инициализация истории диалога

dialog_history = []

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

Основной цикл для взаимодействия с пользователем

while True:
    user_input = input("Введите ваше сообщение ('stop' для завершения): ")

    if user_input.lower() == "stop":
        break

Запускаем бесконечный цикл, который будет принимать ввод от пользователя. Если пользователь введет 'stop', цикл прерывается.

Добавление сообщения пользователя в историю диалога

dialog_history.append({
        "role": "user",
        "content": user_input,
    })

Сообщение пользователя добавляется в dialog_history в виде словаря с ключами role (роль 'user') и content (содержимое сообщения).

Отправка запроса модели и получение ответа

response = client.chat.completions.create(
        model="llama3:8b",
        messages=dialog_history,
    )

Отправляется запрос к модели llama3:8b (если разворачивали контейнер с Llama3, то на вашем компьютере уже установлена модель llama3:8b, иначе загрузите модель отдельно) с текущей историей диалога. Метод client.chat.completions.create создает завершение чата на основе переданных сообщений.

Обработка ответа модели

response_content = response.choices[0].message.content
print("Ответ модели:", response_content)

Ответ модели извлекается из объекта response и выводится на экран.

Добавление ответа модели в историю диалога

dialog_history.append({
        "role": "assistant",
        "content": response_content,
    })

Ответ модели добавляется в dialog_history как сообщение от ассистента.

Демо общения с локальной версией LLama3
Демо общения с локальной версией LLama3

Обратите внимание. В данном примере я использовал модуль openai, а это значит, что данный клиент будет работать в любом боте, который использовал нейронки ChatGPT, Bing и так далее (сейчас GitHub завален проектами, использующими библиотеку openai).

Демо клиента через GROQ

from groq import Groq
from decouple import config

client = Groq(
    api_key=config("GROQ_API_KEY"),
)

dialog_history = []

while True:
    user_input = input("Введите ваше сообщение ('stop' для завершения): ")

    if user_input.lower() == "stop":
        break

    # Добавляем сообщение пользователя в историю диалога
    dialog_history.append({
        "role": "user",
        "content": user_input,
    })

    models = ["gemma-7b-it", "llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768"]
    chat_completion = client.chat.completions.create(
        messages=dialog_history,
        model=models[1],D
    )

    response = chat_completion.choices[0].message.content
    print("Ответ модели:", response)

    # Добавляем ответ модели в историю диалога
    dialog_history.append({
        "role": "assistant",
        "content": response,
    })

Особо внимательные могут заметить, что синтаксис модуля GROQ не особо отличается от синтаксиса OPENAI и это совсем не случайно. Ведь, если мы посмотрим под капот модуля groq, то увидим что основывается он на openai.

А это, как вы поняли, нам на руку.

Импорт и настрйка клиента

from groq import Groq
from decouple import config

client = Groq(
    api_key=config("GROQ_API_KEY"),
)

Я использовал модуль python-decouple, чтоб импортировать из файла .env GROQ_API_KEY.

Доступные модели:

models = ["gemma-7b-it", "llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768"]

На данный момент GROQ поддерживает представленные выше модели. В коде я взял за основу "llama3-70b-8192". Это там самая модель, которая весит более 40GB и самое приятное тут то, что скачивать модель нам не нужно, а достаточно получить у GROQ api key и установить модуль groq.

В остальном отличий от работы со своей локальной моделью через openai нет.

Надеюсь, что данные примеры вам понятны. Переходим к боту

Создаем бота

Сегодня мы создадим бота-ассистента, который будет иметь следующие основные функции:

  1. Добавление пользователя в базу данных по клику на "Старт" и присваивание ему статуса "Не в диалоге" / "В диалоге"

  2. Запуск диалога по клику на кнопку «Начать диалог».

  3. Полное удаление истории общения по клику на кнопку «Очистить историю».

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

Логика сохранения истории

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

  • id: уникальный идентификатор диалога (автоинкрементируемый номер).

  • user_id: Telegram ID пользователя.

  • message: JSON с сообщением бота или пользователя.

Такой подход позволит нам сохранять историю диалога для каждого пользователя по его Telegram ID, а также обеспечить возможность очистки истории.

Хранение информации о пользователе

Для хранения информации о пользователе создадим таблицу users. В этой таблице будут следующие поля:

  • user_id: уникальный идентификатор пользователя в Telegram.

  • user_login: имя пользователя в Telegram.

  • full_name: полное имя пользователя.

  • in_dialog: текущее состояние пользователя («В диалоге» (True) или «Не в диалоге» (False)).

  • date_reg: дата и время регистрации пользователя.

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

Подготовка перед написанием кода

Начну с того, что весь код бота можно найти в моем публичном репозитории Easy_LLama3_Bot. Там вы найдете бота, демки для работы с локальной и GROQ версией Llama3 и пример настройки Dockerfile для быстрого запуска на VPS сервере.

Зависимости (requirementsl.txt)

asyncpg-lite~=0.3.1.3
aiogram~=3.7.0
python-decouple
groq
pytz
openai
  • asyncpg-lite - библиотека для асинхронной работы с PostgreSQL (делал подробное описание Asynpg-lite: лёгкость асинхронных операций на PostgreSQL с SQLAlchemy)

  • aiogram3 - фреймворк для создания телеграмм ботов через Python (в моём профиле на Хабре вы найдёте множество публикаций, в которых подробно рассмотрена тема создания Telegram ботов на aiogram3)

  • python-decouple - модуль для удобной работы с .env

  • groq и openai описывал выше. Выберите для своего бота один из вариантов.

  • pytz - простой модуль для работы с часовыми поясами

ENV-файл (.env)

GROQ_API_KEY=your_groq_token
BOT_API_KEY=your_bot_token
ADMINS=admin1,admin2
ROOT_PASS=your_root_password
PG_LINK=postgresql://username:password@host:port/dbname

Замените данные на свои. Предварительно не забудьте узнать свой телеграмм ID, развернуть базу данных и создать токен бота и токен GROQ_API. О том как создавать токен телеграмм бота через BotFather вы можете узнать тут или, в целом, на просторах интернета.

Структура проекта

- db_handler
    - __init__.py: Инициализация модуля.
    - db_funk.py: Функции для взаимодействия с PostgreSQL.

- handlers
    - __init__.py: Инициализация модуля.
    - user_router.py: Основной и единственный роутер в котором весь код

- keyboards
    - __init__.py: Инициализация модуля.
    - kbs.py: Файл со всеми клавиатурами.

- utils
    - __init__.py: Инициализация модуля.
    - utils.py: Файл с утилитами.

- .env
- .dockerignorefile
- Dockerfile
- Makefile: файл для удобного запуска и управления контейнерами
- .gitignorefile
- aiogram_run.py: файл для запуска бота
- create_bot.py: файл с настройками бота
- llama_groq_demo.py: демка LLama3с работой через GROQ
- llama_localhost_demo.py: демка LLama3с с локальным запуском
- README.md: Короткое описание проекта с GitHub

Файл с настройками бота (create_bot.py):

import logging
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from asyncpg_lite import DatabaseManager
from decouple import config
from groq import Groq
from openai import OpenAI

client_groq = Groq(api_key=config("GROQ_API_KEY"))
local_client = OpenAI(base_url='http://localhost:11434/v1', api_key='ollama')

# получаем список администраторов из .env
admins = [int(admin_id) for admin_id in config('ADMINS').split(',')]

# инициализируем логирование и выводим в переменную для отдельного использования в нужных местах
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# инициализируем объект, который будет отвечать за взаимодействие с базой данных
db_manager = DatabaseManager(db_url=config('PG_LINK'), deletion_password=config('ROOT_PASS'))

# инициализируем объект бота, передавая ему parse_mode=ParseMode.HTML по умолчанию
bot = Bot(token=config('BOT_API_KEY'), default=DefaultBotProperties(parse_mode=ParseMode.HTML))

# инициализируем объект бота
dp = Dispatcher()

Если вы читали мои статьи по Aiogram3 то вам должно быть все понятно. Единственное на что хочу обратить внимание - это настройка клиентов для работы с нейронками.

Вам необходимо выбрать один вариант или GROQ или локальную версию. В коде вывел два для демонстрации.

Файл для запуска бота (aiogram_run.py):

import asyncio
from create_bot import bot, dp, admins
from db_handler.db_funk import get_all_users
from handlers.user_router import user_router
from aiogram.types import BotCommand, BotCommandScopeDefault


# Функция, которая настроит командное меню (дефолтное для всех пользователей)
async def set_commands():
    commands = [BotCommand(command='start', description='Старт'),
                BotCommand(command='restart', description='Очистить диалог')]
    await bot.set_my_commands(commands, BotCommandScopeDefault())


# Функция, которая выполнится когда бот запустится
async def start_bot():
    await set_commands()
    count_users = await get_all_users(count=True)
    try:
        for admin_id in admins:
            await bot.send_message(admin_id, f'Я запущен?. Сейчас в базе данных <b>{count_users}</b> пользователей.')
    except:
        pass


# Функция, которая выполнится когда бот завершит свою работу
async def stop_bot():
    try:
        for admin_id in admins:
            await bot.send_message(admin_id, 'Бот остановлен. За что??')
    except:
        pass


async def main():
    # регистрация роутеров
    dp.include_router(user_router)

    # регистрация функций
    dp.startup.register(start_bot)
    dp.shutdown.register(stop_bot)

    # запуск бота в режиме long polling при запуске бот очищает все обновления, которые были за его моменты бездействия
    try:
        await bot.delete_webhook(drop_pending_updates=True)
        await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
    finally:
        await bot.session.close()


if __name__ == "__main__":
    asyncio.run(main())

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

Если коротко, то тут мы описали функции, которые выполняются при запуске бота и при завершении работы.

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

Напишем хендлер для работы с базой данных (db_handler/db_funk.py):

import json
from sqlalchemy import Integer, String, BigInteger, TIMESTAMP, JSON, Boolean
from create_bot import db_manager
import asyncio


# функция, которая создаст таблицу с пользователями
async def create_table_users(table_name='users'):
    async with db_manager as client:
        columns = [
            {"name": "user_id", "type": BigInteger, "options": {"primary_key": True, "autoincrement": False}},
            {"name": "full_name", "type": String},
            {"name": "user_login", "type": String},
            {"name": "in_dialog", "type": Boolean},
            {"name": "date_reg", "type": TIMESTAMP},
        ]
        await client.create_table(table_name=table_name, columns=columns)


async def create_table_dialog_history(table_name='dialog_history'):
    async with db_manager as client:
        columns = [
            {"name": "id", "type": Integer, "options": {"primary_key": True, "autoincrement": True}},
            {"name": "user_id", "type": BigInteger},
            {"name": "message", "type": JSON}
        ]
        await client.create_table(table_name=table_name, columns=columns)


# функция, для получения информации по конкретному пользователю
async def get_user_data(user_id: int, table_name='users'):
    async with db_manager as client:
        user_data = await client.select_data(table_name=table_name, where_dict={'user_id': user_id}, one_dict=True)
    return user_data


# функция, для получения всех пользователей (для админки)
async def get_all_users(table_name='users', count=False):
    async with db_manager as client:
        all_users = await client.select_data(table_name=table_name)
    if count:
        return len(all_users)
    else:
        return all_users


# функция, для добавления пользователя в базу данных
async def insert_user(user_data: dict, table_name='users', conflict_column='user_id'):
    async with db_manager as client:
        await client.insert_data_with_update(table_name=table_name,
                                             records_data=user_data,
                                             conflict_column=conflict_column,
                                             update_on_conflict=False)


async def get_dialog_history(db_client, user_id: int, table_name='dialog_history'):
    dialog_history_msg = []
    dialog_history = await db_client.select_data(table_name=table_name, where_dict={'user_id': user_id})
    for msg in dialog_history:
        message = json.loads(msg.get('message'))
        dialog_history_msg.append(message)
    return dialog_history_msg


async def add_message_to_dialog_history(user_id: int, message: dict, table_name='dialog_history', return_history=False):
    async with db_manager as client:
        await client.insert_data_with_update(table_name=table_name,
                                             records_data={'user_id': user_id,
                                                           'message': json.dumps(message)},
                                             conflict_column='id')
        if return_history:
            dialog_history = await get_dialog_history(client, user_id)
            return dialog_history


async def update_dialog_status(client, user_id: int, status: bool, table_name='users'):
    await client.update_data(table_name=table_name,
                             where_dict={'user_id': user_id},
                             update_dict={'in_dialog': status})


async def clear_dialog(user_id: int, dialog_status: bool, table_name='dialog_history'):
    async with db_manager as client:
        await client.delete_data(table_name=table_name, where_dict={'user_id': user_id})
        await update_dialog_status(client, user_id, dialog_status)


async def get_dialog_status(user_id: int, table_name='users'):
    async with db_manager as client:
        user_data = await client.select_data(table_name=table_name, where_dict={'user_id': user_id}, one_dict=True)
    return user_data.get('in_dialog')

Да, тут нужно остановиться подробнее.

Для понимания этого кода вам нужно ознакомиться с синтаксисом Asyncpg-lite. Надеюсь, что вы это сделали.

Как я описывал выше, для работы с базой данных PostgreSQL нам необходимы 2 таблицы: users и dialog_history.

# функция, которая создаст таблицу с пользователями
async def create_table_users(table_name='users'):
    async with db_manager as client:
        columns = [
            {"name": "user_id", "type": BigInteger, "options": {"primary_key": True, "autoincrement": False}},
            {"name": "full_name", "type": String},
            {"name": "user_login", "type": String},
            {"name": "in_dialog", "type": Boolean},
            {"name": "date_reg", "type": TIMESTAMP},
        ]
        await client.create_table(table_name=table_name, columns=columns)


async def create_table_dialog_history(table_name='dialog_history'):
    async with db_manager as client:
        columns = [
            {"name": "id", "type": Integer, "options": {"primary_key": True, "autoincrement": True}},
            {"name": "user_id", "type": BigInteger},
            {"name": "message", "type": JSON}
        ]
        await client.create_table(table_name=table_name, columns=columns)

Благодаря этим 2 простым функциям мы эти таблицы и создадим. Выполнять код можно прямо в файле db_funk.py (для этого оставил импорт asyncio) и в конце файла пример вызова.

Функция для получения информации о пользователе:

# функция, для получения информации по конкретному пользователю
async def get_user_data(user_id: int, table_name='users'):
    async with db_manager as client:
        user_data = await client.select_data(table_name=table_name, where_dict={'user_id': user_id}, one_dict=True)
    return user_data

Данную функцию можно будет использовать под следующие задачи:

  • Проверка на наличие пользователя в базе данных (если пользователя не будет в БД, то функция вернет пустой список)

  • Отображение информации о пользователе в личном профиле (в данном проекте не применяется, как сделать личный профиль при помощи данной функции я писал в этой статье «Telegram Боты на Aiogram 3.x: Профиль, админ-панель и реферальная система»)

Получаем информацию о всех пользователях

async def get_all_users(table_name='users', count=False):
    async with db_manager as client:
        all_users = await client.select_data(table_name=table_name)
    if count:
        return len(all_users)
    else:
        return all_users

Отображение информации о всех пользователях в админ-панеле. В данном проекте функция используется для вывода количества пользователей в базе данных при запуске бота.

Пример с просмотром пользователей в админке при помощи этой функции и ее полное описание найдете в этой статье «Telegram Боты на Aiogram 3.x: Профиль, админ-панель и реферальная система».

Функция для добавления пользователя:

async def insert_user(user_data: dict, table_name='users', conflict_column='user_id'):
    async with db_manager as client:
        await client.insert_data_with_update(table_name=table_name,
                                             records_data=user_data,
                                             conflict_column=conflict_column,
                                             update_on_conflict=False)

При клике на "Старт", если пользователя не было в базе данных, мы сформируем массив с данными пользователя (питоновский словарь). После передадим его в эту функцию и пользователь окажется в базе данных.

Функция для получения истории диалога пользователя:

async def get_dialog_history(db_client, user_id: int, table_name='dialog_history'):
    dialog_history_msg = []
    dialog_history = await db_client.select_data(table_name=table_name, where_dict={'user_id': user_id})
    for msg in dialog_history:
        message = json.loads(msg.get('message'))
        dialog_history_msg.append(message)
    return dialog_history_msg

В данном примере мы получаем все сообщения от LLAMA3 и от пользователя, используя идентификатор пользователя (user_id). Это позволяет нам выбирать только те записи, которые относятся к конкретному пользователю. Благодаря этому решению мы избегаем возможных путаниц даже при наличии значительного количества записей в базе данных от разных пользователей, будь то несколько сотен или тысяч записей.

Пример рабочей таблицы
Пример рабочей таблицы

Функция для добавления записи в таблицу dialog_history

async def add_message_to_dialog_history(user_id: int, message: dict, table_name='dialog_history', return_history=False):
    async with db_manager as client:
        await client.insert_data_with_update(table_name=table_name,
                                             records_data={'user_id': user_id,
                                                           'message': json.dumps(message)},
                                             conflict_column='id')
        if return_history:
            dialog_history = await get_dialog_history(client, user_id)
            return dialog_history

Обратите внимание. Данная функция, при передаче параметра return_history=True будет возвращать весь диалог пользователя в виде списка питоновских словарей. Такое решение принял для оптимизации обращений к базе данных. Далее, когда мы начнем рассматривать пример кода бота, вам станет этот момент более понятным.

Обновление статуса диалога (в диалоге или нет) для каждого пользователя:

async def update_dialog_status(client, user_id: int, status: bool, table_name='users'):
    await client.update_data(table_name=table_name,
                             where_dict={'user_id': user_id},
                             update_dict={'in_dialog': status})

Для работы функции достаточно передать user_id и новый статус (True или False) и в таблице произойдет обновление в колонке in_dialog. Вокруг этого статуса мы, в дальнейшем, будем строить логику проверок и вывод функционала.

Функция для полной очистки истории диалогов пользователя:

async def clear_dialog(user_id: int, dialog_status: bool, table_name='dialog_history'):
    async with db_manager as client:
        await client.delete_data(table_name=table_name, where_dict={'user_id': user_id})
        await update_dialog_status(client, user_id, dialog_status)

Функция для получения статуса диалога:

async def get_dialog_status(user_id: int, table_name='users'):
    async with db_manager as client:
        user_data = await client.select_data(table_name=table_name, where_dict={'user_id': user_id}, one_dict=True)
    return user_data.get('in_dialog')

В целом, обратите внимание на то, как перекликаются функции в файлу db_funk.py. Желательно, чтоб вы разобрались с данной логикой. Так как понимание этих моментов позволят вам, в целом, понять как работает бот.

На данный момент:

  • Мы разобрались как подключить Llama3 к своему проекту (локально и через GROQ)

  • Посмотрели как работает програмное взаимодействие с Llama3 (через две демки)

  • Настроили структуру бота

  • Подготовили базу данных и функции для взаимодействия с ней

Это значит, что настало время писать логику диалога.

Файл handlers/user_router.py:

from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message
from create_bot import bot, client_groq, local_client
from db_handler.db_funk import (get_user_data, insert_user, clear_dialog,
                                add_message_to_dialog_history, get_dialog_status)
from keyboards.kbs import start_kb, stop_speak
from utils.utils import get_now_time
from aiogram.utils.chat_action import ChatActionSender

user_router = Router()


# хендлер команды старт
@user_router.message(Command(commands=['start', 'restart']))
async def cmd_start(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        user_info = await get_user_data(user_id=message.from_user.id)

    if len(user_info) == 0:
        await insert_user(user_data={
            'user_id': message.from_user.id,
            'full_name': message.from_user.full_name,
            'user_login': message.from_user.username,
            'in_dialog': False,
            'date_reg': get_now_time()
        })
        await message.answer(text='Привет! Давай начнем общаться. Для этого просто нажми на кнопку "Начать диалог"',
                             reply_markup=start_kb())
    else:
        await clear_dialog(user_id=message.from_user.id, dialog_status=False)
        await message.answer(text='Диалог очищен. Начнем общаться?', reply_markup=start_kb())


# Хендлер для начала диалога
@user_router.message(F.text.lower().contains('начать диалог'))
async def start_speak(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        await clear_dialog(user_id=message.from_user.id, dialog_status=True)
        await message.answer(text='Диалог начат. Введите ваше сообщение:', reply_markup=stop_speak())


@user_router.message(F.text.lower().contains('завершить диалог'))
async def start_speak(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        await clear_dialog(user_id=message.from_user.id, dialog_status=False)
        await message.answer(text='Диалог очищен! Начнем общаться?', reply_markup=start_kb())


# Хендлер для обработки текстовых сообщений
@user_router.message(F.text)
async def handle_message(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        check_open = await get_dialog_status(message.from_user.id)
        if check_open is False:
            await message.answer(text='Для того чтоб начать общение со мной, пожалуйста, нажмите на кнопку '
                                      '"Начать диалог".', reply_markup=start_kb())
            return
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        # формируем словарь с сообщением пользователя
        user_msg_dict = {"role": "user", "content": message.text}

        # сохраняем сообщение в базу данных и получаем историю диалога
        dialog_history = await add_message_to_dialog_history(user_id=message.from_user.id,
                                                             message=user_msg_dict,
                                                             return_history=True)

        #Пример работы с GROQ
        chat_completion = client_groq.chat.completions.create(model="llama3-70b-8192", messages=dialog_history)
        message_llama = await message.answer(text=chat_completion.choices[0].message.content, reply_markup=stop_speak())

        '''
        # Пример работы с локальной моделью
        
        chat_completion = local_client.chat.completions.create(model="llama3:8b", messages=dialog_history)
        message_llama = await message.answer(text=chat_completion.choices[0].message.content, reply_markup=stop_speak())
        '''

    # формируем словарь с сообщением ассистента
    assistant_msg = {"role": "assistant", "content": message_llama.text}

    # сохраняем сообщение ассистента в базу данных
    await add_message_to_dialog_history(user_id=message.from_user.id, message=assistant_msg,
                                        return_history=False)

Импорты:

from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message
from create_bot import bot, client_groq, local_client
from db_handler.db_funk import (get_user_data, insert_user, clear_dialog,
                                add_message_to_dialog_history, get_dialog_status)
from keyboards.kbs import start_kb, stop_speak
from utils.utils import get_now_time
from aiogram.utils.chat_action import ChatActionSender

Из того на что стоит обратить внимание - это импорты клавиатур (далее покажу вам код) и импорты клиентов Llama3.

  • Если вы хотите использовать локальную версию Llama3, то вам достаточно импортировать local_client.

  • Для использования клиента через платформу GROQ достаточно импортировать client_groq.

Клавиатуры:

from aiogram.types import KeyboardButton, ReplyKeyboardMarkup


def start_kb():
    kb_list = [[KeyboardButton(text="▶️ Начать диалог")]]
    return ReplyKeyboardMarkup(
        keyboard=kb_list,
        resize_keyboard=True,
        one_time_keyboard=True,
        input_field_placeholder="Чтоб начать диалог с ботом жмите ?:"
    )


def stop_speak():
    kb_list = [[KeyboardButton(text="❌ Завершить диалог")]]
    return ReplyKeyboardMarkup(
        keyboard=kb_list,
        resize_keyboard=True,
        one_time_keyboard=True,
        input_field_placeholder="Чтоб завершить диалог с ботом жмите ?:"
    )

Как вы видите в боте будет всего 2 текстовые клавиатуры (можно было объеденить и в одну функцию, но я решил что лучше передать их явно. Подробно тему текстовых клавиатур я рассматривал в статье Telegram Боты на Aiogram 3.x: Текстовая клавиатура и Командное меню.

Создаем роутер. Он у нас в проекте будет единственным:

user_router = Router()

Теперь напишем обработчик функции "start" и "restart":

@user_router.message(Command(commands=['start', 'restart']))
async def cmd_start(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        user_info = await get_user_data(user_id=message.from_user.id)

    if len(user_info) == 0:
        await insert_user(user_data={
            'user_id': message.from_user.id,
            'full_name': message.from_user.full_name,
            'user_login': message.from_user.username,
            'in_dialog': False,
            'date_reg': get_now_time()
        })
        await message.answer(text='Привет! Давай начнем общаться. Для этого просто нажми на кнопку "Начать диалог"',
                             reply_markup=start_kb())
    else:
        await clear_dialog(user_id=message.from_user.id, dialog_status=False)
        await message.answer(text='Диалог очищен. Начнем общаться?', reply_markup=start_kb())

Я решил объеденить под две команды один обработчик.

Смысл данной функции в следующем:

  • Если пользователя не было в базе данных, то мы его добавляем и после отправляем сообщение "Привет! Давай начнем общаться. Для этого просто нажми на кнопку "Начать диалог"" с клавиатурой "Начать диалог". Кроме того, статус пользователя станет "Не в диалоге".

  • Если пользователь уже был в базе данных, то мы очистим его историю общения и установим статус "Не в диалоге" (поэтому подвязал команду restart, так как она более явно демонстирует тот процесс, который происходит)

Эти 2 команды вывел в командное меню
Эти 2 команды вывел в командное меню

Функция начала диалога:

@user_router.message(F.text.lower().contains('начать диалог'))
async def start_speak(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        await clear_dialog(user_id=message.from_user.id, dialog_status=True)
        await message.answer(text='Диалог начат. Введите ваше сообщение:', reply_markup=stop_speak())

Данная функция реагирует на словосочетание "начать диалог" выполняя 3 действия:

  1. Меняет статус диалога для пользователя на "В диалоге" (in_dialog=True)

  2. Отправляет сообщение пользователю "Диалог начат. Введите ваше сообщение" с клавиатурой с возможностью "Завершить диалог"

  3. Очищает историю диалога

Функция завершения диалога:

@user_router.message(F.text.lower().contains('завершить диалог'))
async def start_speak(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        await clear_dialog(user_id=message.from_user.id, dialog_status=False)
        await message.answer(text='Диалог очищен! Начнем общаться?', reply_markup=start_kb())

Данная функция реагирует на словосочетание "начать диалог" выполняя 3 действия:

  1. Меняет статус диалога для пользователя на "Не в диалоге" (in_dialog=False)

  2. Отправляет сообщение пользователю "Диалог очищен! Начнем общаться?" с клавиатурой с возможностью "Завершить диалог".

  3. Очищает историю диалога

Главная функция для диалога (на ней остановимся подробнее):

@user_router.message(F.text)
async def handle_message(message: Message):
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        check_open = await get_dialog_status(message.from_user.id)
        if check_open is False:
            await message.answer(text='Для того чтоб начать общение со мной, пожалуйста, нажмите на кнопку '
                                      '"Начать диалог".', reply_markup=start_kb())
            return
    async with ChatActionSender.typing(bot=bot, chat_id=message.from_user.id):
        # формируем словарь с сообщением пользователя
        user_msg_dict = {"role": "user", "content": message.text}

        # сохраняем сообщение в базу данных и получаем историю диалога
        dialog_history = await add_message_to_dialog_history(user_id=message.from_user.id,
                                                             message=user_msg_dict,
                                                             return_history=True)

        #Пример работы с GROQ
        chat_completion = client_groq.chat.completions.create(model="llama3-70b-8192", messages=dialog_history)
        message_llama = await message.answer(text=chat_completion.choices[0].message.content, reply_markup=stop_speak())

        '''
        # Пример работы с локальной моделью
        
        chat_completion = local_client.chat.completions.create(model="llama3:8b", messages=dialog_history)
        message_llama = await message.answer(text=chat_completion.choices[0].message.content, reply_markup=stop_speak())
        '''

    # формируем словарь с сообщением ассистента
    assistant_msg = {"role": "assistant", "content": message_llama.text}

    # сохраняем сообщение ассистента в базу данных
    await add_message_to_dialog_history(user_id=message.from_user.id, message=assistant_msg,
                                        return_history=False)

Данная функция будет реагировать на любое текстовое сообщение от пользователя.

На старте идет проверка находится ли пользователь в диалоге:

  • Не находится - тогда бот отправляет сообщение "Для того чтоб начать общение со мной, пожалуйста, нажмите на кнопку '"Начать диалог"" с клавиатурой "Начать диалог". После отправки этого сообщения функция завершается. Таким образом мы не запускаем диалог с Llama3 пока пользователь не окажется в статусе "В диалоге" (не нажмет на кнопку "Начать диалог")

  • Находится.

Если пользователь находится в диалоге, то запустится логика, в рамках которой:

  • Будет сформировано сообщение от пользователя в формате: {"role": "user", "content": message.text}

  • Сообщение будет сохранено в базе данных

  • Мы вернем всю историю диалога пользователя и клиента Llama3 в виде списка питоновских словарей

  • История общений (весь список словарей) будет отправлена клиенту Llama3 (GROQ или локальному)

  • Мы перехватим ответ от клиента Llama3

  • Сохраним сообщение от клиета Llama3 в базу данных, закрепив его за пользователем

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

В примере что вы видите я оставил модель Llama3 через GROQ, но, если ваша машина позволяет тянуть большие нейронки - можно воспользоваться локальной версией. Для этого просто импортируйте клиент с openai и раскоментируйте кусок в коде, который отвечает за работу с локальным клиентом Llama3:

chat_completion = local_client.chat.completions.create(model="llama3:8b", messages=dialog_history)
message_llama = await message.answer(text=chat_completion.choices[0].message.content, reply_markup=stop_speak())

Обратите внимание! Для пользования сервисом GROQ вам необходимо запустить VPN, как для доступа к сайту GROQ, так и для работы с моделью Llama3.

Запустим бота на VPS сервере (если он у вас, конечно, есть).

В данном моменте я поделюсь самым простым способом запуска Python проекта на VPS сервере через Docker (даже если вы абсолютно не знакомы с этой технологией, просто повторяйте за мной).

  1. В корне проекта бота создаем файл Dockerfile и заполняем его

FROM python

WORKDIR /usr/src/app
# Копируем и устанавливаем зависимости Python
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Копируем все файлы из текущей директории в рабочую директорию контейнера
COPY . .

# Команда запуска контейнера
CMD ["/bin/bash", "-c", "python aiogram_run.py"]
  1. Создаем в корне проекта файл .env с переменными окружения (это можно сделать и на сервере через утилиту nano)

  2. Закидываем все файлы проекта, вместе с .env (если с ним не забудьте репозиторий на гите сделать приватным!) и Dockerfile

  3. Заходим на VPS сервер

  4. Устанавливаем Docker, если он ещё не был установлен

  5. Создаем папку и закидываем с GitHub в нее файлы бота (git clone или git pull)

  6. Создаем свой именной образ:

docker build -t my_image_name .
  1. Запускаем контейнер

docker run -it -d --env-file .env --restart=unless-stopped --name container_name my_image_name

Тут вы видите, как привязать env-file к рабочему контейнеру. Убедитесь, что файл .env в проекте и в нем указаны все необходимые для бота переменные:

GROQ_API_KEY=your_groq_token
BOT_API_KEY=your_bot_token
ADMINS=admin1,admin2
ROOT_PASS=your_root_password
PG_LINK=postgresql://username:password@host:port/dbname

Просмотр логов

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

docker attach container_name

Для выхода из интерактивного режима воспользуйтесь комбинацией клавиш CTRL+P, CTRL+Q.

Если все настроено корректно, то после запуска контейнера произойдет и запуск бота. Проверим.

После запуска контейнера я получил сообщение от бота. Отлично.
После запуска контейнера я получил сообщение от бота. Отлично.
Разве это не мило?)
Разве это не мило?)
После клика на кнопку диалог очистился.
После клика на кнопку диалог очистился.

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

Рассказал о себе и увел беседу в другую тему.
Рассказал о себе и увел беседу в другую тему.
Не забыл.
Не забыл.

А теперь посмотрим что в базе данных у нас происходит:

Все сообщения сохранены.
Все сообщения сохранены.

Теперь я нажму на кнопку "Очистить диалог" и обновлю таблицу:

Нажимаю на кнопку
Нажимаю на кнопку
Смотрю в таблицу.
Смотрю в таблицу.

Обратите внимание, что очистились только мои сообщения, а сообщения от других пользователей остались.

Теперь давайте спросим бота о том что он обо мне знает.

Забыл меня (
Забыл меня (

Напоминаю, что полный код проекта с демками тут - EasyLlamaBot

Поклацать бота можно тут: Llama3Bot

Заключение

Дорогие друзья, вот и подошла к концу эта статья. Я понимаю, что материал может показаться сложным для тех, у кого нет достаточного опыта работы с языком Python и фреймворком Aiogram3. Тем не менее, я старался сделать изложение и код максимально доступными и понятным для каждого читателя.

Теперь вы знакомы с нейросетью Llama3 и знаете, как работать с ней локально и через платформу GROQ. Саму нейросеть, теперь, вы можете использовать не только в телеграмм ботах, но и в любой другом своем проекте.

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

Надеюсь на вашу поддержку.

Если у вас возникнут вопросы, пишите в комментариях, личных сообщениях или мессенджерах (контактные данные указаны в моем профиле).

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

Благодарю вас за внимание и до скорого!

Комментарии (8)


  1. Moog_Prodigy
    30.06.2024 19:04

    Пока не очень понятно, чем эта Лама-3 лучше ламы-2 при равных вводных (требования). А код она лучше Кодестраля пишет или только общение?


    1. yakvenalex Автор
      30.06.2024 19:04

      LLaMA (Large Language Model Application) - это семейство языковых моделей, разработанных Meta AI. LLaMA2 и LLaMA3 - это две последние версии этой модели. Вот некоторые ключевые улучшения, которые делают LLaMA3 лучше, чем LLaMA2:

      1. Большая языковая модель: LLaMA3 имеет более крупную языковую модель, чем LLaMA2, что позволяет ей лучше понимать контекст и генерировать более качественный текст.

      2. Улучшенная генерация текста: LLaMA3 может генерировать текст более высокого качества, чем LLaMA2, с меньшим количеством ошибок и более естественным языком.

      3. Более точное понимание контекста: LLaMA3 лучше понимает контекст и может отвечать на вопросы более точно, чем LLaMA2.

      4. Улучшенная поддержка многоязычности: LLaMA3 поддерживает более 20 языков, в то время как LLaMA2 поддерживала только несколько языков.

      5. Более быстрое обучение: LLaMA3 может обучаться быстрее, чем LLaMA2, что позволяет ей адаптироваться к новым данным и задачам быстрее.

      6. Улучшенная работа с длинными текстами: LLaMA3 может работать с длинными текстами более эффективно, чем LLaMA2, что позволяет ей лучше понимать контекст и генерировать более качественный текст.

      7. Улучшенная поддержка задач с ограничениями: LLaMA3 может работать с задачами, которые имеют ограничения на длину текста, язык или стиль, что позволяет ей генерировать текст, который лучше соответствует требованиям задачи.

      8. Улучшенная интерпретация запросов: LLaMA3 может лучше интерпретировать запросы и понимать, что пользователь хочет получить в ответ, что позволяет ей генерировать более релевантный текст.

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

      Вот вам ответ от самой LLaMA3))


  1. Pol1mus
    30.06.2024 19:04

    llama3-8b уже можно закапывать, на локальном компьютере https://ollama.com/library/gemma2 работает намного лучше а потребление такое же, бесплатно как сервис гемму можно получить на openrouter.ai, там так же как на groq дают апи ключ и можно использовать библиотеку от openai для доступа ко всем моделям.

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


  1. Ryav
    30.06.2024 19:04
    +1

    Форматирования в ответах не хавтает.


    1. yakvenalex Автор
      30.06.2024 19:04

      Да, вы правы. В целом много планов на бота этого. К примеру выбор языковой модели, форматирование, как вы указали и прочее. Сейчас жду отклика от аудитории и если он будет - по плану ещё есть, как минимум 3 статьи. Но пока ждем)


  1. mDoll
    30.06.2024 19:04
    +1

    «pacedoжалуй» — какое интересное слово у него получилось )


    1. yakvenalex Автор
      30.06.2024 19:04

      да, бывает выдает такое)


    1. Pol1mus
      30.06.2024 19:04

      У gemma2 такого не бывает.