260 lines
10 KiB
Plaintext
260 lines
10 KiB
Plaintext
---
|
|
title: "Discord Bots with Honcho"
|
|
icon: 'discord'
|
|
description: "Discord is a powerful chat application that handles many UI complications"
|
|
sidebarTitle: 'Discord'
|
|
---
|
|
|
|
> Example code is available on [GitHub](https://github.com/plastic-labs/discord-python-starter)
|
|
|
|
Any application interface that defines logic based on events and supports
|
|
special commands can work easily with Honcho. Here's how to use Honcho with
|
|
**Discord** as an interface. If you're not familiar with Discord bot
|
|
application logic, the [py-cord](https://pycord.dev/) docs would be a good
|
|
place to start.
|
|
|
|
## Events
|
|
|
|
Most Discord bots have async functions that listen for specific events, the most common one being messages. We can use Honcho to store messages by user and session based on an interface's event logic. Take the following function definition for example:
|
|
```python
|
|
@bot.event
|
|
async def on_message(message):
|
|
"""Event that is run when a message is sent in a channel or DM that the bot has access to"""
|
|
global last_message_id
|
|
if message.author == bot.user:
|
|
# ensure the bot does not reply to itself
|
|
return
|
|
|
|
is_dm = isinstance(message.channel, discord.DMChannel)
|
|
is_reply_to_bot = (
|
|
message.reference and message.reference.resolved.author == bot.user
|
|
)
|
|
is_mention = bot.user.mentioned_in(message)
|
|
|
|
if is_dm or is_reply_to_bot or is_mention:
|
|
# Remove the bot's mention from the message content if present
|
|
input = message.content.replace(f"<@{bot.user.id}>", "").strip()
|
|
|
|
# If the message is empty after removing the mention, ignore it
|
|
if not input:
|
|
return
|
|
|
|
# Get a user object for the message author
|
|
user_id = f"discord_{str(message.author.id)}"
|
|
user = honcho.apps.users.get_or_create(name=user_id, app_id=app.id)
|
|
|
|
# Use the channel ID as the location_id (for DMs, this will be unique to the user)
|
|
location_id = str(message.channel.id)
|
|
|
|
# Get or create a session for this user and location
|
|
session, _ = get_session(user.id, location_id, create=True)
|
|
|
|
# Get messages
|
|
history_iter = honcho.apps.users.sessions.messages.list(
|
|
app_id=app.id, session_id=session.id, user_id=user.id
|
|
)
|
|
history = list(msg for msg in history_iter)
|
|
|
|
# Add user message to session
|
|
user_msg = honcho.apps.users.sessions.messages.create(
|
|
app_id=app.id,
|
|
user_id=user.id,
|
|
session_id=session.id,
|
|
content=input,
|
|
is_user=True,
|
|
)
|
|
last_message_id = user_msg.id
|
|
|
|
async with message.channel.typing():
|
|
response = llm(input, history)
|
|
|
|
if len(response) > 1500:
|
|
# Split response into chunks at newlines, keeping under 1500 chars
|
|
chunks = []
|
|
current_chunk = ""
|
|
for line in response.splitlines(keepends=True):
|
|
if len(current_chunk) + len(line) > 1500:
|
|
chunks.append(current_chunk)
|
|
current_chunk = line
|
|
else:
|
|
current_chunk += line
|
|
if current_chunk:
|
|
chunks.append(current_chunk)
|
|
for chunk in chunks:
|
|
await message.channel.send(chunk)
|
|
else:
|
|
await message.channel.send(response)
|
|
|
|
# Add bot message to session
|
|
honcho.apps.users.sessions.messages.create(
|
|
app_id=app.id,
|
|
user_id=user.id,
|
|
session_id=session.id,
|
|
content=response,
|
|
is_user=False,
|
|
)
|
|
```
|
|
|
|
Let's break down what this code is doing...
|
|
```python
|
|
@bot.event
|
|
async def on_message(message):
|
|
if message.author == bot.user:
|
|
return
|
|
```
|
|
|
|
This is how you define an event function in `py-cord` that listens for messages and checks that the bot doesn't respond to itself.
|
|
|
|
```python
|
|
is_dm = isinstance(message.channel, discord.DMChannel)
|
|
is_reply_to_bot = (
|
|
message.reference and message.reference.resolved.author == bot.user
|
|
)
|
|
is_mention = bot.user.mentioned_in(message)
|
|
```
|
|
|
|
These lines check what kind of message is being sent in Discord, which is a useful condition to check before entering the reply logic. The code inside that if-statement is commented quite well, so we'll just go over the relevant Honcho parts.
|
|
|
|
```python
|
|
# Get a user object for the message author
|
|
user_id = f"discord_{str(message.author.id)}"
|
|
user = honcho.apps.users.get_or_create(name=user_id, app_id=app.id)
|
|
```
|
|
|
|
Here we're getting or creating a user for an app that's been defined at the top of the file.
|
|
|
|
```python
|
|
# Use the channel ID as the location_id (for DMs, this will be unique to the user)
|
|
location_id = str(message.channel.id)
|
|
|
|
# Get or create a session for this user and location
|
|
session, _ = get_session(user.id, location_id, create=True)
|
|
```
|
|
|
|
Here we're using the discord channel ID as a unique `location_id` to attach as metadata to the session. Then we have a nice [helper function](https://github.com/plastic-labs/discord-python-starter/blob/main/src/bot.py#L85) to take care of some of the session querying logic--we'll dive into that shortly.
|
|
|
|
```python
|
|
# Get messages
|
|
history_iter = honcho.apps.users.sessions.messages.list(
|
|
app_id=app.id, session_id=session.id, user_id=user.id
|
|
)
|
|
history = list(msg for msg in history_iter)
|
|
```
|
|
|
|
When you call the `list` method, it returns an iterable which you can quickly loop over to create a list of `Message` objects. Then, we make the call to the LLM using another neat [helper function](https://github.com/plastic-labs/discord-python-starter/blob/main/src/bot.py#L52) that we will cover.
|
|
|
|
## Helper functions
|
|
|
|
The first helper function we create is called `get_session`. This simplifies a lot of our session-querying logic.
|
|
|
|
```python
|
|
def get_session(user_id, location_id, create=False):
|
|
# Get an existing session for the user and location or optionally create a new one if none exists.
|
|
# Returns a tuple of (session, is_new) where is_new indicates if a new session was created.
|
|
|
|
# Query for *active* sessions with both user_id and location_id
|
|
sessions_iter = honcho.apps.users.sessions.list(
|
|
app_id=app.id, user_id=user_id, reverse=True, filters={"is_active": True}
|
|
)
|
|
sessions = list(session for session in sessions_iter)
|
|
|
|
# Find the right session
|
|
for session in sessions:
|
|
if session.metadata.get("location_id") == location_id:
|
|
return session, False
|
|
|
|
# If no session is found and create is True, create a new one
|
|
if create:
|
|
print("No active session found, creating new one")
|
|
return honcho.apps.users.sessions.create(
|
|
user_id=user_id,
|
|
app_id=app.id,
|
|
metadata={"location_id": location_id},
|
|
), True
|
|
|
|
return None, False
|
|
```
|
|
|
|
You can see the `list` method on the sessions object similarly returns an iterable. This is a common pattern in Honcho--use list comprehension to create your new python list. Then loop through those session objects to find the appropriate `location_id` in the metadata, and if none are found then create a new session. You'll also notice we list messages in `reverse=True` order--this means you will get the most recent ones first. We also support native filtering by active sessions and any other fields or user-defined metadata on the session object.
|
|
|
|
The next helper function we create is called `llm`. This simplifies constructing the chat message object we're going to send to the inference provider.
|
|
|
|
```python
|
|
def llm(prompt, previous_chats=None):
|
|
messages = []
|
|
|
|
# Add system message with documentation context
|
|
messages.append(
|
|
{
|
|
"role": "system",
|
|
"content": f"You are a helpful assistant."
|
|
}
|
|
)
|
|
|
|
if previous_chats:
|
|
messages.extend(
|
|
[
|
|
{"role": "user" if msg.is_user else "assistant", "content": msg.content}
|
|
for msg in previous_chats
|
|
]
|
|
)
|
|
|
|
|
|
messages.append({"role": "user", "content": prompt})
|
|
|
|
try:
|
|
completion = openai.chat.completions.create(
|
|
model=MODEL_NAME,
|
|
messages=messages,
|
|
)
|
|
return completion.choices[0].message.content
|
|
except Exception as e:
|
|
print(e)
|
|
return f"Error: {e}"
|
|
```
|
|
|
|
Note that `messages` is a list of dictionaries that are individually defined with key-value pairs for roles and content. We again use list comprehension to unpack historical message objects into the list that we send to the chat completions method. Honcho `Message` objects store role and content natively to make this context construction as simple as possible. If you're interested in learning more about native Honcho objects, you can check out the [`models.py`](https://github.com/plastic-labs/honcho/blob/main/src/models.py) file.
|
|
|
|
## Slash Commands
|
|
|
|
Discord bots also offer slash command functionality. We can use Honcho to do
|
|
interesting things via slash commands. Here's a simple example:
|
|
|
|
```python
|
|
@bot.slash_command(
|
|
name="restart",
|
|
description="Reset all of your messaging history with Honcho in this channel.",
|
|
)
|
|
async def restart(ctx):
|
|
print(f"restarting conversation for {ctx.author.name}")
|
|
async with ctx.typing():
|
|
user_name = f"discord_{str(ctx.author.id)}"
|
|
user = honcho.apps.users.get_or_create(name=user_name, app_id=app.id)
|
|
location_id = str(ctx.channel_id)
|
|
|
|
# Get existing session
|
|
session, _ = get_session(user.id, location_id, create=False)
|
|
|
|
if session:
|
|
# Delete the session
|
|
honcho.apps.users.sessions.delete(
|
|
app_id=app.id, user_id=user.id, session_id=session.id
|
|
)
|
|
|
|
msg = "The conversation has been restarted."
|
|
|
|
await ctx.respond(msg)
|
|
```
|
|
|
|
This slash command restarts a conversation with a bot. In Honcho, the `delete` method marks a session's `is_active` field to `False`.
|
|
|
|
## Recap
|
|
|
|
How you use Honcho is tightly coupled with the client you're building in. Here, Discord serves as an example of an interactive chat interface. We're just scratching the surface of things you can do with Honcho, but we learned some key patterns:
|
|
- how to register users
|
|
- how to work with iterables when listing sessions, messages
|
|
- how to attach metadata to Honcho objects (like `location_id` on sessions)
|
|
- how to sort and filter when calling list methods
|
|
|
|
You are well on your way to becoming a context construction master! Stay tuned for more in-depth examples. If you want a challenge, try deciphering how we construct context in one of our apps, [Bloom](https://github.com/plastic-labs/tutor-gpt/blob/main/app/Chat.tsx).
|