All Projects
·6 min read

Reading DNA Bot: The AI That Decodes Your Reading Taste and Finds Your Next Favorite Book

Telegram bot powered by Google ADK 2.1 and Gemini 2.5 Flash that extracts your Reading DNA from books you love, then recommends your perfect next read with a match score, book cover, and follow-up options. Built with FastAPI, deployed on Google Cloud Run.

PythonGoogle ADK 2.1Gemini 2.5 FlashVertex AIFastAPITelegram Bot APIGoogle Cloud RunAI AgentsGoogle Books APIWebhooks

What it does

Tell the bot three books you loved. It extracts your Reading DNA, a four-part fingerprint of your taste. It asks two quick questions about your reading situation. Then it recommends a book with a percentage match score, a cover image pulled from Google Books, and four follow-up options: where to buy, two backup picks, a 30-second pitch, or audiobook details.

The whole conversation runs inside Telegram through tap-able buttons. You never type more than one sentence at a time.

Your Reading DNA has four dimensions:

  • Protagonist Type — Brilliant Maverick, Relatable Everyman, Complex Anti-Hero, or Heroic Leader
  • Pacing — Lightning Fast, Steady Momentum, or Slow Burn
  • Tone — Optimistic and Uplifting, Dark and Intense, Witty and Playful, or Thoughtful and Reflective
  • Story Driver — Plot-driven, Character-driven, or Ideas-driven

The bot remembers your DNA for the entire session. Once extracted, it skips straight to recommendations for every follow-up request.

How it works, from message to response

You type a message in Telegram and tap Send
        |
        v
Telegram's servers
        Checks its database: "which webhook URL is registered for this bot?"
        Answer: https://reading-dna-bot-xxxx.us-central1.run.app/webhook
        |
        v  HTTP POST with your message as JSON
Google Cloud Run (our FastAPI server, running 24/7)
        |
        |-- Sends "typing..." indicator back to Telegram
        |
        |-- Calls agent.send_message(chat_id, user_text)
        |           |
        |           v
        |     Google ADK 2.1 Runner
        |           Looks up this user's session (full conversation history)
        |           Builds: system instruction + history + new message
        |           |
        |           v
        |     Vertex AI (Gemini 2.5 Flash)
        |           Reads the context, follows the Reading DNA rules
        |           Returns structured text (QUESTION: or RECOMMENDATION: block)
        |           ADK saves this turn to session memory
        |           |
        |           v
        |     Response text back in FastAPI
        |
        |-- Parses the response
        |       QUESTION: with A/B/C/D  -->  sends tap-able buttons
        |       RECOMMENDATION: block   -->  fetches cover from Google Books API
        |                               -->  sends photo + match score + buttons
        |       Plain text              -->  sends text message
        |
        v
Telegram delivers the message to your phone

When you tap a button:

You tap "C) 30-second pitch for this book"
        |
        v
Telegram sends a callback_query (not a message) to Cloud Run
        |
        |-- Acknowledges the tap (removes loading spinner)
        |-- Updates the button itself to show: "✅ C) 30-second pitch"
        |   (all other buttons stay active so you can tap another option next)
        |-- Sends button text to the agent exactly like a typed message
        |
        v
Agent responds with the pitch, Cloud Run sends it to Telegram

The three parts of Google ADK 2.1

This project uses ADK 2.1, released at Google I/O 2026. It has three core pieces.

LlmAgent is the brain. You give it a name, a model string, and a system instruction that defines its entire personality and rules. The agent does not write code. It reads the instruction and follows it when generating every response.

InMemorySessionService is the memory. Every message in a conversation is stored per user. When you send your third message, ADK automatically includes your previous two messages in the context sent to Gemini, so the bot remembers your Reading DNA without you repeating it.

Runner is the engine. It connects the agent and the session service, runs each conversation turn, and yields events. One event type is is_final_response(), which marks the complete output to display.

reading_dna_agent = LlmAgent(
    name="reading_dna_bot",
    model="gemini-2.5-flash",
    instruction=SYSTEM_INSTRUCTION,
)

session_service = InMemorySessionService()

runner = Runner(
    agent=reading_dna_agent,
    session_service=session_service,
    app_name="reading_dna_bot",
)

async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=message):
    if event.is_final_response():
        response_text = event.content.parts[0].text

What the agent outputs and what the code does with it

The agent outputs plain text. The system instruction forces a strict format for structured responses.

A question looks like this in the raw agent output:

QUESTION: What's your reading situation right now?
A) Relaxed weekend morning
B) Commute or travel
C) Before bed
D) Break from stress

A recommendation looks like this:

RECOMMENDATION:
TITLE: Project Hail Mary
AUTHOR: Andy Weir
MATCH_SCORE: 91%
DNA_MATCH: Two sentences explaining why this matches the user's Reading DNA.
CONTEXT_MATCH: One sentence on why it fits their situation and energy right now.

The code in main.py detects which format arrived and decides how to display it. If it sees QUESTION: with A/B/C/D lines, it creates tap-able buttons. If it sees RECOMMENDATION:, it extracts the title and author, calls Google Books API for the cover image, and sends it as a Telegram photo with a caption. The intelligence lives entirely in the agent. The code handles only the display layer.

Webhook registration: one curl command, permanent effect

Telegram uses a webhook model. Instead of our server constantly asking Telegram "any new messages?", we register our Cloud Run URL once:

https://api.telegram.org/bot{TOKEN}/setWebhook?url=https://our-url/webhook

Telegram stores this URL in their database, linked to our bot token. From that moment, every message any user sends to the bot triggers an automatic HTTP POST from Telegram to our URL. One API call, permanent. No polling, no background jobs, no timers.

Tech Stack

| Layer | Technology | |---|---| | AI Agent Framework | Google ADK 2.1 | | AI Model | Gemini 2.5 Flash (via Vertex AI) | | Web Server | FastAPI (Python, async) | | Telegram Integration | Telegram Bot API (webhooks, inline keyboards, photo messages) | | Book Covers | Google Books API | | Deployment | Google Cloud Run (serverless, scales to zero) | | Session Memory | InMemorySessionService (ADK 2.1 built-in) |

What I Learned

  1. The system instruction is the hardest part. Getting Gemini to output RECOMMENDATION: and QUESTION: blocks consistently, with exact field names on exact lines, required significant iteration. The model is capable. The instruction has to be precise.

  2. ADK 2.1 is designed for embedding, not just the CLI. ADK 1.x worked well with adk web . but embedding it in a custom server required workarounds. ADK 2.1's Runner class is purpose-built for this pattern.

  3. Telegram has two event types and handling both matters. A typed message and a button tap arrive as completely different JSON shapes at the webhook. Missing this distinction means buttons silently do nothing.

  4. Webhook registration is genuinely one command. Every Telegram bot in production, from small side projects to large commercial bots, is set up with the same single setWebhook API call. Telegram handles all the routing from that point.

  5. Cloud Run scales to zero. When no one is using the bot, the container shuts down and costs nothing. The first message after idle takes a second or two longer (cold start) but every subsequent message is instant. For a Telegram bot with occasional traffic, this is the right deployment model.