Ubuntu TechHive
enhancing-discord-bots-with-llms-and-python.md
使用 LLM 和 Python 增强 Discord 机器人
article.细节

使用 LLM 和 Python 增强 Discord 机器人

reading.进展 7 分钟阅读数

使用 LLM 和 Python 增强 Discord 机器人的说明

使用 LLM 和 Python 增强 Discord 机器人

功能

  • 执行用户命令
  • 过滤垃圾信息
  • 执行后台任务

flowchart TD
    A[应用程序] -->|发送输入| B[函数调用器]
    B -->|查询| C[LLM]
    C -->|返回原始输出| D[响应处理器]
    D -->|使用以下工具构建数据| E[Pydantic 模型]
    E --> F{结构化输出}
    F -->|供使用| A

    subgraph large_language_models [大语言模型]
    C
    end

    subgraph response_handling [响应处理]
    D
    E
    end
  • 应用程序:这是需要与 LLM 交互的主系统或服务。
  • 函数调用器:充当中间件,将输入发送给 LLM 并接收原始输出。该组件封装了与 LLM 通信所需的逻辑。
  • LLM(大语言模型):根据函数调用处理输入并生成输出的 AI 模型。例如 GPT-4、Claude3 等。
  • 响应处理器:接收来自 LLM 的原始输出并开始对其进行结构化处理。这可能涉及错误检查、过滤以及为转换为结构化格式准备数据。
  • Pydantic 模型:用于显式定义输出数据的结构。Pydantic 模型强制执行类型检查和数据验证,有助于确保数据符合指定的模式。
  • 结构化输出:最终输出,结构良好且可供应用程序使用。这种输出是可预测的,更容易集成到下游流程或系统中。
flowchart TD
    A[客户端应用程序] -->|使用| B[Instructor 库]
    B --> |封装| C[GPT-4]
    B --> |封装| D[LLAMA3]
    B --> |封装| E[Claude3]

    C --> G((Pydantic 模型))
    D --> G
    E --> G

    G --> H{结构化输出}
    H -->|返回给| A

    subgraph large_language_models [大语言模型]
    C
    D
    E
    end

    subgraph pydantic_modelling [输出结构化]
    G
    end
  • 客户端应用程序:这是需要与大语言模型交互的 Python 应用程序。它使用 Instructor 库来促进这些交互。
  • Instructor 库:充当封装大语言模型的中间件。它负责向这些模型发送请求并处理其输出。
  • 大语言模型:包括 GPT-4、LLAMA3 和 Claude3。这些模型中的每一个都可以根据接收到的输入生成复杂的文本输出。
  • Pydantic 模型:在 Instructor 库中使用,用于将语言模型的原始输出结构化为更易于管理和定义的格式,使应用程序开发更简洁、更可预测。
  • 结构化输出:最终的结构化输出随后返回给客户端应用程序,在那里可以进一步利用或显示。

实现

API 实现

#!/usr/bin/env python3

from litestar import Litestar, get, post
from litestar.openapi import OpenAPIConfig
from litestar.openapi.plugins import (
    ScalarRenderPlugin,
    RapidocRenderPlugin,
    RedocRenderPlugin,
    SwaggerRenderPlugin,
)

from enhanced_discord_bot_llms.llm_svc import (
    LLMModel,
    gen_async_client,
    UserInfo,
    streaming_usine_de_gaou_creation,
)


@get("/", sync_to_thread=False)
def read_root() -> dict:
    return {"Hello": "World"}


@post("/gaou/{parametre:str}")
async def creer_gaou(parametre: str) -> UserInfo:
    model = LLMModel.LLAMA3
    client = gen_async_client(model=model)
    gaou = await streaming_usine_de_gaou_creation(client, parametre, model=model)
    print(f"Nouveau gaou créé: {gaou},\n selon le paramètre {parametre}\n\n")
    return gaou


app = Litestar(
    route_handlers=[read_root, creer_gaou],
    openapi_config=OpenAPIConfig(
        title="Gaou API",
        description="API pour créer des Gaous",
        version="0.1.0",
        path="/docs",
        render_plugins=[
            RapidocRenderPlugin(),
            # RedocRenderPlugin(),
            # ScalarRenderPlugin(),
            # SwaggerRenderPlugin(),
        ],
    ),
    debug=True,
)

if __name__ == "__main__":
    import uvicorn

    application = "gaouapp:app"
    uvicorn.run(application, host="0.0.0.0", port=8000, reload=True)

机器人服务

#!/usr/bin/env python3
import os

import instructor

from instructor import Instructor, AsyncInstructor
from anthropic import Anthropic, AsyncAnthropic
from groq import Groq, AsyncGroq
from openai import OpenAI, AsyncOpenAI
from pydantic import BaseModel, Field

from enum import Enum, auto


class LLMModel(str, Enum):
    Claude3 = "claude-3-opus-20240229"
    GPT4_Omni = "gpt-4o"
    LLAMA3 = "llama3-70b-8192"


def gen_client(model=LLMModel.GPT4_Omni) -> Instructor:
    match model:
        case LLMModel.Claude3:
            client = instructor.from_anthropic(Anthropic())
        case LLMModel.GPT4_Omni:
            client = instructor.patch(OpenAI())
        case LLMModel.LLAMA3:
            client = instructor.patch(Groq())
    return client


def gen_async_client(model=LLMModel.GPT4_Omni) -> AsyncInstructor:
    match model:
        case LLMModel.Claude3:
            client = instructor.from_anthropic(AsyncAnthropic())
        case LLMModel.GPT4_Omni:
            client = instructor.patch(AsyncOpenAI())
        case LLMModel.LLAMA3:
            client = instructor.patch(AsyncGroq())
    return client


## Gaou 领域


class UserInfo(BaseModel):
    name: str
    age: int
    is_teenager: bool
    is_intelligent: bool


def usine_de_gaou_creation(
    ai_client: Instructor, parametre: str, model=LLMModel.GPT4_Omni
) -> UserInfo:
    gaou = ai_client.chat.completions.create(
        model=model,
        response_model=UserInfo,
        messages=[{"role": "user", "content": parametre}],
    )
    return gaou


async def streaming_usine_de_gaou_creation(
    ai_client: AsyncInstructor, parametre: str, model=LLMModel.GPT4_Omni
) -> UserInfo:
    gaou = await ai_client.chat.completions.create(
        model=model,
        response_model=UserInfo,
        messages=[
            {
                "role": "system",
                "content": "The user may provide a prompt in their language of choice (such as english, french, creol, spanish etc.), so take that fact into account.",
            },
            {"role": "user", "content": parametre},
        ],
    )
    return gaou


class Language(str, Enum):
    nouchi = "Nouchi"
    moore = "Mooré"
    lingala = "Lingala"
    english = "English"
    french = "French"
    creole = "Créole"
    spanish = "Spanish"


class GaouJoke(BaseModel):
    friend_gaou_joke: str = Field(
        ...,
        description="The joke that qualifies the friend as a Gaou. The joke should be light and humorous as well as alternate between Nouchi, Mooré, Lingala, English, French, Créole and Spanish.",
    )
    language: Language


async def streaming_gaou_formula(
    ai_client: AsyncInstructor, gaou_name: str, model=LLMModel.GPT4_Omni
) -> GaouJoke:
    gaou = await ai_client.chat.completions.create(
        model=model,
        temperature=1,  # Go wild with the temperature!!!!
        max_tokens=1024,
        response_model=GaouJoke,
        messages=[
            {
                "role": "system",
                "content": f"""
                The term 'Gaou' is a funny term, used only amongs friends. For example, {gaou_name} is so Gaou!.
                You will assist in qualifying a friend as a Gaou, based on the following criteria:
                - The friend's name
                - Make up a light joke which always ends up qualifying the friend as a Gaou
                - Mix in some humor and sarcasm
                - In a way Gaou means someone who is naive, gullible, or easily fooled but in a friendly way
                - Use different languages out of one of the following: Mooré, English, French, Créole, Spanish etc.
                """,
            },
            {"role": "user", "content": gaou_name},
        ],
    )
    return gaou

机器人实现

#!/usr/bin/env python3

import os
import random

import discord

from asyncio import sleep

from discord.ext import commands, tasks

from enhanced_discord_bot_llms.constants import (
    WORDS_THE_BOT_DONT_LIKE,
    FROWNING_FACE_EMOJI,
)
from enhanced_discord_bot_llms.llm_svc import (
    gen_client,
    usine_de_gaou_creation,
    gen_async_client,
    streaming_usine_de_gaou_creation,
    LLMModel,
    streaming_gaou_formula,
)

intents = discord.Intents.default()
intents.typing = False
intents.messages = True
intents.message_content = True
intents.reactions = True
intents.members = True

bot = commands.Bot(command_prefix="?", intents=intents)


@bot.event
async def on_message(message):
    # we do not want the bot to reply to itself
    if message.author == bot.user:
        return

    try:
        content = message.content.lower()
        for word in WORDS_THE_BOT_DONT_LIKE:
            if word in content:
                await sleep(10)
                await message.channel.send(
                    f"{message.author.mention} Hey! Do not use that word again {FROWNING_FACE_EMOJI}"
                )
                await message.channel.send(
                    f"{message.author.mention} You called me: {word} and I don't like it. I've deleted your message."
                )
                await sleep(10)
                await message.delete()
    except Exception as e:
        print(f"Error: {e}")

    if message.content == "pingGG":
        await message.channel.send("pongGG")
        return

    await bot.process_commands(message)


@bot.event
async def on_message_edit(before, after):
    if before.author == bot.user:
        return

    if after.content == "ping":
        await after.channel.send("pong")
        return

    await bot.process_commands(after)


@bot.command()
@commands.guild_only()
async def ping(ctx: commands.Context):
    """
    ctx: Context (discord.ext.commands.Context, information about the command)

    ?ping
    """
    await ctx.reply("pong")


@bot.command()
@commands.guild_only()
async def new_gaou(ctx: commands.Context, parametre: str):
    """
    ctx: Context (discord.ext.commands.Context, information about the command)
    parametre: str (message to send to the model)

    ?new_gaou "I am not a Gaou named Lambert who is 15 years old and is intelligent."
    """
    model = LLMModel.GPT4_Omni
    # model = LLMModel.LLAMA3
    try:
        client = gen_async_client(model=model)
        gueou = await streaming_usine_de_gaou_creation(client, parametre, model=model)
        await ctx.reply(f"""
```json

{gueou.model_dump_json(
    indent=4
)}
```
""")
    except Exception as e:
        print(f"Error: {e}")
        await ctx.reply(f"An error occurred: {e}")


@bot.command()
@commands.has_permissions(administrator=True)
@commands.bot_has_permissions(manage_messages=True)
async def cleanup(ctx: commands.Context, limit: int):
    """
    ctx: Context (discord.ext.commands.Context, information about the command)
    limit: int (number of messages to delete)

    ?cleanup 10
    """
    await delete_messages(ctx, limit)


@bot.command()
@commands.dm_only()
async def dm_cleanup(ctx: commands.Context, limit: int):
    """
    ctx: Context (discord.ext.commands.Context, information about the command)
    limit: int (number of messages to delete)

    ?dm_cleanup 10
    """
    await delete_messages(ctx, limit)


async def delete_messages(ctx: commands.Context, limit: int):
    print(f"Cleaning up: {limit} messages...")
    async for msg in ctx.channel.history(limit=limit):
        try:
            print(f"Deleting message: {msg.content}")
            await sleep(1)
            await msg.delete()
        except Exception as e:
            print(f"Error: {e}")
            await ctx.reply(f"You may not have permission to delete messages.")
            continue


@tasks.loop(minutes=16)
async def my_background_gaou_tasks():
    await bot.change_presence(activity=discord.Game(name="With Gaous"))
    # members = [[member for member in guild.members] for guild in bot.guilds]
    # members = bot.get_all_members()
    channels = bot.get_all_channels()
    for chnl in channels:
        if isinstance(chnl, discord.TextChannel) and chnl.name == "botexperiments":
            await chnl.send(
                f"Who's Gaou anyway? Me Gaou? Think again... {chnl.mention}"
            )
            chnl_members = chnl.members
            for chnl_m in chnl_members:
                if chnl_m.bot:
                    continue
                elif (
                    "african" in chnl_m.name.lower()
                    or "dog" in chnl_m.name.lower()
                    or "lle" in chnl_m.name.lower()
                    or "bru" in chnl_m.name.lower()
                ):
                    await sleep(8)
                    # model = random.choice(
                    #     [model.value for model in LLMModel]
                    # )  # Choose a model at random
                    model = LLMModel.Claude3
                    client = gen_async_client(model=model)
                    gueou_joke = await streaming_gaou_formula(
                        client, chnl_m.display_name, model=model
                    )
                    # message_to_gueou = f"{gueou_joke.friend_gaou_joke} ({gueou_joke.language.name} => {gueou_joke.language.value}) {chnl_m.mention}"
                    message_to_gueou = f"{gueou_joke.friend_gaou_joke} ({gueou_joke.language.value}) {chnl_m.mention}"
                    await chnl.send(message_to_gueou)


@my_background_gaou_tasks.before_loop
async def before_gueou():
    await bot.wait_until_ready()
    print("Ready for Gaous!")


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user} (ID: {bot.user.id})")
    my_background_gaou_tasks.start()


if __name__ == "__main__":
    token = os.environ["DISCORD_BOT_TOKEN"]
    bot.run(token)

部署

Docker 容器

Dockerize Discord API

FROM python:3.12.3-alpine3.19

COPY . .

RUN apk add --no-cache libffi-dev openssl-dev gcc musl-dev make

RUN pip install -r requirements.lock

WORKDIR /src/enhanced_discord_bot_llms

CMD ["python", "gaouapp.py"]

Dockerize Discord Bot

FROM python:3.12.3-alpine3.19

COPY . .

RUN apk add --no-cache libffi-dev openssl-dev gcc musl-dev make

RUN pip install -r requirements.lock

WORKDIR /src/enhanced_discord_bot_llms

CMD ["python", "gaoubot.py"]

辅助脚本

#!/usr/bin/env bash
set -x #echo on

BASEDIR=$(dirname "$0")
DOCKERDIR=$BASEDIR/docker
PLATFORM=linux/amd64
REGISTRY=ttl.sh

echo "BASEDIR: $BASEDIR"
echo "DOCKERDIR: $DOCKERDIR"

case "$1" in
  "dockerize:api")
    echo "Building Docker image for API..."
    docker buildx build --platform $PLATFORM -t $2 -f $DOCKERDIR/Dockerfile.api $BASEDIR
    ;;
  "dockerize:bot")
    echo "Building Docker image for Bot..."
    docker buildx build --platform $PLATFORM -t $2 -f $DOCKERDIR/Dockerfile.bot $BASEDIR
    ;;
  "docker:publish")
    echo "Publishing Docker image..."
    docker push $2
    ;;
  *)
    echo "Usage: $0 {dockerize:api|dockerize:bot|docker:publish}"
    exit 1
    ;;
esac

exit 0

基础设施

graph TB

DockerEngine(Docker 引擎)

DockerEngine -- 运行 --> DockerContainer
DockerEngine -- 构建 --> DockerImage

DockerFile(Dockerfile: 镜像配方) -- 定义 --> DockerImage

DockerHub(Docker Hub: 公共仓库) -- 存储和共享 --> DockerImage

DockerContainer(Docker 容器: 微型、独立、可执行的包)
DockerImage(Docker 镜像: 容器蓝图) -- 创建 --> DockerContainer

subgraph "类比:建筑"
  DockerFile -- "建筑师规划图" --> DockerImage
  DockerImage -- "预制房屋部件" --> DockerContainer
end
在 Ubuntu 上安装 Docker
# 更新现有的软件包列表
sudo apt update

# 安装一些必备软件包,允许 `apt` 通过 HTTPS 使用软件包
sudo apt install apt-transport-https ca-certificates curl software-properties-common

# 将官方 Docker 仓库的 GPG 密钥添加到您的系统
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 将 Docker 仓库添加到 APT 源
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 再次更新现有的软件包列表,以便识别新增内容
sudo apt update

# 确保您将从 Docker 仓库而不是默认的 Ubuntu 仓库进行安装
apt-cache policy docker-ce

# 安装 Docker
sudo apt install docker-ce

# 检查它是否正在运行
sudo systemctl status docker
配置 Docker

无需 `sudo` 即可运行 Docker

# 将您的用户名添加到 docker 组
sudo usermod -aG docker ${USER}

应用新的组成员身份,注销服务器并重新登录(可选?)

su - ${USER}

groups
环境变量管理

我们将使用 direnv 并将其在虚拟机内部为 bash 进行配置

sudo apt install direnv

eval "$(direnv hook bash)"