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 contextWhat Telegram Provides on a Reply
The update.message object contains:
| Field | Type | Content |
|---|---|---|
text | str | Caesar's new message text |
reply_to_message | Message | None | The quoted message (None if not a reply) |
reply_to_message.text | str | Text of the quoted message |
reply_to_message.from_user.id | int | Who sent the quoted message |
reply_to_message.from_user.is_bot | bool | Whether sender was the bot |
reply_to_message.message_id | int | ID of the quoted message |
reply_to_message.date | datetime | When the quoted message was sent |
Entry Points That Send Text to the LLM
| Handler | File | Line | Uses run_legatus_request |
|---|---|---|---|
_handle_message → _handle_request | bot.py | 115, 160 | Yes |
_handle_create → _handle_request | bot.py | 156, 160 | Yes |
handle_edict | commands.py | 199 | Yes |
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 messageOption B — Quoted text with sender (recommended):
[Replying to legatus: "the quoted message text"]
Caesar's new messageOption C — XML context block (consistent with praetorium format):
<reply_context sender="legatus" message_id="12345">
the quoted message text
</reply_context>
Caesar's new messageRecommendation: 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
/edicthandler incommands.pywould 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:
<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_botis True → sender is"legatus"(or could be a centurio, but Telegram only shows the bot user) - If
from_user.idmatches 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_messagefield 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:
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
- Extract
Modify
_handle_request():- After
text = update.message.text, check for reply context - Prepend context XML if present
- After
legio/telegram/commands.py:
- Modify
handle_edict():- Same reply context extraction before passing to
run_legatus_request() - Or: refactor extraction into a shared utility in
utils.py
- Same reply context extraction before passing to
Better approach — extract in utils.py:
New function in
utils.py:extract_reply_context(update) -> str:- Centralized extraction, used by both
bot.pyandcommands.py - Returns empty string if no reply context
- Centralized extraction, used by both
Modify
run_legatus_request():- Accept
update(already does) and extract reply context there - Prepend to
textparameter before passing tohandle_message() - Single integration point — all callers benefit automatically
- Accept
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