December 19, 2024 Go Telegram Gemini Ai Aws Lambda Bot
In this article, we’ll explore how to build an intelligent Telegram bot using Go that acts as a proxy between users and Google’s Gemini API. The bot will handle two primary functions: answering user messages and generating images. While this mechanism can be significantly extended with additional capabilities like voice and video generation, we’ll focus on these two request types for simplicity.
The bot uses an intelligent routing mechanism where the first request to the AI acts as a router to determine the type of user request. The AI is instructed to either return a normal conversational response or a structured output that can be forwarded to a specialized model for image generation.
graph TD
A[User Message] --> B[Telegram Bot Handler]
B --> C[Router AI Request]
C --> D{Request Type?}
D -->|Text Response| E[Gemini Chat API]
D -->|Image Generation| F[Gemini Image Generation API]
E --> G[Send Text to User]
F --> H[Send Image to User]
Let’s start by setting up the basic project structure:
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"google.golang.org/api/option"
"google.golang.org/api/generativeai"
)
First, we need to configure the bot with API keys:
type Config struct {
TelegramToken string
GeminiAPIKey string
}
func loadConfig() *Config {
return &Config{
TelegramToken: os.Getenv("TELEGRAM_TOKEN"),
GeminiAPIKey: os.Getenv("GEMINI_API_KEY"),
}
}
The router uses Gemini to analyze the user’s message and determine the appropriate action:
type RequestType string
const (
RequestTypeText RequestType = "text"
RequestTypeImage RequestType = "image"
)
type RouterResponse struct {
Type RequestType `json:"type"`
Prompt string `json:"prompt,omitempty"`
Message string `json:"message,omitempty"`
}
The router prompt should be concise and clear:
You are a request router for a Telegram bot. Analyze user messages and respond with JSON:
- If the user wants an image generated, return: {"type":"image","prompt":"description"}
- Otherwise, return: {"type":"text","message":"your response"}
Here’s the router implementation:
func routeRequest(ctx context.Context, client *generativeai.Client,
userMsg string) RouterResponse {
model := client.GenerativeModel("gemini-3-pro-preview")
model.SystemInstruction = "Route requests: return JSON with type and prompt/message"
resp, _ := model.GenerateContent(ctx, userMsg)
var routerResp RouterResponse
json.Unmarshal([]byte(resp.Text), &routerResp)
return routerResp
}
Here’s how we handle incoming Telegram messages:
func handleMessage(ctx context.Context, bot *tgbotapi.BotAPI,
client *generativeai.Client, update tgbotapi.Update) {
userMsg := update.Message.Text
routerResp := routeRequest(ctx, client, userMsg)
switch routerResp.Type {
case RequestTypeImage:
generateAndSendImage(ctx, bot, client, update, routerResp.Prompt)
case RequestTypeText:
sendTextResponse(bot, update, routerResp.Message)
}
}
For regular text responses, we use Gemini’s chat API:
func sendTextResponse(bot *tgbotapi.BotAPI,
update tgbotapi.Update, message string) {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, message)
msg.ReplyToMessageID = update.Message.MessageID
bot.Send(msg)
}
For image generation, we call Gemini’s image generation endpoint:
func generateAndSendImage(ctx context.Context, bot *tgbotapi.BotAPI,
client *generativeai.Client,
update tgbotapi.Update, prompt string) {
model := client.GenerativeModel("gemini-2.5-flash-image")
resp, err := model.GenerateContent(ctx, prompt)
if err != nil {
sendTextResponse(bot, update, "Error generating image")
return
}
photo := tgbotapi.NewPhoto(update.Message.Chat.ID,
tgbotapi.FileBytes{
Name: "image.png",
Bytes: resp.ImageData,
})
bot.Send(photo)
}
The main function sets up the bot and starts listening:
func main() {
cfg := loadConfig()
bot, _ := tgbotapi.NewBotAPI(cfg.TelegramToken)
client, _ := generativeai.NewClient(context.Background(),
option.WithAPIKey(cfg.GeminiAPIKey))
u := tgbotapi.NewUpdate(0)
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil {
go handleMessage(context.Background(), bot, client, update)
}
}
}
Here are concise system prompts for different scenarios:
Router Prompt:
Route user requests: return JSON with "type":"image" and "prompt" for image requests,
or "type":"text" with "message" for conversations.
Chat Assistant Prompt:
You are a helpful assistant. Provide clear, concise answers to user questions.
Image Generation Prompt Template:
Generate a high-quality image based on: {user_prompt}
This architecture can be easily extended to support additional capabilities:
RequestTypeVoice and use text-to-speech APIsRequestTypeVideo and integrate video generation servicesThe router pattern allows you to add new request types without modifying the core message handling logic.
For a bot to understand context and maintain coherent conversations, it needs access to message history. Since Lambda functions are stateless, we need external storage for conversation history. AWS S3 is an excellent choice for this purpose, as it integrates seamlessly with Lambda and provides durable, scalable storage.
AI models like Gemini perform significantly better when they have access to conversation context. Without history, each message is treated independently, making it impossible for the bot to:
We can store each conversation as a JSON file in S3, using the chat ID as the key. This approach allows us to:
Here’s how to implement conversation storage:
func saveConversation(ctx context.Context, s3Client *s3.Client,
chatID int64, messages []Message) error {
key := fmt.Sprintf("conversations/%d.json", chatID)
data, _ := json.Marshal(messages)
_, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String("bot-conversations"),
Key: aws.String(key),
Body: bytes.NewReader(data),
})
return err
}
Loading conversation history:
func loadConversation(ctx context.Context, s3Client *s3.Client,
chatID int64) ([]Message, error) {
key := fmt.Sprintf("conversations/%d.json", chatID)
resp, err := s3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String("bot-conversations"),
Key: aws.String(key),
})
if err != nil {
return []Message{}, nil // Return empty if not found
}
defer resp.Body.Close()
var messages []Message
json.NewDecoder(resp.Body).Decode(&messages)
return messages, nil
}
When processing a message, load the history, append the new message, send the full context to Gemini, and save the updated conversation back to S3. This ensures the bot maintains context across multiple interactions while leveraging Lambda’s stateless architecture.
Deploying this bot as an AWS Lambda function provides several significant advantages over traditional server-based deployments. Let’s explore why Lambda is particularly well-suited for Telegram bots:
1. Cost Efficiency
2. Automatic Scaling
3. Zero Infrastructure Management
4. High Availability and Reliability
5. Perfect Match for Webhook Architecture
6. Development and Deployment Benefits
For a Telegram bot that may have variable traffic patterns and doesn’t need constant uptime, Lambda provides an optimal balance of cost, performance, and operational simplicity.
Detailed instructions for setting up AWS CLI, creating Lambda function, configuring Function URL, and automating deployment are covered in a separate article: Deploying Telegram Bot to AWS Lambda with Function URL.
In that article you’ll find:
This architecture provides a flexible foundation for building AI-powered Telegram bots. The router pattern allows easy extension to new capabilities while keeping the codebase maintainable. Deploying on AWS Lambda ensures scalability and cost-effectiveness for production use.
This article is a continuation of “Building an AI Telegram Bot with Go, Gemini API, and AWS Lambda” and contains detailed instructions for setting up and deploying a Telegram bot to AWS Lambda using Function URL.
Read More → Go Telegram Aws Lambda Deploy Function-Url Cli