Example code is available on GitHub

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 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:

@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…

@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.

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.

# 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.

# 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 to take care of some of the session querying logic—we’ll dive into that shortly.

# 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 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.

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, 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.

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.

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 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:

@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.