feat: add Telegram file attachment support (inbound + outbound)
Inbound:
- tg-ingest detects document/photo/audio/video/voice attachments
- Downloads files via Telegram Bot API (getFile + download)
- Inlines small text files (<50KB) directly in the prompt
- Stores binary/large files to S3 (attachments/{chat_id}/{update_id}/{filename})
- agent-runner appends file context to the AgentCore prompt
Outbound:
- New send_file tool for the agent to send documents back to users
- TelegramAdapter.send_document uses multipart/form-data POST
- CDK grants tg-ingest S3 write access and passes bucket name env var
This commit is contained in:
@@ -63,6 +63,43 @@ class TelegramAdapter:
|
||||
import traceback
|
||||
print(f'[telegram] send_typing failed: {e}\n{traceback.format_exc()}')
|
||||
|
||||
def send_document(self, file_bytes: bytes, filename: str, caption: str = '') -> str:
|
||||
"""Send a file as a Telegram document using multipart/form-data. Returns message_id."""
|
||||
import io
|
||||
token = self._get_token()
|
||||
url = f'https://api.telegram.org/bot{token}/sendDocument'
|
||||
|
||||
boundary = '----AgentClawBoundary'
|
||||
body = io.BytesIO()
|
||||
|
||||
def add_field(name: str, value: str):
|
||||
body.write(f'--{boundary}\r\n'.encode())
|
||||
body.write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
|
||||
body.write(f'{value}\r\n'.encode())
|
||||
|
||||
def add_file(name: str, fname: str, data: bytes):
|
||||
body.write(f'--{boundary}\r\n'.encode())
|
||||
body.write(f'Content-Disposition: form-data; name="{name}"; filename="{fname}"\r\n'.encode())
|
||||
body.write(b'Content-Type: application/octet-stream\r\n\r\n')
|
||||
body.write(data)
|
||||
body.write(b'\r\n')
|
||||
|
||||
add_field('chat_id', self.chat_id)
|
||||
if self.thread_id is not None:
|
||||
add_field('message_thread_id', str(self.thread_id))
|
||||
if caption:
|
||||
add_field('caption', caption)
|
||||
add_file('document', filename, file_bytes)
|
||||
body.write(f'--{boundary}--\r\n'.encode())
|
||||
|
||||
req = urllib.request.Request(
|
||||
url, data=body.getvalue(),
|
||||
headers={'Content-Type': f'multipart/form-data; boundary={boundary}'},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
result = json.loads(resp.read())
|
||||
return str(result.get('result', {}).get('message_id', ''))
|
||||
|
||||
def edit(self, message_id: str, text: str) -> None:
|
||||
"""Edit an existing message in-place."""
|
||||
try:
|
||||
|
||||
@@ -17,6 +17,7 @@ from tools.scheduler import schedule_reminder, list_reminders, cancel_reminder
|
||||
import tools.scheduler as _scheduler_module
|
||||
from tools.home_assistant import home_assistant, set_ha_config
|
||||
from tools.google_workspace import list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message
|
||||
from tools.send_file import send_file as _send_file_impl
|
||||
import tools.google_workspace as _gws
|
||||
import httpx
|
||||
import botocore.auth
|
||||
@@ -74,6 +75,19 @@ def web_search(query: str) -> str:
|
||||
return web_tools.brave_search(query)
|
||||
|
||||
|
||||
@tool
|
||||
def send_file(file_content: str, filename: str, caption: str = '') -> str:
|
||||
"""Send a file to the user as a Telegram document attachment.
|
||||
Use this when you need to send code, data, or any text content as a downloadable file.
|
||||
|
||||
Args:
|
||||
file_content: The text content of the file to send.
|
||||
filename: The filename with extension (e.g. 'report.txt', 'data.csv', 'script.py').
|
||||
caption: Optional caption to display with the file.
|
||||
"""
|
||||
return _send_file_impl(file_content, filename, caption)
|
||||
|
||||
|
||||
@tool
|
||||
def web_fetch(url: str) -> str:
|
||||
"""Fetch and extract readable text content from a URL."""
|
||||
@@ -360,7 +374,7 @@ async def main(payload: dict, context):
|
||||
home_assistant, connect_google_account, list_google_accounts, remove_google_account,
|
||||
manage_service, schedule_reminder, list_reminders, cancel_reminder,
|
||||
list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message,
|
||||
run_code]
|
||||
run_code, send_file]
|
||||
|
||||
agent = Agent(
|
||||
model=model,
|
||||
|
||||
20
agentclaw/app/agent_claw_main/tools/send_file.py
Normal file
20
agentclaw/app/agent_claw_main/tools/send_file.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Send file tool — sends documents to the user via Telegram."""
|
||||
from tools import messaging
|
||||
|
||||
|
||||
def send_file(file_content: str, filename: str, caption: str = '') -> str:
|
||||
"""Send a file to the user as a Telegram document attachment.
|
||||
|
||||
Args:
|
||||
file_content: The text content of the file to send.
|
||||
filename: The filename (e.g. 'report.txt', 'data.csv').
|
||||
caption: Optional caption to display with the file.
|
||||
"""
|
||||
adapter = messaging._adapter
|
||||
if adapter is None:
|
||||
return 'No channel adapter configured.'
|
||||
if not hasattr(adapter, 'send_document'):
|
||||
return 'Channel adapter does not support file sending.'
|
||||
file_bytes = file_content.encode('utf-8')
|
||||
msg_id = adapter.send_document(file_bytes, filename, caption)
|
||||
return f'File "{filename}" sent (id={msg_id})' if msg_id else f'File "{filename}" sent'
|
||||
Reference in New Issue
Block a user