feat: add SMS (Telnyx) platform adapter
Implement SMS as a first-class messaging platform following ADDING_A_PLATFORM.md checklist. All 16 integration points covered: - gateway/platforms/sms.py: Core adapter with aiohttp webhook server, Telnyx REST API send, markdown stripping, 1600-char chunking, echo loop prevention, multi-number reply-from tracking - gateway/config.py: Platform.SMS enum + env override block - gateway/run.py: Adapter factory + auth maps (SMS_ALLOWED_USERS, SMS_ALLOW_ALL_USERS) - toolsets.py: hermes-sms toolset + included in hermes-gateway - cron/scheduler.py: SMS in platform_map for cron delivery - tools/send_message_tool.py: SMS routing + _send_sms() standalone sender - tools/cronjob_tools.py: 'sms' in deliver description - gateway/channel_directory.py: SMS in session-based discovery - agent/prompt_builder.py: SMS platform hint (plain text, concise) - hermes_cli/status.py: SMS in platforms status display - hermes_cli/gateway.py: SMS in setup wizard with Telnyx instructions - pyproject.toml: sms optional dependency group (aiohttp>=3.9.0) - tests/gateway/test_sms.py: Unit tests for config, format, truncate, echo prevention, requirements, toolset integration Co-authored-by: sunsakis <teo@sunsakis.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
71c6b1ee99
commit
ef67037f8e
13 changed files with 645 additions and 4 deletions
|
|
@ -125,6 +125,7 @@ def _handle_send(args):
|
|||
"whatsapp": Platform.WHATSAPP,
|
||||
"signal": Platform.SIGNAL,
|
||||
"email": Platform.EMAIL,
|
||||
"sms": Platform.SMS,
|
||||
}
|
||||
platform = platform_map.get(platform_name)
|
||||
if not platform:
|
||||
|
|
@ -334,6 +335,8 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
|
|||
result = await _send_signal(pconfig.extra, chat_id, chunk)
|
||||
elif platform == Platform.EMAIL:
|
||||
result = await _send_email(pconfig.extra, chat_id, chunk)
|
||||
elif platform == Platform.SMS:
|
||||
result = await _send_sms(pconfig.api_key, chat_id, chunk)
|
||||
else:
|
||||
result = {"error": f"Direct sending not yet implemented for {platform.value}"}
|
||||
|
||||
|
|
@ -562,6 +565,54 @@ async def _send_email(extra, chat_id, message):
|
|||
return {"error": f"Email send failed: {e}"}
|
||||
|
||||
|
||||
async def _send_sms(api_key, chat_id, message):
|
||||
"""Send via Telnyx SMS REST API (one-shot, no persistent connection needed)."""
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
return {"error": "aiohttp not installed. Run: pip install aiohttp"}
|
||||
try:
|
||||
from_number = os.getenv("TELNYX_FROM_NUMBERS", "").split(",")[0].strip()
|
||||
if not from_number:
|
||||
return {"error": "TELNYX_FROM_NUMBERS not configured"}
|
||||
if not api_key:
|
||||
api_key = os.getenv("TELNYX_API_KEY", "")
|
||||
if not api_key:
|
||||
return {"error": "TELNYX_API_KEY not configured"}
|
||||
|
||||
# Strip markdown for SMS
|
||||
text = re.sub(r"\*\*(.+?)\*\*", r"\1", message, flags=re.DOTALL)
|
||||
text = re.sub(r"\*(.+?)\*", r"\1", text, flags=re.DOTALL)
|
||||
text = re.sub(r"```[a-z]*\n?", "", text)
|
||||
text = re.sub(r"`(.+?)`", r"\1", text)
|
||||
text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
|
||||
text = text.strip()
|
||||
|
||||
# Chunk to 1600 chars
|
||||
chunks = [text[i:i+1600] for i in range(0, len(text), 1600)] if len(text) > 1600 else [text]
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
message_ids = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for chunk in chunks:
|
||||
payload = {"from": from_number, "to": chat_id, "text": chunk}
|
||||
async with session.post(
|
||||
"https://api.telnyx.com/v2/messages",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
) as resp:
|
||||
body = await resp.json()
|
||||
if resp.status >= 400:
|
||||
return {"error": f"Telnyx API error ({resp.status}): {body}"}
|
||||
message_ids.append(body.get("data", {}).get("id", ""))
|
||||
return {"success": True, "platform": "sms", "chat_id": chat_id, "message_ids": message_ids}
|
||||
except Exception as e:
|
||||
return {"error": f"SMS send failed: {e}"}
|
||||
|
||||
|
||||
def _check_send_message():
|
||||
"""Gate send_message on gateway running (always available on messaging platforms)."""
|
||||
platform = os.getenv("HERMES_SESSION_PLATFORM", "")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue