From 863f189cde33ef1e20e1b2ae8210c843b3ca43e9 Mon Sep 17 00:00:00 2001 From: sarveshpatil-plivo Date: Thu, 23 Jul 2026 18:13:37 +0530 Subject: [PATCH 1/2] feat: add Plivo SMS skill Adds a plivo-sms skill under skills/ so an agent can send an SMS through Plivo. It follows the same layout as the existing gmail-email skill, a SKILL.md with YAML frontmatter plus a script in scripts/. The script sends an SMS through the Plivo Messages API. It reads PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN from the environment or a .env file in the skill directory, and takes from, to and text as arguments. It uses only the Python standard library so there is nothing extra to install. Verified against a live Plivo account, the message was delivered. --- skills/plivo-sms/SKILL.md | 50 +++++++++++++ skills/plivo-sms/scripts/send_sms.py | 104 +++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 skills/plivo-sms/SKILL.md create mode 100644 skills/plivo-sms/scripts/send_sms.py diff --git a/skills/plivo-sms/SKILL.md b/skills/plivo-sms/SKILL.md new file mode 100644 index 0000000..e3de4fc --- /dev/null +++ b/skills/plivo-sms/SKILL.md @@ -0,0 +1,50 @@ +--- +name: plivo-sms +description: Send SMS messages via the Plivo Messages API using Auth ID/Auth Token authentication. +--- + +# Plivo SMS Skill + +Send SMS messages via Plivo. + +## Setup + +1. **Create a Plivo account** and open the console at https://cx.plivo.com +2. **Get your credentials**: + - Copy your **Auth ID** and **Auth Token** from the dashboard + - Rent a Plivo phone number (or use an approved sender ID) to send from + +3. **Configure credentials**: + ```bash + export PLIVO_AUTH_ID="your-auth-id" + export PLIVO_AUTH_TOKEN="your-auth-token" + ``` + + Or create a `.env` file in the skill directory: + ``` + PLIVO_AUTH_ID=your-auth-id + PLIVO_AUTH_TOKEN=your-auth-token + ``` + +## Usage + +```bash +python3 scripts/send_sms.py \ + --from "+14150000002" \ + --to "+14150000001" \ + --text "Message body text" +``` + +Send to multiple recipients by joining numbers with `<`: + +```bash +python3 scripts/send_sms.py \ + --from "+14150000002" \ + --to "+14150000001<+14160000003" \ + --text "Message body text" +``` + +## Requirements + +- Python 3.6+ +- No additional packages needed (uses stdlib urllib) diff --git a/skills/plivo-sms/scripts/send_sms.py b/skills/plivo-sms/scripts/send_sms.py new file mode 100644 index 0000000..ee07c04 --- /dev/null +++ b/skills/plivo-sms/scripts/send_sms.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Send SMS via Plivo Messages API +""" +import argparse +import base64 +import json +import os +import sys +import urllib.error +import urllib.request +from pathlib import Path + +def load_env(): + """Load environment variables from .env file if it exists""" + env_file = Path(__file__).parent.parent / '.env' + if env_file.exists(): + with open(env_file) as f: + for line in f: + line = line.strip() + if not line or line.startswith('#') or '=' not in line: + continue + key, value = line.split('=', 1) + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): + value = value[1:-1] + os.environ[key.strip()] = value + +def send_sms(from_number, to, text, auth_id=None, auth_token=None): + """Send SMS via Plivo Messages API""" + + # Get credentials + auth_id = auth_id or os.getenv('PLIVO_AUTH_ID') + auth_token = auth_token or os.getenv('PLIVO_AUTH_TOKEN') + + if not auth_id or not auth_token: + print("ERROR: Plivo credentials not found!", file=sys.stderr) + print("\nPlease set credentials using one of these methods:", file=sys.stderr) + print("\n1. Environment variables:", file=sys.stderr) + print(" export PLIVO_AUTH_ID='your-auth-id'", file=sys.stderr) + print(" export PLIVO_AUTH_TOKEN='your-auth-token'", file=sys.stderr) + print("\n2. Create a .env file in skills/plivo-sms/:", file=sys.stderr) + print(" PLIVO_AUTH_ID=your-auth-id", file=sys.stderr) + print(" PLIVO_AUTH_TOKEN=your-auth-token", file=sys.stderr) + print("\nFind both in the Plivo console at https://cx.plivo.com", file=sys.stderr) + sys.exit(1) + + # Build request + url = f"https://api.plivo.com/v1/Account/{auth_id}/Message/" + payload = json.dumps({"src": from_number, "dst": to, "text": text, "type": "sms"}).encode() + credentials = base64.b64encode(f"{auth_id}:{auth_token}".encode()).decode() + + request = urllib.request.Request(url, data=payload, method='POST') + request.add_header('Authorization', f"Basic {credentials}") + request.add_header('Content-Type', 'application/json') + + # Send SMS + try: + print(f"Sending SMS to {to}...", file=sys.stderr) + with urllib.request.urlopen(request) as response: + body = json.loads(response.read().decode()) + + uuids = body.get('message_uuid', []) + if not uuids: + print(f"ERROR: Plivo accepted the request but returned no message_uuid: {body}", file=sys.stderr) + return False + print(f"✓ SMS queued successfully (message_uuid: {', '.join(uuids)})") + return True + + except urllib.error.HTTPError as e: + error_body = e.read().decode() + print(f"ERROR: Plivo returned HTTP {e.code}", file=sys.stderr) + print(error_body, file=sys.stderr) + return False + except Exception as e: + print(f"ERROR: Failed to send SMS: {e}", file=sys.stderr) + return False + +def main(): + parser = argparse.ArgumentParser(description='Send SMS via Plivo Messages API') + parser.add_argument('--to', required=True, help='Recipient number in E.164 (join multiple with "<")') + parser.add_argument('--from', dest='from_number', required=True, help='Sender number or sender ID') + parser.add_argument('--text', required=True, help='Message body') + parser.add_argument('--auth-id', dest='auth_id', help='Plivo Auth ID (default: PLIVO_AUTH_ID env var)') + parser.add_argument('--auth-token', dest='auth_token', help='Plivo Auth Token (default: PLIVO_AUTH_TOKEN env var)') + + args = parser.parse_args() + + # Load .env file if exists + load_env() + + # Send SMS + success = send_sms( + from_number=args.from_number, + to=args.to, + text=args.text, + auth_id=args.auth_id, + auth_token=args.auth_token + ) + + sys.exit(0 if success else 1) + +if __name__ == '__main__': + main() From 3171324c88919bc6180cf6412a7a189caf5da43c Mon Sep 17 00:00:00 2001 From: sarveshpatil-plivo Date: Thu, 23 Jul 2026 18:20:57 +0530 Subject: [PATCH 2/2] feat: add Plivo voice skill Adds a plivo-voice skill that places an outbound call through the Plivo Call API, completing the SMS and voice pair from the issue. Same layout as plivo-sms and gmail-email, a SKILL.md plus a script under scripts/. Reads PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN from the environment or a .env file, takes from, to and an answer URL, and uses only the Python standard library. When the call connects Plivo runs the XML at the answer URL, which can speak a message. --- skills/plivo-voice/SKILL.md | 49 ++++++++++ skills/plivo-voice/scripts/make_call.py | 113 ++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 skills/plivo-voice/SKILL.md create mode 100644 skills/plivo-voice/scripts/make_call.py diff --git a/skills/plivo-voice/SKILL.md b/skills/plivo-voice/SKILL.md new file mode 100644 index 0000000..b0296cf --- /dev/null +++ b/skills/plivo-voice/SKILL.md @@ -0,0 +1,49 @@ +--- +name: plivo-voice +description: Place outbound voice calls via the Plivo Call API using Auth ID/Auth Token authentication. +--- + +# Plivo Voice Skill + +Place outbound voice calls via Plivo. + +## Setup + +1. **Create a Plivo account** and open the console at https://cx.plivo.com +2. **Get your credentials**: + - Copy your **Auth ID** and **Auth Token** from the dashboard + - Rent a Plivo phone number to call from + +3. **Configure credentials**: + ```bash + export PLIVO_AUTH_ID="your-auth-id" + export PLIVO_AUTH_TOKEN="your-auth-token" + ``` + + Or create a `.env` file in the skill directory: + ``` + PLIVO_AUTH_ID=your-auth-id + PLIVO_AUTH_TOKEN=your-auth-token + ``` + +## Usage + +```bash +python3 scripts/make_call.py \ + --from "+14150000002" \ + --to "+14150000001" \ + --answer-url "https://example.com/answer.xml" +``` + +When the call is answered, Plivo fetches the answer URL and runs the Plivo XML it returns. To speak a message, have that URL return XML such as: + +```xml + + Your task has finished. + +``` + +## Requirements + +- Python 3.6+ +- No additional packages needed (uses stdlib urllib) diff --git a/skills/plivo-voice/scripts/make_call.py b/skills/plivo-voice/scripts/make_call.py new file mode 100644 index 0000000..a8373f6 --- /dev/null +++ b/skills/plivo-voice/scripts/make_call.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Make an outbound voice call via the Plivo Call API +""" +import argparse +import base64 +import json +import os +import sys +import urllib.error +import urllib.request +from pathlib import Path + +def load_env(): + """Load environment variables from .env file if it exists""" + env_file = Path(__file__).parent.parent / '.env' + if env_file.exists(): + with open(env_file) as f: + for line in f: + line = line.strip() + if not line or line.startswith('#') or '=' not in line: + continue + key, value = line.split('=', 1) + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): + value = value[1:-1] + os.environ[key.strip()] = value + +def make_call(from_number, to, answer_url, answer_method='GET', auth_id=None, auth_token=None): + """Place an outbound call via the Plivo Call API""" + + # Get credentials + auth_id = auth_id or os.getenv('PLIVO_AUTH_ID') + auth_token = auth_token or os.getenv('PLIVO_AUTH_TOKEN') + + if not auth_id or not auth_token: + print("ERROR: Plivo credentials not found!", file=sys.stderr) + print("\nPlease set credentials using one of these methods:", file=sys.stderr) + print("\n1. Environment variables:", file=sys.stderr) + print(" export PLIVO_AUTH_ID='your-auth-id'", file=sys.stderr) + print(" export PLIVO_AUTH_TOKEN='your-auth-token'", file=sys.stderr) + print("\n2. Create a .env file in skills/plivo-voice/:", file=sys.stderr) + print(" PLIVO_AUTH_ID=your-auth-id", file=sys.stderr) + print(" PLIVO_AUTH_TOKEN=your-auth-token", file=sys.stderr) + print("\nFind both in the Plivo console at https://cx.plivo.com", file=sys.stderr) + sys.exit(1) + + # Build request + url = f"https://api.plivo.com/v1/Account/{auth_id}/Call/" + payload = json.dumps({ + "from": from_number, + "to": to, + "answer_url": answer_url, + "answer_method": answer_method, + }).encode() + credentials = base64.b64encode(f"{auth_id}:{auth_token}".encode()).decode() + + request = urllib.request.Request(url, data=payload, method='POST') + request.add_header('Authorization', f"Basic {credentials}") + request.add_header('Content-Type', 'application/json') + + # Place call + try: + print(f"Placing call to {to}...", file=sys.stderr) + with urllib.request.urlopen(request) as response: + body = json.loads(response.read().decode()) + + request_uuid = body.get('request_uuid') + if not request_uuid: + print(f"ERROR: Plivo accepted the request but returned no request_uuid: {body}", file=sys.stderr) + return False + print(f"✓ Call placed successfully (request_uuid: {request_uuid})") + return True + + except urllib.error.HTTPError as e: + error_body = e.read().decode() + print(f"ERROR: Plivo returned HTTP {e.code}", file=sys.stderr) + print(error_body, file=sys.stderr) + return False + except Exception as e: + print(f"ERROR: Failed to place call: {e}", file=sys.stderr) + return False + +def main(): + parser = argparse.ArgumentParser(description='Make an outbound voice call via the Plivo Call API') + parser.add_argument('--to', required=True, help='Recipient number in E.164') + parser.add_argument('--from', dest='from_number', required=True, help='Caller ID (a Plivo number)') + parser.add_argument('--answer-url', dest='answer_url', required=True, + help='URL returning Plivo XML for the call (e.g. a response)') + parser.add_argument('--answer-method', dest='answer_method', default='GET', + help='HTTP method Plivo uses to fetch the answer URL (default: GET)') + parser.add_argument('--auth-id', dest='auth_id', help='Plivo Auth ID (default: PLIVO_AUTH_ID env var)') + parser.add_argument('--auth-token', dest='auth_token', help='Plivo Auth Token (default: PLIVO_AUTH_TOKEN env var)') + + args = parser.parse_args() + + # Load .env file if exists + load_env() + + # Place call + success = make_call( + from_number=args.from_number, + to=args.to, + answer_url=args.answer_url, + answer_method=args.answer_method, + auth_id=args.auth_id, + auth_token=args.auth_token + ) + + sys.exit(0 if success else 1) + +if __name__ == '__main__': + main()