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.