360 lines
12 KiB
Plaintext
360 lines
12 KiB
Plaintext
---
|
|
title: "Telegram Bots with Honcho"
|
|
icon: 'telegram'
|
|
description: "Use Honcho to build a Telegram bot with conversational memory and context management."
|
|
sidebarTitle: 'Telegram Bot'
|
|
---
|
|
|
|
> Example code is available on [GitHub](https://github.com/plastic-labs/telegram-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
|
|
**Telegram** as an interface. If you're not familiar with Telegram bot
|
|
development, the [python-telegram-bot](https://docs.python-telegram-bot.org/en/stable/) docs would be a good
|
|
place to start.
|
|
|
|
## Message Handling
|
|
|
|
Most Telegram bots have async functions that handle incoming messages. We can use Honcho to store messages by user and session based on the chat context. Take the following function definition for example:
|
|
|
|
```python
|
|
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Receive a message from Telegram and respond with a message from our LLM assistant.
|
|
"""
|
|
if not validate_message(update, context):
|
|
return
|
|
|
|
message_text = update.effective_message.text
|
|
input_text = sanitize_message(message_text, context.bot.username)
|
|
|
|
# If the message is empty after sanitizing, ignore it
|
|
if not input_text:
|
|
return
|
|
|
|
peer = honcho_client.peer(id=get_peer_id_from_telegram(update))
|
|
session = honcho_client.session(id=str(update.effective_chat.id))
|
|
|
|
# Send typing indicator
|
|
await context.bot.send_chat_action(
|
|
chat_id=update.effective_chat.id, action="typing"
|
|
)
|
|
|
|
response = llm(session, input_text)
|
|
|
|
await send_telegram_message(update, context, response)
|
|
|
|
# Save both the user's message and the bot's response to the session
|
|
session.add_messages(
|
|
[
|
|
peer.message(input_text),
|
|
assistant.message(response),
|
|
]
|
|
)
|
|
```
|
|
|
|
Let's break down what this code is doing...
|
|
|
|
```python
|
|
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
if not validate_message(update, context):
|
|
return
|
|
```
|
|
|
|
This is how you define a message handler in `python-telegram-bot` that processes incoming messages. We use a helper function `validate_message()` to check if the message should be processed.
|
|
|
|
## Helper Functions
|
|
|
|
The code uses several helper functions to keep the main logic clean and readable. Let's examine each one:
|
|
|
|
### Message Validation
|
|
|
|
```python
|
|
def validate_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
|
"""
|
|
Determine if the message is valid for the bot to respond to.
|
|
Return True if it is, False otherwise. The bot will respond to:
|
|
- Direct messages (private chats)
|
|
- Group messages that mention the bot or reply to it
|
|
- Messages that are not from the bot itself
|
|
"""
|
|
message = update.effective_message
|
|
|
|
if not message or not message.text:
|
|
return False
|
|
|
|
# Don't respond to our own messages
|
|
if message.from_user.id == context.bot.id:
|
|
return False
|
|
|
|
# Always respond in private chats
|
|
if update.effective_chat.type == "private":
|
|
return True
|
|
|
|
# In groups, only respond if mentioned or replied to
|
|
if (
|
|
message.reply_to_message
|
|
and message.reply_to_message.from_user.id == context.bot.id
|
|
):
|
|
return True
|
|
|
|
# Check if bot is mentioned
|
|
if message.entities:
|
|
for entity in message.entities:
|
|
if entity.type == "mention":
|
|
username = message.text[entity.offset : entity.offset + entity.length]
|
|
if username == f"@{context.bot.username}":
|
|
return True
|
|
|
|
return False
|
|
```
|
|
|
|
This function centralizes all the logic for determining whether the bot should respond to a message. It handles different chat types:
|
|
- **Private chats**: Always respond
|
|
- **Group chats**: Only respond when mentioned or when replying to the bot's messages
|
|
- **Bot prevention**: Never respond to the bot's own messages
|
|
|
|
### Message Sanitization
|
|
|
|
```python
|
|
def sanitize_message(message_text: str, bot_username: str) -> str | None:
|
|
"""Remove the bot's mention from the message content if present"""
|
|
content = message_text.replace(f"@{bot_username}", "").strip()
|
|
if not content:
|
|
return None
|
|
return content
|
|
```
|
|
|
|
This helper removes the bot's mention from the message content, leaving just the actual user input.
|
|
|
|
### Peer ID Generation
|
|
|
|
```python
|
|
def get_peer_id_from_telegram(update: Update) -> str:
|
|
"""Get a Honcho peer ID for the message author"""
|
|
return f"telegram_{update.effective_user.id}"
|
|
```
|
|
|
|
This creates a unique peer identifier for each Telegram user by prefixing their Telegram user ID.
|
|
|
|
### LLM Integration
|
|
|
|
```python
|
|
def llm(session, prompt) -> str:
|
|
"""
|
|
Call the LLM with the given prompt and chat history.
|
|
|
|
You should expand this function with custom logic, prompts, etc.
|
|
"""
|
|
messages: list[dict[str, object]] = session.get_context().to_openai(
|
|
assistant=assistant
|
|
)
|
|
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:
|
|
logger.error(f"LLM error: {e}")
|
|
return f"Error: {e}"
|
|
```
|
|
|
|
This function handles the LLM interaction. It uses Honcho's built-in `to_openai()` method to automatically convert the session context into the format expected by OpenAI's chat completions API.
|
|
|
|
### Message Sending
|
|
|
|
```python
|
|
async def send_telegram_message(
|
|
update: Update, context: ContextTypes.DEFAULT_TYPE, response_content: str
|
|
):
|
|
"""Send a message to the Telegram chat, splitting if necessary"""
|
|
# Telegram has a 4096 character limit, but we'll use 4000 to be safe
|
|
max_length = 4000
|
|
|
|
if len(response_content) <= max_length:
|
|
await update.effective_message.reply_text(response_content)
|
|
else:
|
|
# Split response into chunks at newlines, keeping under max_length chars
|
|
chunks = []
|
|
current_chunk = ""
|
|
|
|
for line in response_content.splitlines(keepends=True):
|
|
if len(current_chunk) + len(line) > max_length:
|
|
if current_chunk:
|
|
chunks.append(current_chunk)
|
|
current_chunk = line
|
|
else:
|
|
current_chunk += line
|
|
|
|
if current_chunk:
|
|
chunks.append(current_chunk)
|
|
|
|
for chunk in chunks:
|
|
await update.effective_message.reply_text(chunk)
|
|
```
|
|
|
|
This function handles sending messages to Telegram, automatically splitting long responses into multiple messages to stay within Telegram's 4096 character limit. It also includes a typing indicator to show the bot is processing.
|
|
|
|
## Honcho Integration
|
|
|
|
The new Honcho peer/session API makes integration much simpler:
|
|
|
|
```python
|
|
peer = honcho_client.peer(id=get_peer_id_from_telegram(update))
|
|
session = honcho_client.session(id=str(update.effective_chat.id))
|
|
```
|
|
|
|
Here we create a peer object for the user and a session object using the Telegram chat ID. This automatically handles user and session management across both private chats and group conversations.
|
|
|
|
```python
|
|
# Save both the user's message and the bot's response to the session
|
|
session.add_messages(
|
|
[
|
|
peer.message(input_text),
|
|
assistant.message(response),
|
|
]
|
|
)
|
|
```
|
|
|
|
After generating the response, we save both the user's input and the bot's response to the session using the `add_messages()` method. The `peer.message()` creates a message from the user, while `assistant.message()` creates a message from the assistant.
|
|
|
|
## Commands
|
|
|
|
Telegram bots support slash commands natively. Here's how to implement the `/dialectic` command using Honcho's dialectic feature:
|
|
|
|
```python
|
|
async def dialectic_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle the /dialectic command to query the Honcho Dialectic endpoint.
|
|
"""
|
|
if not context.args:
|
|
await update.message.reply_text(
|
|
"Please provide a query. Usage: /dialectic <your query>"
|
|
)
|
|
return
|
|
|
|
query = " ".join(context.args)
|
|
|
|
try:
|
|
peer = honcho_client.peer(id=get_peer_id_from_telegram(update))
|
|
session = honcho_client.session(id=str(update.effective_chat.id))
|
|
|
|
response = peer.chat(
|
|
query=query,
|
|
session_id=session.id,
|
|
)
|
|
|
|
if response:
|
|
await send_telegram_message(update, context, response)
|
|
else:
|
|
await update.message.reply_text(
|
|
f"I don't know anything about {update.effective_user.first_name} because we haven't talked yet!"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error calling Dialectic API: {e}")
|
|
await update.message.reply_text(
|
|
f"Sorry, there was an error processing your request: {str(e)}"
|
|
)
|
|
```
|
|
|
|
You can also add a `/start` command for user onboarding:
|
|
|
|
```python
|
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handle the /start command"""
|
|
await update.message.reply_text(
|
|
"Hello! I'm your AI assistant. You can:\n"
|
|
"• Chat with me directly in private messages\n"
|
|
"• Mention me (@username) in groups to get my attention\n"
|
|
"• Use /dialectic <query> to search our conversation history\n\n"
|
|
"Let's start chatting!"
|
|
)
|
|
```
|
|
|
|
## Setup and Configuration
|
|
|
|
The bot requires several environment variables and setup:
|
|
|
|
```python
|
|
honcho_client = Honcho()
|
|
assistant = honcho_client.peer(id="assistant", config={"observe_me": False})
|
|
openai = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=MODEL_API_KEY)
|
|
```
|
|
|
|
- `honcho_client`: The main Honcho client
|
|
- `assistant`: A peer representing the bot/assistant
|
|
- `openai`: OpenAI client configured to use OpenRouter
|
|
|
|
### Application Setup
|
|
|
|
Register your handlers with the Telegram application:
|
|
|
|
```python
|
|
def main():
|
|
"""Start the bot"""
|
|
if not BOT_TOKEN:
|
|
logger.error("BOT_TOKEN not found in environment variables")
|
|
return
|
|
|
|
# Create the Application
|
|
application = Application.builder().token(BOT_TOKEN).build()
|
|
|
|
# Add handlers
|
|
application.add_handler(CommandHandler("start", start_command))
|
|
application.add_handler(CommandHandler("dialectic", dialectic_command))
|
|
application.add_handler(
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)
|
|
)
|
|
|
|
# Start the bot
|
|
logger.info("Starting Telegram bot...")
|
|
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
Your bot needs these environment variables:
|
|
|
|
```env
|
|
# Your Telegram bot token from BotFather
|
|
BOT_TOKEN=<your-token>
|
|
|
|
# AI model to use (see OpenRouter for available models)
|
|
MODEL_NAME=<your-model>
|
|
|
|
# Your OpenRouter API key
|
|
MODEL_API_KEY=<your-openrouter-api-key>
|
|
```
|
|
|
|
## Chat Types and Behavior
|
|
|
|
The bot handles different Telegram chat types intelligently:
|
|
|
|
### Private Chats
|
|
- **Behavior**: Responds to all messages
|
|
- **Session ID**: Uses the private chat ID
|
|
- **Memory**: Maintains conversation history per user
|
|
|
|
### Group Chats
|
|
- **Behavior**: Only responds when mentioned or replied to
|
|
- **Session ID**: Uses the group chat ID (shared across all members)
|
|
- **Memory**: Maintains group conversation context
|
|
|
|
## Recap
|
|
|
|
The new Honcho peer/session API makes Telegram bot integration much simpler and more intuitive. Key patterns we learned:
|
|
|
|
- **Peer/Session Model**: Users are represented as peers, conversations as sessions
|
|
- **Chat Type Handling**: Different validation logic for private vs group chats
|
|
- **Automatic Context Management**: `session.get_context().to_openai()` automatically formats chat history
|
|
- **Message Storage**: `session.add_messages()` stores both user and assistant messages
|
|
- **Dialectic Queries**: `peer.chat()` enables querying conversation history
|
|
- **Command System**: Native Telegram command support with `/start` and `/dialectic`
|
|
- **Message Splitting**: Automatic handling of Telegram's character limits
|
|
- **Helper Functions**: Clean code organization with focused helper functions
|
|
|
|
This approach provides a clean, maintainable structure for building Telegram bots with conversational memory and context management across both private conversations and group chats.
|