From 2c9806b7c6fc9386926a1a3475acf8eafb96d9ad Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Tue, 30 Jan 2024 17:14:43 +0100 Subject: [PATCH 01/13] Infobip API wrapper and tests --- .../langchain_community/utilities/__init__.py | 9 + .../langchain_community/utilities/infobip.py | 185 ++++++++++++++++++ .../utilities/test_infobip.py | 116 +++++++++++ .../unit_tests/utilities/test_imports.py | 1 + 4 files changed, 311 insertions(+) create mode 100644 libs/community/langchain_community/utilities/infobip.py create mode 100644 libs/community/tests/integration_tests/utilities/test_infobip.py diff --git a/libs/community/langchain_community/utilities/__init__.py b/libs/community/langchain_community/utilities/__init__.py index ac91e964d2de9..b029c48eb616b 100644 --- a/libs/community/langchain_community/utilities/__init__.py +++ b/libs/community/langchain_community/utilities/__init__.py @@ -128,6 +128,12 @@ def _import_graphql() -> Any: return GraphQLAPIWrapper +def _import_infobip() -> Any: + from langchain_community.utilities.infobip import InfobipAPIWrapper + + return InfobipAPIWrapper + + def _import_jira() -> Any: from langchain_community.utilities.jira import JiraAPIWrapper @@ -311,6 +317,8 @@ def __getattr__(name: str) -> Any: return _import_google_serper() elif name == "GraphQLAPIWrapper": return _import_graphql() + elif name == "InfobipAPIWrapper": + return _import_infobip() elif name == "JiraAPIWrapper": return _import_jira() elif name == "MaxComputeAPIWrapper": @@ -382,6 +390,7 @@ def __getattr__(name: str) -> Any: "GoogleSearchAPIWrapper", "GoogleSerperAPIWrapper", "GraphQLAPIWrapper", + "InfobipAPIWrapper", "JiraAPIWrapper", "LambdaWrapper", "MaxComputeAPIWrapper", diff --git a/libs/community/langchain_community/utilities/infobip.py b/libs/community/langchain_community/utilities/infobip.py new file mode 100644 index 0000000000000..4f82695ed46d7 --- /dev/null +++ b/libs/community/langchain_community/utilities/infobip.py @@ -0,0 +1,185 @@ +"""Util that sends messages via Infobip.""" +from typing import Dict, List, Optional + +import requests +from langchain_core.pydantic_v1 import BaseModel, Extra, root_validator +from langchain_core.utils import get_from_dict_or_env +from requests.adapters import HTTPAdapter +from requests_toolbelt import MultipartEncoder +from urllib3.util import Retry + + +class InfobipAPIWrapper(BaseModel): + """Wrapper for Infobip API for messaging.""" + + infobip_api_key: Optional[str] = None + infobip_base_url: Optional[str] = "https://api.infobip.com" + + class Config: + """Configuration for this pydantic object.""" + + extra = Extra.forbid + + @root_validator(pre=True) + def validate_environment(cls, values: Dict) -> Dict: + """Validate that api key exists in environment.""" + values["infobip_api_key"] = get_from_dict_or_env( + values, "infobip_api_key", "INFOBIP_API_KEY" + ) + values["infobip_base_url"] = get_from_dict_or_env( + values, "infobip_base_url", "INFOBIP_BASE_URL" + ) + return values + + def _get_requests_session(self) -> requests.Session: + """Get a requests session with the correct headers.""" + retry_strategy: Retry = Retry( + total=4, # Maximum number of retries + backoff_factor=2, # Exponential backoff factor + status_forcelist=[429, 500, 502, 503, 504], # HTTP status codes to retry on + ) + adapter: HTTPAdapter = HTTPAdapter(max_retries=retry_strategy) + + session = requests.Session() + session.mount("http://", adapter) + session.mount("https://", adapter) + session.headers.update( + { + "Authorization": f"App {self.infobip_api_key}", + "User-Agent": "langchain-community", + } + ) + return session + + def _send_sms( + self, sender: str, destination_phone_numbers: List[str], text: str + ) -> str: + """Send an SMS message.""" + json: Dict = { + "messages": [ + { + "destinations": [ + {"to": destination} for destination in destination_phone_numbers + ], + "from": sender, + "text": text, + } + ] + } + + session: requests.Session = self._get_requests_session() + session.headers.update( + { + "Content-Type": "application/json", + } + ) + + response: requests.Response = session.post( + f"{self.infobip_base_url}/sms/2/text/advanced", + json=json, + ) + response.raise_for_status() + + return response.json()["messages"][0]["messageId"] + + def _send_email( + self, from_email: str, to_email: str, subject: str, body: str + ) -> str: + """Send an email message.""" + form_data: Dict = { + "from": from_email, + "to": to_email, + "subject": subject, + "text": body, + } + + data = MultipartEncoder(fields=form_data) + + session: requests.Session = self._get_requests_session() + session.headers.update( + { + "Content-Type": data.content_type, + } + ) + + response: requests.Response = session.post( + f"{self.infobip_base_url}/email/3/send", + data=data, + ) + + response.raise_for_status() + return response.json()["messages"][0]["messageId"] + + def _validate_email_address(self, email_address: str) -> str: + """Validate an email address.""" + json: Dict = { + "to": email_address, + } + + session: requests.Session = self._get_requests_session() + session.headers.update( + { + "Content-Type": "application/json", + } + ) + + response: requests.Response = session.post( + f"{self.infobip_base_url}/email/2/validation", + json=json, + ) + + response.raise_for_status() + validation_status: bool = response.json()["validMailbox"] + return f"Email address {email_address} is valid: {validation_status}" + + def run( + self, + body: str = "", + to: str = "", + sender: str = "", + subject: str = "", + channel: str = "sms", + ) -> str: + if channel == "sms": + if sender == "": + return "Sender must be specified for SMS messages" + + if to == "": + return "Destination must be specified for SMS messages" + + if body == "": + return "Body must be specified for SMS messages" + + return self._send_sms( + sender=sender, + destination_phone_numbers=[to], + text=body, + ) + elif channel == "email": + if sender == "": + return "Sender must be specified for email messages" + + if to == "": + return "Destination must be specified for email messages" + + if subject == "": + return "Subject must be specified for email messages" + + if body == "": + return "Body must be specified for email messages" + + return self._send_email( + from_email=sender, + to_email=to, + subject=subject, + body=body, + ) + elif channel == "email-validation": + if to == "": + return "Email address must be specified for email validation" + + return self._validate_email_address( + email_address=to, + ) + else: + return "Invalid channel, must be one of: sms, email, email-validation" diff --git a/libs/community/tests/integration_tests/utilities/test_infobip.py b/libs/community/tests/integration_tests/utilities/test_infobip.py new file mode 100644 index 0000000000000..58513594d4b7e --- /dev/null +++ b/libs/community/tests/integration_tests/utilities/test_infobip.py @@ -0,0 +1,116 @@ +from typing import Dict + +import responses + +from langchain_community.utilities.infobip import InfobipAPIWrapper + + +def test_send_sms() -> None: + infobip: InfobipAPIWrapper = InfobipAPIWrapper( + infobip_api_key="test", + infobip_base_url="https://api.infobip.com", + ) + + json_response: Dict = { + "messages": [ + { + "messageId": "123", + "status": { + "description": "Message sent to next instance", + "groupId": 1, + "groupName": "PENDING", + "id": 26, + "name": "PENDING_ACCEPTED", + }, + "to": "+1234567890", + } + ] + } + + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://api.infobip.com/sms/2/text/advanced", + json=json_response, + status=200, + ) + + response: str = infobip.run( + body="test", + to="+1234567890", + sender="+1234567890", + channel="sms", + ) + assert response == "123" + + +def test_send_email() -> None: + infobip: InfobipAPIWrapper = InfobipAPIWrapper( + infobip_api_key="test", + infobip_base_url="https://api.infobip.com", + ) + + json_response: Dict = { + "bulkId": "123", + "messages": [ + { + "to": "test@test.com", + "messageId": "123", + "status": { + "groupId": 1, + "groupName": "PENDING", + "id": 26, + "name": "PENDING_ACCEPTED", + "description": "Message accepted, pending for delivery.", + }, + } + ], + } + + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://api.infobip.com/email/3/send", + json=json_response, + status=200, + ) + + response: str = infobip.run( + body="test", + to="test@test.com", + sender="test@test.com", + channel="email", + ) + + assert response == "123" + + +def test_email_validation() -> None: + infobip: InfobipAPIWrapper = InfobipAPIWrapper( + infobip_api_key="test", + infobip_base_url="https://api.infobip.com", + ) + + json_response: Dict = { + "to": "voviro5448@giratex.com", + "validMailbox": "true", + "validSyntax": True, + "catchAll": False, + "didYouMean": None, + "disposable": False, + "roleBased": False, + } + + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://api.infobip.com/email/2/validation", + json=json_response, + status=200, + ) + response: str = infobip.run( + to="voviro5448@giratex.com", + channel="email-validation", + ) + + assert response == "Email address voviro5448@giratex.com is valid: true" diff --git a/libs/community/tests/unit_tests/utilities/test_imports.py b/libs/community/tests/unit_tests/utilities/test_imports.py index 6499d1b857b2e..d7f5d5d211c60 100644 --- a/libs/community/tests/unit_tests/utilities/test_imports.py +++ b/libs/community/tests/unit_tests/utilities/test_imports.py @@ -19,6 +19,7 @@ "GoogleSerperAPIWrapper", "GoogleTrendsAPIWrapper", "GraphQLAPIWrapper", + "InfobipAPIWrapper", "JiraAPIWrapper", "LambdaWrapper", "MaxComputeAPIWrapper", From 63eaa47bd2ceb67844b54bbcd020ca1283643342 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Tue, 30 Jan 2024 17:21:22 +0100 Subject: [PATCH 02/13] API wrapper should return machine frendly values --- .../langchain_community/utilities/infobip.py | 20 +++++++++---------- .../utilities/test_infobip.py | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libs/community/langchain_community/utilities/infobip.py b/libs/community/langchain_community/utilities/infobip.py index 4f82695ed46d7..662f679dc95d6 100644 --- a/libs/community/langchain_community/utilities/infobip.py +++ b/libs/community/langchain_community/utilities/infobip.py @@ -130,7 +130,7 @@ def _validate_email_address(self, email_address: str) -> str: response.raise_for_status() validation_status: bool = response.json()["validMailbox"] - return f"Email address {email_address} is valid: {validation_status}" + return str(validation_status) def run( self, @@ -142,13 +142,13 @@ def run( ) -> str: if channel == "sms": if sender == "": - return "Sender must be specified for SMS messages" + raise ValueError("Sender must be specified for SMS messages") if to == "": - return "Destination must be specified for SMS messages" + raise ValueError("Destination must be specified for SMS messages") if body == "": - return "Body must be specified for SMS messages" + raise ValueError("Body must be specified for SMS messages") return self._send_sms( sender=sender, @@ -157,16 +157,16 @@ def run( ) elif channel == "email": if sender == "": - return "Sender must be specified for email messages" + raise ValueError("Sender must be specified for email messages") if to == "": - return "Destination must be specified for email messages" + raise ValueError("Destination must be specified for email messages") if subject == "": - return "Subject must be specified for email messages" + raise ValueError("Subject must be specified for email messages") if body == "": - return "Body must be specified for email messages" + raise ValueError("Body must be specified for email messages") return self._send_email( from_email=sender, @@ -176,10 +176,10 @@ def run( ) elif channel == "email-validation": if to == "": - return "Email address must be specified for email validation" + raise ValueError("Destination must be specified for email validation") return self._validate_email_address( email_address=to, ) else: - return "Invalid channel, must be one of: sms, email, email-validation" + raise ValueError(f"Channel {channel} is not supported") diff --git a/libs/community/tests/integration_tests/utilities/test_infobip.py b/libs/community/tests/integration_tests/utilities/test_infobip.py index 58513594d4b7e..d8ce8608b4a4f 100644 --- a/libs/community/tests/integration_tests/utilities/test_infobip.py +++ b/libs/community/tests/integration_tests/utilities/test_infobip.py @@ -79,6 +79,7 @@ def test_send_email() -> None: body="test", to="test@test.com", sender="test@test.com", + subject="test", channel="email", ) @@ -113,4 +114,4 @@ def test_email_validation() -> None: channel="email-validation", ) - assert response == "Email address voviro5448@giratex.com is valid: true" + assert response == "true" From b8ea81841e174249bddb8e277726be095b560ded Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Tue, 30 Jan 2024 17:44:00 +0100 Subject: [PATCH 03/13] docs for Infobip Tool --- docs/docs/integrations/tools/infobip.ipynb | 123 +++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/docs/integrations/tools/infobip.ipynb diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb new file mode 100644 index 0000000000000..f7b44b1586f43 --- /dev/null +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -0,0 +1,123 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Infobip\n", + "This notebook that shows how to use [Infobip](https://www.infobip.com/) API wrapper to send SMS messages, emails or validate emails.\n", + "\n", + "Infobip provides many services, but this notebook will focus on SMS and email services. You can find more information about the API and other channels [here](https://www.infobip.com/docs/api)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "To use this tool you need to have an Infobip account. You can create [free trial account](https://www.infobip.com/docs/essentials/free-trial).\n", + "\n", + "\n", + "`InfobipAPIWrapper` uses name parameters where you can provide credentials:\n", + "- `infobip_api_key` - API key that you can find in your [developer tools](https://portal.infobip.com/dev/api-keys)\n", + "- `infobip_base_url` - base URL for Infobip API. You can use default value `https://api.infobip.com/`.\n", + "\n", + "You can also provide `infobip_api_key` and `infobip_base_url` as environment variables `INFOBIP_API_KEY` and `INFOBIP_BASE_URL`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sending a SMS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from langchain_community.utilities.infobip import InfobipAPIWrapper\n", + "\n", + "infobip: InfobipAPIWrapper = InfobipAPIWrapper()\n", + "\n", + "infobip.run(\n", + " to=\"+821012345678\",\n", + " text=\"Hello, World!\",\n", + " sender=\"Langchain\",\n", + " channel=\"sms\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sending a email" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from langchain_community.utilities.infobip import InfobipAPIWrapper\n", + "\n", + "infobip: InfobipAPIWrapper = InfobipAPIWrapper()\n", + "\n", + "infobip.run(\n", + " to=\"test@test.com\",\n", + " sender=\"test@test.com\",\n", + " subject=\"example\",\n", + " body=\"example\",\n", + " channel=\"email\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Validate email" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from langchain_community.utilities.infobip import InfobipAPIWrapper\n", + "\n", + "infobip: InfobipAPIWrapper = InfobipAPIWrapper()\n", + "\n", + "infobip.run(\n", + " to=\"test@test.com\",\n", + " channel=\"email-validation\",\n", + ")" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From f323ff2231a30f11ec923d2dde7226bda5c32255 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Tue, 30 Jan 2024 17:58:33 +0100 Subject: [PATCH 04/13] infobip tool docs --- docs/docs/integrations/tools/infobip.ipynb | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index f7b44b1586f43..affa81feb5ff6 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -111,6 +111,68 @@ " channel=\"email-validation\",\n", ")" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to use it inside an Agent " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, Tool, create_openai_functions_agent\n", + "from langchain.pydantic_v1 import BaseModel, Field\n", + "from langchain_community.utilities.infobip import InfobipAPIWrapper\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "instructions = \"\"\"\n", + "You are a communication agent. You are talking to a customer who wants to know about the product.\n", + "The customer asks you about the product. You answer the customer's question via email if you have a offer.\n", + "\"\"\"\n", + "base_prompt = hub.pull(\"langchain-ai/openai-functions-template\")\n", + "prompt = base_prompt.partial(instructions=instructions)\n", + "llm = ChatOpenAI(temperature=0)\n", + "\n", + "\n", + "class EmailInput(BaseModel):\n", + " body: str = Field(description=\"Email body text\")\n", + " to: int = Field(description=\"Email address to send to\")\n", + " sender: str = Field(description=\"Email address to send from\")\n", + " subject: str = Field(description=\"Email subject\")\n", + " channel: str = Field(\n", + " description=\"Channel type (email, sms, email-validation) (default: email)\"\n", + " )\n", + "\n", + "\n", + "infobip_api_wrapper: InfobipAPIWrapper = InfobipAPIWrapper()\n", + "infobip_tool = Tool(\n", + " name=\"infobip_sms\",\n", + " description=\"Send SMS via Infobip. If you need to send email, use infobip_email\",\n", + " func=infobip_api_wrapper.run,\n", + ")\n", + "tools = [infobip_tool]\n", + "\n", + "agent = create_openai_functions_agent(llm, tools, prompt)\n", + "agent_executor = AgentExecutor(\n", + " agent=agent,\n", + " tools=tools,\n", + " verbose=True,\n", + ")\n", + "\n", + "agent_executor.invoke(\n", + " {\"input\": \"Which products are usable inside langchain community?\"}\n", + ")" + ] } ], "metadata": { From 08803535616e0f3b18c8d7fba348ee3643eb4355 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Tue, 30 Jan 2024 23:23:37 +0100 Subject: [PATCH 05/13] fix example inside infobip.ipynb docs --- docs/docs/integrations/tools/infobip.ipynb | 47 +++++++++++++++------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index affa81feb5ff6..a5203d171a522 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -130,15 +130,13 @@ "outputs": [], "source": [ "from langchain import hub\n", - "from langchain.agents import AgentExecutor, Tool, create_openai_functions_agent\n", - "from langchain.pydantic_v1 import BaseModel, Field\n", + "from langchain.agents import AgentExecutor, create_openai_functions_agent\n", + "from langchain.tools import StructuredTool\n", "from langchain_community.utilities.infobip import InfobipAPIWrapper\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", "from langchain_openai import ChatOpenAI\n", "\n", - "instructions = \"\"\"\n", - "You are a communication agent. You are talking to a customer who wants to know about the product.\n", - "The customer asks you about the product. You answer the customer's question via email if you have a offer.\n", - "\"\"\"\n", + "instructions = \"You are a coding teacher. You are teaching a student how to code. The student asks you a question. You answer the question.\"\n", "base_prompt = hub.pull(\"langchain-ai/openai-functions-template\")\n", "prompt = base_prompt.partial(instructions=instructions)\n", "llm = ChatOpenAI(temperature=0)\n", @@ -146,19 +144,20 @@ "\n", "class EmailInput(BaseModel):\n", " body: str = Field(description=\"Email body text\")\n", - " to: int = Field(description=\"Email address to send to\")\n", - " sender: str = Field(description=\"Email address to send from\")\n", - " subject: str = Field(description=\"Email subject\")\n", - " channel: str = Field(\n", - " description=\"Channel type (email, sms, email-validation) (default: email)\"\n", + " to: str = Field(description=\"Email address to send to. Example: email@example.com\")\n", + " sender: str = Field(\n", + " description=\"Email address to send from, must be 'validemail@email.com'\"\n", " )\n", + " subject: str = Field(description=\"Email subject\")\n", + " channel: str = Field(description=\"Email channel, must be 'email'\")\n", "\n", "\n", "infobip_api_wrapper: InfobipAPIWrapper = InfobipAPIWrapper()\n", - "infobip_tool = Tool(\n", - " name=\"infobip_sms\",\n", - " description=\"Send SMS via Infobip. If you need to send email, use infobip_email\",\n", + "infobip_tool = StructuredTool.from_function(\n", + " name=\"infobip_email\",\n", + " description=\"Send Email via Infobip. If you need to send email, use infobip_email\",\n", " func=infobip_api_wrapper.run,\n", + " args_schema=EmailInput,\n", ")\n", "tools = [infobip_tool]\n", "\n", @@ -170,9 +169,27 @@ ")\n", "\n", "agent_executor.invoke(\n", - " {\"input\": \"Which products are usable inside langchain community?\"}\n", + " {\n", + " \"input\": \"Hi, can you please send me an example of Python recursion to my email voviro5448@giratex.com\"\n", + " }\n", ")" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "````bash\n", + "> Entering new AgentExecutor chain...\n", + "\n", + "Invoking: `infobip_email` with `{'body': 'Hi,\\n\\nHere is a simple example of a recursive function in Python:\\n\\n```\\ndef factorial(n):\\n if n == 1:\\n return 1\\n else:\\n return n * factorial(n-1)\\n```\\n\\nThis function calculates the factorial of a number. The factorial of a number is the product of all positive integers less than or equal to that number. The function calls itself with a smaller argument until it reaches the base case where n equals 1.\\n\\nBest,\\nCoding Teacher', 'to': 'voviro5448@giratex.com', 'sender': 'validemail@email.com', 'subject': 'Python Recursion Example', 'channel': 'email'}`\n", + "\n", + "\n", + "I have sent an example of Python recursion to your email. Please check your inbox.\n", + "\n", + "> Finished chain.\n", + "```" + ] } ], "metadata": { From 41ef1397804426326bef0211239adf4eacae32b0 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Tue, 30 Jan 2024 23:25:50 +0100 Subject: [PATCH 06/13] fix typo inside infobip docs --- docs/docs/integrations/tools/infobip.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index a5203d171a522..8bb27fec269e6 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -179,7 +179,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "````bash\n", + "```bash\n", "> Entering new AgentExecutor chain...\n", "\n", "Invoking: `infobip_email` with `{'body': 'Hi,\\n\\nHere is a simple example of a recursive function in Python:\\n\\n```\\ndef factorial(n):\\n if n == 1:\\n return 1\\n else:\\n return n * factorial(n-1)\\n```\\n\\nThis function calculates the factorial of a number. The factorial of a number is the product of all positive integers less than or equal to that number. The function calls itself with a smaller argument until it reaches the base case where n equals 1.\\n\\nBest,\\nCoding Teacher', 'to': 'voviro5448@giratex.com', 'sender': 'validemail@email.com', 'subject': 'Python Recursion Example', 'channel': 'email'}`\n", From f9ada3518c8318547adebac084c5cd85ce07dc37 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Wed, 31 Jan 2024 00:01:43 +0100 Subject: [PATCH 07/13] docs formating --- docs/docs/integrations/tools/infobip.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index 8bb27fec269e6..9bdbadab5d467 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -20,6 +20,7 @@ "\n", "\n", "`InfobipAPIWrapper` uses name parameters where you can provide credentials:\n", + "\n", "- `infobip_api_key` - API key that you can find in your [developer tools](https://portal.infobip.com/dev/api-keys)\n", "- `infobip_base_url` - base URL for Infobip API. You can use default value `https://api.infobip.com/`.\n", "\n", From 68024b9bd388a936f900098114f496b5652c71df Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Thu, 1 Feb 2024 10:11:09 +0100 Subject: [PATCH 08/13] use emails and phone numbers that we know are not in use --- docs/docs/integrations/tools/infobip.ipynb | 12 ++++++------ .../integration_tests/utilities/test_infobip.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index 9bdbadab5d467..13a2fabda2c28 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -49,7 +49,7 @@ "infobip: InfobipAPIWrapper = InfobipAPIWrapper()\n", "\n", "infobip.run(\n", - " to=\"+821012345678\",\n", + " to=\"41793026727\",\n", " text=\"Hello, World!\",\n", " sender=\"Langchain\",\n", " channel=\"sms\",\n", @@ -78,8 +78,8 @@ "infobip: InfobipAPIWrapper = InfobipAPIWrapper()\n", "\n", "infobip.run(\n", - " to=\"test@test.com\",\n", - " sender=\"test@test.com\",\n", + " to=\"test@example.com\",\n", + " sender=\"test@example.com\",\n", " subject=\"example\",\n", " body=\"example\",\n", " channel=\"email\",\n", @@ -147,7 +147,7 @@ " body: str = Field(description=\"Email body text\")\n", " to: str = Field(description=\"Email address to send to. Example: email@example.com\")\n", " sender: str = Field(\n", - " description=\"Email address to send from, must be 'validemail@email.com'\"\n", + " description=\"Email address to send from, must be 'validemail@example.com'\"\n", " )\n", " subject: str = Field(description=\"Email subject\")\n", " channel: str = Field(description=\"Email channel, must be 'email'\")\n", @@ -171,7 +171,7 @@ "\n", "agent_executor.invoke(\n", " {\n", - " \"input\": \"Hi, can you please send me an example of Python recursion to my email voviro5448@giratex.com\"\n", + " \"input\": \"Hi, can you please send me an example of Python recursion to my email email@example.com\"\n", " }\n", ")" ] @@ -183,7 +183,7 @@ "```bash\n", "> Entering new AgentExecutor chain...\n", "\n", - "Invoking: `infobip_email` with `{'body': 'Hi,\\n\\nHere is a simple example of a recursive function in Python:\\n\\n```\\ndef factorial(n):\\n if n == 1:\\n return 1\\n else:\\n return n * factorial(n-1)\\n```\\n\\nThis function calculates the factorial of a number. The factorial of a number is the product of all positive integers less than or equal to that number. The function calls itself with a smaller argument until it reaches the base case where n equals 1.\\n\\nBest,\\nCoding Teacher', 'to': 'voviro5448@giratex.com', 'sender': 'validemail@email.com', 'subject': 'Python Recursion Example', 'channel': 'email'}`\n", + "Invoking: `infobip_email` with `{'body': 'Hi,\\n\\nHere is a simple example of a recursive function in Python:\\n\\n```\\ndef factorial(n):\\n if n == 1:\\n return 1\\n else:\\n return n * factorial(n-1)\\n```\\n\\nThis function calculates the factorial of a number. The factorial of a number is the product of all positive integers less than or equal to that number. The function calls itself with a smaller argument until it reaches the base case where n equals 1.\\n\\nBest,\\nCoding Teacher', 'to': 'email@example.com', 'sender': 'validemail@example.com', 'subject': 'Python Recursion Example', 'channel': 'email'}`\n", "\n", "\n", "I have sent an example of Python recursion to your email. Please check your inbox.\n", diff --git a/libs/community/tests/integration_tests/utilities/test_infobip.py b/libs/community/tests/integration_tests/utilities/test_infobip.py index d8ce8608b4a4f..b05f5d0b8b701 100644 --- a/libs/community/tests/integration_tests/utilities/test_infobip.py +++ b/libs/community/tests/integration_tests/utilities/test_infobip.py @@ -77,8 +77,8 @@ def test_send_email() -> None: response: str = infobip.run( body="test", - to="test@test.com", - sender="test@test.com", + to="test@example.com", + sender="test@example.com", subject="test", channel="email", ) @@ -93,7 +93,7 @@ def test_email_validation() -> None: ) json_response: Dict = { - "to": "voviro5448@giratex.com", + "to": "test@example.com", "validMailbox": "true", "validSyntax": True, "catchAll": False, @@ -110,7 +110,7 @@ def test_email_validation() -> None: status=200, ) response: str = infobip.run( - to="voviro5448@giratex.com", + to="test@example.com", channel="email-validation", ) From 402d6d54383a8812c5d3110838bfd949985f7816 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Thu, 1 Feb 2024 11:20:16 +0100 Subject: [PATCH 09/13] use email we know that we use --- docs/docs/integrations/tools/infobip.ipynb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index 13a2fabda2c28..67b059fe57593 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -10,6 +10,11 @@ "Infobip provides many services, but this notebook will focus on SMS and email services. You can find more information about the API and other channels [here](https://www.infobip.com/docs/api)." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -108,7 +113,7 @@ "infobip: InfobipAPIWrapper = InfobipAPIWrapper()\n", "\n", "infobip.run(\n", - " to=\"test@test.com\",\n", + " to=\"test@example.com\",\n", " channel=\"email-validation\",\n", ")" ] From b6a8fd24c8acb089a1633715b04a3054957cc017 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Thu, 1 Feb 2024 13:13:50 +0100 Subject: [PATCH 10/13] remove email validation from infobip tool as it not needed --- docs/docs/integrations/tools/infobip.ipynb | 10 +++--- .../langchain_community/utilities/infobip.py | 7 ---- .../utilities/test_infobip.py | 33 +------------------ 3 files changed, 6 insertions(+), 44 deletions(-) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index 67b059fe57593..5fcefa382cc82 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -7,7 +7,7 @@ "# Infobip\n", "This notebook that shows how to use [Infobip](https://www.infobip.com/) API wrapper to send SMS messages, emails or validate emails.\n", "\n", - "Infobip provides many services, but this notebook will focus on SMS and email services. You can find more information about the API and other channels [here](https://www.infobip.com/docs/api)." + "Infobip provides many services, but this notebook will focus on SMS and Email services. You can find more information about the API and other channels [here](https://www.infobip.com/docs/api)." ] }, { @@ -26,8 +26,8 @@ "\n", "`InfobipAPIWrapper` uses name parameters where you can provide credentials:\n", "\n", - "- `infobip_api_key` - API key that you can find in your [developer tools](https://portal.infobip.com/dev/api-keys)\n", - "- `infobip_base_url` - base URL for Infobip API. You can use default value `https://api.infobip.com/`.\n", + "- `infobip_api_key` - API Key that you can find in your [developer tools](https://portal.infobip.com/dev/api-keys)\n", + "- `infobip_base_url` - Base url for Infobip API. You can use default value `https://api.infobip.com/`.\n", "\n", "You can also provide `infobip_api_key` and `infobip_base_url` as environment variables `INFOBIP_API_KEY` and `INFOBIP_BASE_URL`." ] @@ -65,7 +65,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Sending a email" + "## Sending a Email" ] }, { @@ -95,7 +95,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Validate email" + "## Validate Email" ] }, { diff --git a/libs/community/langchain_community/utilities/infobip.py b/libs/community/langchain_community/utilities/infobip.py index 662f679dc95d6..5a894cb1484ac 100644 --- a/libs/community/langchain_community/utilities/infobip.py +++ b/libs/community/langchain_community/utilities/infobip.py @@ -174,12 +174,5 @@ def run( subject=subject, body=body, ) - elif channel == "email-validation": - if to == "": - raise ValueError("Destination must be specified for email validation") - - return self._validate_email_address( - email_address=to, - ) else: raise ValueError(f"Channel {channel} is not supported") diff --git a/libs/community/tests/integration_tests/utilities/test_infobip.py b/libs/community/tests/integration_tests/utilities/test_infobip.py index b05f5d0b8b701..3e68b4c30541b 100644 --- a/libs/community/tests/integration_tests/utilities/test_infobip.py +++ b/libs/community/tests/integration_tests/utilities/test_infobip.py @@ -83,35 +83,4 @@ def test_send_email() -> None: channel="email", ) - assert response == "123" - - -def test_email_validation() -> None: - infobip: InfobipAPIWrapper = InfobipAPIWrapper( - infobip_api_key="test", - infobip_base_url="https://api.infobip.com", - ) - - json_response: Dict = { - "to": "test@example.com", - "validMailbox": "true", - "validSyntax": True, - "catchAll": False, - "didYouMean": None, - "disposable": False, - "roleBased": False, - } - - with responses.RequestsMock() as rsps: - rsps.add( - responses.POST, - "https://api.infobip.com/email/2/validation", - json=json_response, - status=200, - ) - response: str = infobip.run( - to="test@example.com", - channel="email-validation", - ) - - assert response == "true" + assert response == "123" \ No newline at end of file From 3d429ce25aa348f64007817cdb4c327bc8836f9c Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Thu, 1 Feb 2024 13:31:25 +0100 Subject: [PATCH 11/13] return error text from Infobip API if status code is not 200 --- docs/docs/integrations/tools/infobip.ipynb | 38 ++-------------- .../langchain_community/utilities/infobip.py | 45 +++++++++---------- .../utilities/test_infobip.py | 10 ++--- 3 files changed, 28 insertions(+), 65 deletions(-) diff --git a/docs/docs/integrations/tools/infobip.ipynb b/docs/docs/integrations/tools/infobip.ipynb index 5fcefa382cc82..72561b2fc92d3 100644 --- a/docs/docs/integrations/tools/infobip.ipynb +++ b/docs/docs/integrations/tools/infobip.ipynb @@ -5,16 +5,11 @@ "metadata": {}, "source": [ "# Infobip\n", - "This notebook that shows how to use [Infobip](https://www.infobip.com/) API wrapper to send SMS messages, emails or validate emails.\n", + "This notebook that shows how to use [Infobip](https://www.infobip.com/) API wrapper to send SMS messages, emails.\n", "\n", "Infobip provides many services, but this notebook will focus on SMS and Email services. You can find more information about the API and other channels [here](https://www.infobip.com/docs/api)." ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, { "cell_type": "markdown", "metadata": {}, @@ -26,8 +21,8 @@ "\n", "`InfobipAPIWrapper` uses name parameters where you can provide credentials:\n", "\n", - "- `infobip_api_key` - API Key that you can find in your [developer tools](https://portal.infobip.com/dev/api-keys)\n", - "- `infobip_base_url` - Base url for Infobip API. You can use default value `https://api.infobip.com/`.\n", + "- `infobip_api_key` - [API Key](https://www.infobip.com/docs/essentials/api-authentication#api-key-header) that you can find in your [developer tools](https://portal.infobip.com/dev/api-keys)\n", + "- `infobip_base_url` - [Base url](https://www.infobip.com/docs/essentials/base-url) for Infobip API. You can use default value `https://api.infobip.com/`.\n", "\n", "You can also provide `infobip_api_key` and `infobip_base_url` as environment variables `INFOBIP_API_KEY` and `INFOBIP_BASE_URL`." ] @@ -91,33 +86,6 @@ ")" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Validate Email" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "plaintext" - } - }, - "outputs": [], - "source": [ - "from langchain_community.utilities.infobip import InfobipAPIWrapper\n", - "\n", - "infobip: InfobipAPIWrapper = InfobipAPIWrapper()\n", - "\n", - "infobip.run(\n", - " to=\"test@example.com\",\n", - " channel=\"email-validation\",\n", - ")" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/libs/community/langchain_community/utilities/infobip.py b/libs/community/langchain_community/utilities/infobip.py index 5a894cb1484ac..9011c4a0962e1 100644 --- a/libs/community/langchain_community/utilities/infobip.py +++ b/libs/community/langchain_community/utilities/infobip.py @@ -46,7 +46,7 @@ def _get_requests_session(self) -> requests.Session: session.headers.update( { "Authorization": f"App {self.infobip_api_key}", - "User-Agent": "langchain-community", + "User-Agent": "infobip-langchain-community", } ) return session @@ -78,9 +78,17 @@ def _send_sms( f"{self.infobip_base_url}/sms/2/text/advanced", json=json, ) - response.raise_for_status() - return response.json()["messages"][0]["messageId"] + response_json: Dict = response.json() + if response.status_code != 200: + return response_json["requestError"]["serviceException"]["text"] + + try: + return response_json["messages"][0]["messageId"] + except KeyError: + return ( + "Could not get message ID from response, message was sent successfully" + ) def _send_email( self, from_email: str, to_email: str, subject: str, body: str @@ -107,30 +115,17 @@ def _send_email( data=data, ) - response.raise_for_status() - return response.json()["messages"][0]["messageId"] - - def _validate_email_address(self, email_address: str) -> str: - """Validate an email address.""" - json: Dict = { - "to": email_address, - } - - session: requests.Session = self._get_requests_session() - session.headers.update( - { - "Content-Type": "application/json", - } - ) + response_json: Dict = response.json() - response: requests.Response = session.post( - f"{self.infobip_base_url}/email/2/validation", - json=json, - ) + if response.status_code != 200: + return response_json["requestError"]["serviceException"]["text"] - response.raise_for_status() - validation_status: bool = response.json()["validMailbox"] - return str(validation_status) + try: + return response_json["messages"][0]["messageId"] + except KeyError: + return ( + "Could not get message ID from response, message was sent successfully" + ) def run( self, diff --git a/libs/community/tests/integration_tests/utilities/test_infobip.py b/libs/community/tests/integration_tests/utilities/test_infobip.py index 3e68b4c30541b..eaa1979e0f616 100644 --- a/libs/community/tests/integration_tests/utilities/test_infobip.py +++ b/libs/community/tests/integration_tests/utilities/test_infobip.py @@ -22,7 +22,7 @@ def test_send_sms() -> None: "id": 26, "name": "PENDING_ACCEPTED", }, - "to": "+1234567890", + "to": "41793026727", } ] } @@ -37,8 +37,8 @@ def test_send_sms() -> None: response: str = infobip.run( body="test", - to="+1234567890", - sender="+1234567890", + to="41793026727", + sender="41793026727", channel="sms", ) assert response == "123" @@ -54,7 +54,7 @@ def test_send_email() -> None: "bulkId": "123", "messages": [ { - "to": "test@test.com", + "to": "test@example.com", "messageId": "123", "status": { "groupId": 1, @@ -83,4 +83,4 @@ def test_send_email() -> None: channel="email", ) - assert response == "123" \ No newline at end of file + assert response == "123" From 36260dab2874167aafd81acda7559235492edc31 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Thu, 1 Feb 2024 14:06:21 +0100 Subject: [PATCH 12/13] Handle potential KeyErrors --- .../langchain_community/utilities/infobip.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/libs/community/langchain_community/utilities/infobip.py b/libs/community/langchain_community/utilities/infobip.py index 9011c4a0962e1..43cdcb3dd4e85 100644 --- a/libs/community/langchain_community/utilities/infobip.py +++ b/libs/community/langchain_community/utilities/infobip.py @@ -80,8 +80,11 @@ def _send_sms( ) response_json: Dict = response.json() - if response.status_code != 200: - return response_json["requestError"]["serviceException"]["text"] + try: + if response.status_code != 200: + return response_json["requestError"]["serviceException"]["text"] + except KeyError: + return "Failed to send message" try: return response_json["messages"][0]["messageId"] @@ -117,8 +120,11 @@ def _send_email( response_json: Dict = response.json() - if response.status_code != 200: - return response_json["requestError"]["serviceException"]["text"] + try: + if response.status_code != 200: + return response_json["requestError"]["serviceException"]["text"] + except KeyError: + return "Failed to send message" try: return response_json["messages"][0]["messageId"] From 091cfa307fde5de96edfd7eb8248c8be0423ada5 Mon Sep 17 00:00:00 2001 From: hmilkovi Date: Thu, 1 Feb 2024 14:31:48 +0100 Subject: [PATCH 13/13] remove unused code --- libs/community/langchain_community/utilities/infobip.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/community/langchain_community/utilities/infobip.py b/libs/community/langchain_community/utilities/infobip.py index 43cdcb3dd4e85..67a93e8b06b54 100644 --- a/libs/community/langchain_community/utilities/infobip.py +++ b/libs/community/langchain_community/utilities/infobip.py @@ -41,7 +41,6 @@ def _get_requests_session(self) -> requests.Session: adapter: HTTPAdapter = HTTPAdapter(max_retries=retry_strategy) session = requests.Session() - session.mount("http://", adapter) session.mount("https://", adapter) session.headers.update( {