diff --git a/pyproject.toml b/pyproject.toml index d2ee2dc..ddbdfce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/nodetool/nodes/nodetool/input.py b/src/nodetool/nodes/nodetool/input.py index 861b7b4..81ca7ca 100644 --- a/src/nodetool/nodes/nodetool/input.py +++ b/src/nodetool/nodes/nodetool/input.py @@ -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): diff --git a/src/nodetool/nodes/twilio/sms.py b/src/nodetool/nodes/twilio/sms.py new file mode 100644 index 0000000..094e0e4 --- /dev/null +++ b/src/nodetool/nodes/twilio/sms.py @@ -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", "") diff --git a/src/nodetool/package_metadata/nodetool-base.json b/src/nodetool/package_metadata/nodetool-base.json index ce886a4..3847921 100644 --- a/src/nodetool/package_metadata/nodetool-base.json +++ b/src/nodetool/package_metadata/nodetool-base.json @@ -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": [ diff --git a/tests/nodetool/test_twilio.py b/tests/nodetool/test_twilio.py new file mode 100644 index 0000000..503a435 --- /dev/null +++ b/tests/nodetool/test_twilio.py @@ -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"