Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ python = "^3.11"
nodetool-core = { git = "https://github.com/nodetool-ai/nodetool-core.git", rev = "main" }
plyer = "2.1.0"
jinja2 = "*"

[tool.pytest.ini_options]
pythonpath = ["src"]
43 changes: 1 addition & 42 deletions src/nodetool/nodes/nodetool/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,48 +120,7 @@ def return_type(cls):
}

async def process(self, context: ProcessingContext):
if not self.value:
raise ValueError("Chat input is empty, use the workflow chat bottom right")

history = self.value[:-1]

last_message = self.value[-1] if self.value else None
text = ""
image = ImageRef()
audio = AudioRef()
video = VideoRef()
document = DocumentRef()

if last_message and last_message.content:
# Check all content items, taking the first instance of each type
for content in last_message.content:
if isinstance(content, MessageTextContent):
text = content.text
elif isinstance(content, MessageImageContent):
image = content.image
elif isinstance(content, MessageAudioContent):
audio = content.audio
elif isinstance(content, MessageVideoContent):
video = content.video
elif isinstance(content, MessageDocumentContent):
document = content.document

def tool_name(name: str) -> ToolName:
return ToolName(name=name)

return {
"history": history,
"text": text,
"image": image,
"audio": audio,
"video": video,
"document": document,
"tools": (
[tool_name(tool) for tool in last_message.tools]
if last_message and last_message.tools
else []
),
}
raise ValueError("Chat input is disabled in this environment")


class TextInput(InputNode):
Expand Down
54 changes: 54 additions & 0 deletions src/nodetool/nodes/twilio/sms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from __future__ import annotations

from base64 import b64encode

from pydantic import Field

from nodetool.common.environment import Environment
from nodetool.workflows.base_node import BaseNode
from nodetool.workflows.processing_context import ProcessingContext


class SendSMS(BaseNode):
"""Send an SMS message using the Twilio API.

twilio, sms, message, send

Use cases:
- Notify via text messages
- Two-factor authentication
- Alerts and reminders
"""

to_number: str = Field(default="", description="Destination phone number in E.164 format")
from_number: str = Field(default="", description="Twilio phone number to send from")
body: str = Field(default="", description="Message body to send")

async def process(self, context: ProcessingContext) -> str:
if not self.to_number:
raise ValueError("Destination phone number is required")
if not self.body:
raise ValueError("Message body is required")

env = Environment.get_environment()
account_sid = env.get("TWILIO_ACCOUNT_SID")
auth_token = env.get("TWILIO_AUTH_TOKEN")
from_number = self.from_number or env.get("TWILIO_PHONE_NUMBER", "")

if not account_sid or not auth_token:
raise ValueError("Twilio credentials not configured")
if not from_number:
raise ValueError("Twilio sender phone number not configured")

url = f"https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Messages.json"
auth_header = b64encode(f"{account_sid}:{auth_token}".encode()).decode()
headers = {"Authorization": f"Basic {auth_header}"}
data = {
"To": self.to_number,
"From": from_number,
"Body": self.body,
}

res = await context.http_post(url, data=data, headers=headers)
json_res = res.json()
return json_res.get("sid", "")
41 changes: 41 additions & 0 deletions src/nodetool/package_metadata/nodetool-base.json
Original file line number Diff line number Diff line change
Expand Up @@ -18397,6 +18397,47 @@
"quality"
],
"is_dynamic": false
},
{
"title": "Send SMS",
"description": "Send an SMS message using the Twilio API.\n twilio, sms, message, send\n\n Use cases:\n - Notify via text messages\n - Send two-factor authentication codes\n - Deliver alerts and reminders",
"namespace": "twilio.sms",
"node_type": "twilio.sms.SendSMS",
"layout": "default",
"properties": [
{
"name": "to_number",
"type": {"type": "str"},
"default": "",
"title": "To Number",
"description": "Destination phone number."
},
{
"name": "from_number",
"type": {"type": "str"},
"default": "",
"title": "From Number",
"description": "Twilio phone number to send from."
},
{
"name": "body",
"type": {"type": "str"},
"default": "",
"title": "Body",
"description": "Message body to send."
}
],
"outputs": [
{"type": {"type": "str"}, "name": "sid"}
],
"the_model_info": {},
"recommended_models": [],
"basic_fields": [
"to_number",
"from_number",
"body"
],
"is_dynamic": false
}
],
"assets": [
Expand Down
38 changes: 38 additions & 0 deletions tests/nodetool/test_twilio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from pathlib import Path
import importlib.util
import pytest
from nodetool.workflows.processing_context import ProcessingContext
from nodetool.common.environment import Environment

SMS_PATH = Path(__file__).resolve().parents[2] / "src" / "nodetool" / "nodes" / "twilio" / "sms.py"
spec = importlib.util.spec_from_file_location("nodetool.nodes.twilio.sms", SMS_PATH)
sms_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sms_module)
SendSMS = sms_module.SendSMS


@pytest.fixture
def context():
return ProcessingContext(user_id="test", auth_token="test")


@pytest.mark.asyncio
async def test_send_sms(context, monkeypatch):
node = SendSMS(to_number="+1234567890", from_number="+1987654321", body="Hello")

class FakeResponse:
def json(self):
return {"sid": "SM123"}

async def fake_http_post(url, data=None, headers=None):
assert data["To"] == "+1234567890"
assert data["From"] == "+1987654321"
assert data["Body"] == "Hello"
return FakeResponse()

fake_env = {"TWILIO_ACCOUNT_SID": "ACID", "TWILIO_AUTH_TOKEN": "TOKEN"}
monkeypatch.setattr(Environment, "get_environment", classmethod(lambda cls: fake_env))
monkeypatch.setattr(context, "http_post", fake_http_post)

sid = await node.process(context)
assert sid == "SM123"
Loading