Skip to content

Telegram Reply Context

Research into passing quoted/replied-to message context to the LLM when Caesar replies to a message from chat history.


Problem

When Caesar swipe-replies to a previous message in Telegram and types a new message, the bot only reads the new text. The quoted message is silently discarded, so the LLM has no idea what "this" or "that" refers to.

Example:

[Previous bot message]: "Here are 3 options for the deployment strategy..."

Caesar replies → "elaborate on option 2"

Legatus receives only "elaborate on option 2" — with no context about which message or which options. The response will be confused or generic.

Current Architecture

Message Flow

Telegram Update
  → _handle_message() in bot.py:115
    → _handle_request() in bot.py:160
      → text = update.message.text           ← only new text
      → run_legatus_request(..., text, ...)
        → legatus.handle_message(text, ...)
          → _query_legatus(text, ...)
            → client.query(status_xml + text)   ← no reply context

What Telegram Provides on a Reply

The update.message object contains:

FieldTypeContent
textstrCaesar's new message text
reply_to_messageMessage | NoneThe quoted message (None if not a reply)
reply_to_message.textstrText of the quoted message
reply_to_message.from_user.idintWho sent the quoted message
reply_to_message.from_user.is_botboolWhether sender was the bot
reply_to_message.message_idintID of the quoted message
reply_to_message.datedatetimeWhen the quoted message was sent

Entry Points That Send Text to the LLM

HandlerFileLineUses run_legatus_request
_handle_message_handle_requestbot.py115, 160Yes
_handle_create_handle_requestbot.py156, 160Yes
handle_edictcommands.py199Yes

All three pass update.message.text (or text from the update) directly. None extract reply_to_message.

Design Considerations

What Context to Include

Option A — Quoted text only (minimal):

[Replying to: "the quoted message text"]

Caesar's new message

Option B — Quoted text with sender (recommended):

[Replying to legatus: "the quoted message text"]

Caesar's new message

Option C — XML context block (consistent with praetorium format):

xml
<reply_context sender="legatus" message_id="12345">
the quoted message text
</reply_context>

Caesar's new message

Recommendation: Option C (XML). It matches the existing <praetorium> and <centurio_status> XML conventions used in system prompts and message injection. The LLM is already primed to understand XML context blocks.

Where to Inject

The reply context should be prepended to the user text before it enters handle_message() in the legatus. This means the extraction happens in _handle_request() in bot.py — the single gateway between Telegram and the LLM.

_handle_request():
  text = update.message.text
  reply = update.message.reply_to_message
  if reply and reply.text:
    context = build_reply_context(reply)
    text = f"{context}\n\n{text}"
  → run_legatus_request(..., text, ...)

This approach:

  • Keeps handle_message() / _query_legatus() unchanged
  • Works for both free-form messages and /create
  • The /edict handler in commands.py would need the same treatment

Truncation

Quoted messages can be very long (up to 4096 chars from a previous bot response). Including the full text would waste tokens.

Proposal: Truncate quoted text to ~500 characters with an ellipsis indicator:

xml
<reply_context sender="legatus" truncated="true">
first 500 characters of the quoted message...
</reply_context>

This provides enough context for the LLM to understand the reference without consuming excessive tokens. The LLM also has praetorium history available via the get_history tool if it needs the full conversation.

Sender Attribution

Determine the sender name from reply_to_message.from_user:

  • If from_user.is_bot is True → sender is "legatus" (or could be a centurio, but Telegram only shows the bot user)
  • If from_user.id matches Caesar's ID → sender is "caesar"
  • Otherwise → sender is "unknown"

Limitation: When legatus responds on behalf of a centurio (with attribution header), Telegram still shows the bot as the sender. We could parse the attribution header (⚔️ vorenus — ...) to extract the centurio name, but this is fragile. For v1, using "legatus" for all bot messages is sufficient.

Security

  • Quoted text is untrusted (it could be an edited message, or a forwarded message from outside).
  • Must escape with xml.sax.saxutils.escape() before injecting into the XML block.
  • The reply_to_message field itself is trustworthy (populated by Telegram, not user-editable in the API), but the content could contain anything.

OTP Interaction

The try_verify_otp() check in _handle_message() runs before the reply context logic. OTP messages are plain 6-digit codes, never replies. No conflict.

Implementation Plan

Changes Required

legio/telegram/bot.py:

  1. New function _build_reply_context(reply_message) -> str | None:

    • Extract reply_to_message.text
    • Determine sender (legatus / caesar / unknown)
    • Truncate to 500 chars if needed
    • Build XML block with escape()
    • Return None if no reply or empty text
  2. Modify _handle_request():

    • After text = update.message.text, check for reply context
    • Prepend context XML if present

legio/telegram/commands.py:

  1. Modify handle_edict():
    • Same reply context extraction before passing to run_legatus_request()
    • Or: refactor extraction into a shared utility in utils.py

Better approach — extract in utils.py:

  1. New function in utils.py: extract_reply_context(update) -> str:

    • Centralized extraction, used by both bot.py and commands.py
    • Returns empty string if no reply context
  2. Modify run_legatus_request():

    • Accept update (already does) and extract reply context there
    • Prepend to text parameter before passing to handle_message()
    • Single integration point — all callers benefit automatically

Tests Required

  • Reply with quoted text → context included in LLM input
  • Reply without text (e.g., reply to a photo) → no context, normal flow
  • No reply (normal message) → no context, normal flow
  • Long quoted text → truncated to 500 chars
  • Quoted text with XML special characters → properly escaped
  • OTP reply → not affected by reply context logic
  • Sender detection: bot message → legatus, Caesar message → caesar

Estimated Scope

  • ~30 lines new code in utils.py (extraction function)
  • ~5 lines modified in run_legatus_request() or _handle_request()
  • ~100 lines of tests
  • No new dependencies

Built with Roman discipline.