From b159dce45035b0cfba7502e3deef6bd66735d2e2 Mon Sep 17 00:00:00 2001 From: ansub Date: Sat, 8 Mar 2025 23:12:14 +0400 Subject: [PATCH 01/19] whatsapp-integration --- .../whatsapp_chat_agent.py | 123 +++++++++ libs/agno/agno/tools/whatsapp.py | 235 ++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py create mode 100644 libs/agno/agno/tools/whatsapp.py diff --git a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py new file mode 100644 index 0000000000..86abc18982 --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py @@ -0,0 +1,123 @@ +from agno.agent import Agent +from agno.models.openai import OpenAIChat +from agno.storage.agent.sqlite import SqliteAgentStorage +from agno.tools.whatsapp import WhatsAppTools +from agno.tools.yfinance import YFinanceTools +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import PlainTextResponse +from dotenv import load_dotenv +import os +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Load environment variables +load_dotenv() + +# Configure constants +VERIFY_TOKEN = os.getenv("WHATSAPP_VERIFY_TOKEN") +if not VERIFY_TOKEN: + raise ValueError("WHATSAPP_VERIFY_TOKEN must be set in .envrc") + +WEBHOOK_URL = os.getenv("WHATSAPP_WEBHOOK_URL") +if not WEBHOOK_URL: + raise ValueError("WHATSAPP_WEBHOOK_URL must be set in .envrc") + +AGENT_STORAGE_FILE = "tmp/whatsapp_agents.db" + +# Initialize WhatsApp tools and agent +whatsapp = WhatsAppTools() +agent = Agent( + name="WhatsApp Assistant", + model=OpenAIChat(id="gpt-4"), + tools=[ + whatsapp, + YFinanceTools( + stock_price=True, + analyst_recommendations=True, + stock_fundamentals=True, + historical_prices=True, + company_info=True, + company_news=True, + ) + ], + storage=SqliteAgentStorage(table_name="whatsapp_agent", db_file=AGENT_STORAGE_FILE), + add_history_to_messages=True, + num_history_responses=3, + markdown=True, + description="You are also a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses." +) + +# Create FastAPI app +app = FastAPI() + +@app.get("/webhook") +async def verify_webhook(request: Request): + """Handle WhatsApp webhook verification""" + mode = request.query_params.get("hub.mode") + token = request.query_params.get("hub.verify_token") + challenge = request.query_params.get("hub.challenge") + + logger.info(f"Webhook verification request - Mode: {mode}, Token: {token}") + + if mode == "subscribe" and token == VERIFY_TOKEN: + if not challenge: + raise HTTPException(status_code=400, detail="No challenge received") + return PlainTextResponse(content=challenge) + + raise HTTPException(status_code=403, detail="Invalid verify token or mode") + +@app.post("/webhook") +async def handle_message(request: Request): + """Handle incoming WhatsApp messages""" + try: + body = await request.json() + + # Validate webhook data + if body.get("object") != "whatsapp_business_account": + logger.warning(f"Received non-WhatsApp webhook object: {body.get('object')}") + return {"status": "ignored"} + + # Process messages + for entry in body.get("entry", []): + for change in entry.get("changes", []): + messages = change.get("value", {}).get("messages", []) + + if not messages: + continue + + message = messages[0] + if message.get("type") != "text": + continue + + # Extract message details + phone_number = message["from"] + message_text = message["text"]["body"] + + logger.info(f"Processing message from {phone_number}: {message_text}") + + # Generate and send response + response = agent.run(message_text) + whatsapp.send_text_message_sync( + recipient=phone_number, + text=response.content + ) + logger.info(f"Response sent to {phone_number}") + + return {"status": "ok"} + + except Exception as e: + logger.error(f"Error processing webhook: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +if __name__ == "__main__": + import uvicorn + + logger.info("Starting WhatsApp Bot Server") + logger.info(f"Webhook URL: {WEBHOOK_URL}") + logger.info(f"Verify Token: {VERIFY_TOKEN}") + logger.info("Make sure your .env file contains WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID") + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py new file mode 100644 index 0000000000..0b732c7072 --- /dev/null +++ b/libs/agno/agno/tools/whatsapp.py @@ -0,0 +1,235 @@ +import os +from typing import Optional, Dict, Any, List +import json +import httpx +from dotenv import load_dotenv + +from agno.tools import Toolkit +from agno.utils.log import logger + +# Try to load from both .env and .envrc +load_dotenv() + +class WhatsAppTools(Toolkit): + """WhatsApp Business API toolkit for sending messages.""" + + base_url = "https://graph.facebook.com" + + def __init__( + self, + access_token: Optional[str] = None, + phone_number_id: Optional[str] = None, + version: str = "v22.0", + recipient_waid: Optional[str] = None, + ): + """Initialize WhatsApp toolkit. + + Args: + access_token: WhatsApp Business API access token + phone_number_id: WhatsApp Business Account phone number ID + version: API version to use + recipient_waid: Default recipient WhatsApp ID (optional) + """ + super().__init__(name="whatsapp") + + # Core credentials + self.access_token = access_token or os.getenv("WHATSAPP_ACCESS_TOKEN") or os.getenv("ACCESS_TOKEN") + if not self.access_token: + logger.error("WHATSAPP_ACCESS_TOKEN not set. Please set the WHATSAPP_ACCESS_TOKEN environment variable.") + + self.phone_number_id = phone_number_id or os.getenv("WHATSAPP_PHONE_NUMBER_ID") or os.getenv("PHONE_NUMBER_ID") + if not self.phone_number_id: + logger.error("WHATSAPP_PHONE_NUMBER_ID not set. Please set the WHATSAPP_PHONE_NUMBER_ID environment variable.") + + # Optional default recipient + self.default_recipient = recipient_waid or os.getenv("WHATSAPP_RECIPIENT_WAID") or os.getenv("RECIPIENT_WAID") + + # API version + self.version = version or os.getenv("WHATSAPP_VERSION") or os.getenv("VERSION", "v22.0") + + # Register methods that can be used by the agent + self.register(self.send_text_message_sync) + self.register(self.send_template_message_sync) + + # Log configuration status + self._log_config_status() + + def _log_config_status(self): + """Log the configuration status of the WhatsApp toolkit.""" + config_status = { + "Core credentials": { + "access_token": bool(self.access_token), + "phone_number_id": bool(self.phone_number_id) + }, + "Optional settings": { + "default_recipient": bool(self.default_recipient), + "api_version": self.version + } + } + logger.debug(f"WhatsApp toolkit configuration status: {json.dumps(config_status, indent=2)}") + + def _get_headers(self) -> Dict[str, str]: + """Get headers for API requests.""" + return { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json" + } + + def _get_messages_url(self) -> str: + """Get the messages endpoint URL.""" + return f"{self.base_url}/{self.version}/{self.phone_number_id}/messages" + + async def _send_message_async(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Send a message asynchronously using the WhatsApp API. + + Args: + data: Message data to send + + Returns: + API response as dictionary + """ + async with httpx.AsyncClient() as client: + response = await client.post( + self._get_messages_url(), + headers=self._get_headers(), + json=data + ) + response.raise_for_status() + return response.json() + + def _send_message_sync(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Send a message synchronously using the WhatsApp API. + + Args: + data: Message data to send + + Returns: + API response as dictionary + """ + url = self._get_messages_url() + headers = self._get_headers() + + logger.debug(f"Sending WhatsApp request to URL: {url}") + logger.debug(f"Request data: {json.dumps(data, indent=2)}") + logger.debug(f"Headers: {json.dumps(headers, indent=2)}") + + response = httpx.post( + url, + headers=headers, + json=data + ) + + logger.debug(f"Response status code: {response.status_code}") + logger.debug(f"Response headers: {dict(response.headers)}") + logger.debug(f"Response body: {response.text}") + + response.raise_for_status() + return response.json() + + def send_text_message_sync( + self, + recipient: Optional[str] = None, + text: str = "", + preview_url: bool = False + ) -> str: + """Send a text message to a WhatsApp user (synchronous version). + + Args: + recipient: Recipient's WhatsApp ID or phone number (e.g., "+1234567890"). If not provided, uses default_recipient + text: The text message to send + preview_url: Whether to generate previews for links in the message + + Returns: + Success message with message ID + """ + # Use default recipient if none provided + if recipient is None: + if not self.default_recipient: + raise ValueError("No recipient provided and no default recipient set") + recipient = self.default_recipient + logger.debug(f"Using default recipient: {recipient}") + + logger.debug(f"Sending WhatsApp message to {recipient}: {text}") + logger.debug(f"Current config - Phone Number ID: {self.phone_number_id}, Version: {self.version}") + + data = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": recipient, + "type": "text", + "text": { + "preview_url": preview_url, + "body": text + } + } + + try: + response = self._send_message_sync(data) + message_id = response.get("messages", [{}])[0].get("id", "unknown") + logger.debug(f"Full API response: {json.dumps(response, indent=2)}") + return f"Message sent successfully! Message ID: {message_id}" + except httpx.HTTPStatusError as e: + logger.error(f"Failed to send WhatsApp message: {e}") + logger.error(f"Error response: {e.response.text if hasattr(e, 'response') else 'No response text'}") + raise + except Exception as e: + logger.error(f"Unexpected error sending WhatsApp message: {str(e)}") + raise + + def send_template_message_sync( + self, + recipient: Optional[str] = None, + template_name: str = "", + language_code: str = "en_US", + components: Optional[List[Dict[str, Any]]] = None + ) -> str: + """Send a template message to a WhatsApp user (synchronous version). + + Args: + recipient: Recipient's WhatsApp ID or phone number (e.g., "+1234567890"). If not provided, uses default_recipient + template_name: Name of the template to use + language_code: Language code for the template (e.g., "en_US") + components: Optional list of template components (header, body, buttons) + + Returns: + Success message with message ID + """ + # Use default recipient if none provided + if recipient is None: + if not self.default_recipient: + raise ValueError("No recipient provided and no default recipient set") + recipient = self.default_recipient + + logger.debug(f"Sending WhatsApp template message to {recipient}: {template_name}") + + data = { + "messaging_product": "whatsapp", + "to": recipient, + "type": "template", + "template": { + "name": template_name, + "language": { + "code": language_code + } + } + } + + if components: + data["template"]["components"] = components + + try: + response = self._send_message_sync(data) + message_id = response.get("messages", [{}])[0].get("id", "unknown") + return f"Template message sent successfully! Message ID: {message_id}" + except httpx.HTTPStatusError as e: + logger.error(f"Failed to send WhatsApp template message: {e}") + raise + + # Keep the async methods for compatibility but mark them as internal + async def send_text_message(self, *args, **kwargs): + """Internal async version - use send_text_message_sync instead""" + return self.send_text_message_sync(*args, **kwargs) + + async def send_template_message(self, *args, **kwargs): + """Internal async version - use send_template_message_sync instead""" + return self.send_template_message_sync(*args, **kwargs) From 023fb59403aefc9fe619acae100f226d5dac1a50 Mon Sep 17 00:00:00 2001 From: ansub Date: Sat, 8 Mar 2025 23:16:33 +0400 Subject: [PATCH 02/19] Update WhatsApp chat agent: GPT-4o model and description refinement --- .../apps/whatsapp_chat_agent/whatsapp_chat_agent.py | 4 ++-- cookbook/tools/whatsapp_tools.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 cookbook/tools/whatsapp_tools.py diff --git a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py index 86abc18982..d33ce26f6a 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py @@ -31,7 +31,7 @@ whatsapp = WhatsAppTools() agent = Agent( name="WhatsApp Assistant", - model=OpenAIChat(id="gpt-4"), + model=OpenAIChat(id="gpt-4o"), tools=[ whatsapp, YFinanceTools( @@ -47,7 +47,7 @@ add_history_to_messages=True, num_history_responses=3, markdown=True, - description="You are also a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses." + description="You are a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses." ) # Create FastAPI app diff --git a/cookbook/tools/whatsapp_tools.py b/cookbook/tools/whatsapp_tools.py new file mode 100644 index 0000000000..4c18331179 --- /dev/null +++ b/cookbook/tools/whatsapp_tools.py @@ -0,0 +1,9 @@ +from agno.agent import Agent +from agno.tools.whatsapp import WhatsAppTools + +agent = Agent( + name="whatsapp", + tools=[WhatsAppTools()], +) + +agent.print_response("Send message to whatsapp chat a paragraph about the moon") From c56527bc03c8d54868cf0cd69a737fb6356bc6cb Mon Sep 17 00:00:00 2001 From: ansub Date: Sat, 8 Mar 2025 23:33:16 +0400 Subject: [PATCH 03/19] Add comprehensive WhatsApp integration cookbook with setup instructions --- cookbook/tools/whatsapp_tools.py | 41 +++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/cookbook/tools/whatsapp_tools.py b/cookbook/tools/whatsapp_tools.py index 4c18331179..1d1342cc5f 100644 --- a/cookbook/tools/whatsapp_tools.py +++ b/cookbook/tools/whatsapp_tools.py @@ -1,3 +1,38 @@ +""" +WhatsApp Cookbook +---------------- + +This cookbook demonstrates how to use WhatsApp integration with Agno. Before running this example, +you'll need to complete these setup steps: + +1. Create Meta Developer Account + - Go to Meta Developer Portal (https://developers.facebook.com/) and create a new account + - Create a new app at Meta Apps Dashboard (https://developers.facebook.com/apps/) + - Enable WhatsApp integration for your app (https://developers.facebook.com/docs/whatsapp/cloud-api/get-started) + +2. Set Up WhatsApp Business API + - Get your WhatsApp Business Account ID from Business Settings (https://business.facebook.com/settings/) + - Generate a permanent access token in System Users (https://business.facebook.com/settings/system-users) + - Set up a test phone number (https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#testing-your-app) + - Create a message template in Meta Business Manager (https://business.facebook.com/wa/manage/message-templates/) + +3. Configure Environment + - Set these environment variables: + WHATSAPP_ACCESS_TOKEN=your_access_token # Permanent access token from System Users + WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id # Your WhatsApp test phone number ID + +Important Notes: +- WhatsApp has a 24-hour messaging window policy +- You can only send free-form messages to users who have messaged you in the last 24 hours +- For first-time outreach, you must use pre-approved message templates + (https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates) +- Test messages can only be sent to numbers that are registered in your test environment + +The example below shows how to send a template message using Agno's WhatsApp tools. +For more complex use cases, check out the WhatsApp Cloud API documentation: +https://developers.facebook.com/docs/whatsapp/cloud-api/overview +""" + from agno.agent import Agent from agno.tools.whatsapp import WhatsAppTools @@ -6,4 +41,8 @@ tools=[WhatsAppTools()], ) -agent.print_response("Send message to whatsapp chat a paragraph about the moon") +# Example: Send a template message +# Note: Replace 'hello_world' with your actual template name +agent.print_response( + "Send a template message using the 'hello_world' template in English" +) From f385eeee36d8cfdff04bd6dad6123a0ba01842bc Mon Sep 17 00:00:00 2001 From: ansub Date: Sat, 8 Mar 2025 23:33:58 +0400 Subject: [PATCH 04/19] Refactor WhatsApp chat agent and tools with code formatting and minor improvements --- .../whatsapp_chat_agent.py | 27 +++++---- libs/agno/agno/tools/whatsapp.py | 55 ++++++------------- 2 files changed, 33 insertions(+), 49 deletions(-) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py index d33ce26f6a..d468e6e25c 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py @@ -1,13 +1,14 @@ +import logging +import os + from agno.agent import Agent from agno.models.openai import OpenAIChat from agno.storage.agent.sqlite import SqliteAgentStorage from agno.tools.whatsapp import WhatsAppTools from agno.tools.yfinance import YFinanceTools -from fastapi import FastAPI, Request, HTTPException -from fastapi.responses import PlainTextResponse from dotenv import load_dotenv -import os -import logging +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import PlainTextResponse # Configure logging logging.basicConfig(level=logging.INFO) @@ -41,18 +42,19 @@ historical_prices=True, company_info=True, company_news=True, - ) + ), ], storage=SqliteAgentStorage(table_name="whatsapp_agent", db_file=AGENT_STORAGE_FILE), add_history_to_messages=True, num_history_responses=3, markdown=True, - description="You are a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses." + description="You are a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses.", ) # Create FastAPI app app = FastAPI() + @app.get("/webhook") async def verify_webhook(request: Request): """Handle WhatsApp webhook verification""" @@ -69,6 +71,7 @@ async def verify_webhook(request: Request): raise HTTPException(status_code=403, detail="Invalid verify token or mode") + @app.post("/webhook") async def handle_message(request: Request): """Handle incoming WhatsApp messages""" @@ -77,7 +80,9 @@ async def handle_message(request: Request): # Validate webhook data if body.get("object") != "whatsapp_business_account": - logger.warning(f"Received non-WhatsApp webhook object: {body.get('object')}") + logger.warning( + f"Received non-WhatsApp webhook object: {body.get('object')}" + ) return {"status": "ignored"} # Process messages @@ -101,8 +106,7 @@ async def handle_message(request: Request): # Generate and send response response = agent.run(message_text) whatsapp.send_text_message_sync( - recipient=phone_number, - text=response.content + recipient=phone_number, text=response.content ) logger.info(f"Response sent to {phone_number}") @@ -112,12 +116,15 @@ async def handle_message(request: Request): logger.error(f"Error processing webhook: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) + if __name__ == "__main__": import uvicorn logger.info("Starting WhatsApp Bot Server") logger.info(f"Webhook URL: {WEBHOOK_URL}") logger.info(f"Verify Token: {VERIFY_TOKEN}") - logger.info("Make sure your .env file contains WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID") + logger.info( + "Make sure your .env file contains WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID" + ) uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 0b732c7072..22ec204e5a 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -1,6 +1,7 @@ -import os -from typing import Optional, Dict, Any, List import json +import os +from typing import Any, Dict, List, Optional + import httpx from dotenv import load_dotenv @@ -10,6 +11,7 @@ # Try to load from both .env and .envrc load_dotenv() + class WhatsAppTools(Toolkit): """WhatsApp Business API toolkit for sending messages.""" @@ -39,7 +41,9 @@ def __init__( self.phone_number_id = phone_number_id or os.getenv("WHATSAPP_PHONE_NUMBER_ID") or os.getenv("PHONE_NUMBER_ID") if not self.phone_number_id: - logger.error("WHATSAPP_PHONE_NUMBER_ID not set. Please set the WHATSAPP_PHONE_NUMBER_ID environment variable.") + logger.error( + "WHATSAPP_PHONE_NUMBER_ID not set. Please set the WHATSAPP_PHONE_NUMBER_ID environment variable." + ) # Optional default recipient self.default_recipient = recipient_waid or os.getenv("WHATSAPP_RECIPIENT_WAID") or os.getenv("RECIPIENT_WAID") @@ -59,21 +63,15 @@ def _log_config_status(self): config_status = { "Core credentials": { "access_token": bool(self.access_token), - "phone_number_id": bool(self.phone_number_id) + "phone_number_id": bool(self.phone_number_id), }, - "Optional settings": { - "default_recipient": bool(self.default_recipient), - "api_version": self.version - } + "Optional settings": {"default_recipient": bool(self.default_recipient), "api_version": self.version}, } logger.debug(f"WhatsApp toolkit configuration status: {json.dumps(config_status, indent=2)}") def _get_headers(self) -> Dict[str, str]: """Get headers for API requests.""" - return { - "Authorization": f"Bearer {self.access_token}", - "Content-Type": "application/json" - } + return {"Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json"} def _get_messages_url(self) -> str: """Get the messages endpoint URL.""" @@ -89,11 +87,7 @@ async def _send_message_async(self, data: Dict[str, Any]) -> Dict[str, Any]: API response as dictionary """ async with httpx.AsyncClient() as client: - response = await client.post( - self._get_messages_url(), - headers=self._get_headers(), - json=data - ) + response = await client.post(self._get_messages_url(), headers=self._get_headers(), json=data) response.raise_for_status() return response.json() @@ -113,11 +107,7 @@ def _send_message_sync(self, data: Dict[str, Any]) -> Dict[str, Any]: logger.debug(f"Request data: {json.dumps(data, indent=2)}") logger.debug(f"Headers: {json.dumps(headers, indent=2)}") - response = httpx.post( - url, - headers=headers, - json=data - ) + response = httpx.post(url, headers=headers, json=data) logger.debug(f"Response status code: {response.status_code}") logger.debug(f"Response headers: {dict(response.headers)}") @@ -126,12 +116,7 @@ def _send_message_sync(self, data: Dict[str, Any]) -> Dict[str, Any]: response.raise_for_status() return response.json() - def send_text_message_sync( - self, - recipient: Optional[str] = None, - text: str = "", - preview_url: bool = False - ) -> str: + def send_text_message_sync(self, recipient: Optional[str] = None, text: str = "", preview_url: bool = False) -> str: """Send a text message to a WhatsApp user (synchronous version). Args: @@ -157,10 +142,7 @@ def send_text_message_sync( "recipient_type": "individual", "to": recipient, "type": "text", - "text": { - "preview_url": preview_url, - "body": text - } + "text": {"preview_url": preview_url, "body": text}, } try: @@ -181,7 +163,7 @@ def send_template_message_sync( recipient: Optional[str] = None, template_name: str = "", language_code: str = "en_US", - components: Optional[List[Dict[str, Any]]] = None + components: Optional[List[Dict[str, Any]]] = None, ) -> str: """Send a template message to a WhatsApp user (synchronous version). @@ -206,12 +188,7 @@ def send_template_message_sync( "messaging_product": "whatsapp", "to": recipient, "type": "template", - "template": { - "name": template_name, - "language": { - "code": language_code - } - } + "template": {"name": template_name, "language": {"code": language_code}}, } if components: From 6840a53213da6c3f0caf31727697051257184b25 Mon Sep 17 00:00:00 2001 From: ansub Date: Sat, 8 Mar 2025 23:41:30 +0400 Subject: [PATCH 05/19] fix --- .../apps/whatsapp_chat_agent/readme.md | 94 +++++++++++++++++++ .../apps/whatsapp_chat_agent/requirements.txt | 10 ++ 2 files changed, 104 insertions(+) create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/readme.md create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/requirements.txt diff --git a/cookbook/examples/apps/whatsapp_chat_agent/readme.md b/cookbook/examples/apps/whatsapp_chat_agent/readme.md new file mode 100644 index 0000000000..ea77e21262 --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/readme.md @@ -0,0 +1,94 @@ +# WhatsApp Chat Agent with Stock Market Insights + +This is a WhatsApp chatbot that provides stock market insights and financial advice using the WhatsApp Business API. The bot is built using FastAPI and can be run locally using ngrok for development and testing. + +## Prerequisites + +- Python 3.7+ +- ngrok account (free tier works fine) +- WhatsApp Business API access +- Meta Developer account +- OpenAI API key + +## Setup Instructions + +1. **Install Dependencies** + +```bash +pip install -r requirements.txt +``` + +2. **Set up ngrok** + + - Download and install ngrok from https://ngrok.com/download + - Sign up for a free account and get your authtoken + - Authenticate ngrok with your token: + ```bash + ngrok config add-authtoken YOUR_AUTH_TOKEN + ``` + +3. **Create a Meta Developer Account** + + - Go to https://developers.facebook.com/ + - Create a new app + - Set up WhatsApp in your app + - Get your WhatsApp Business Account ID and Phone Number ID + +4. **Environment Variables** + Create a `.env` file in the project root with the following variables: + +```env +WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token +WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id +WHATSAPP_VERIFY_TOKEN=your_custom_verify_token # Can be any string you choose +OPENAI_API_KEY=your_openai_api_key +``` + +## Running the Application + +1. **Start the FastAPI server** + +```bash +python whatsapp_chat_agent.py +``` + +2. **Start ngrok** + In a new terminal window: + +```bash +ngrok http 8000 +``` + +3. **Configure Webhook** + - Copy the HTTPS URL provided by ngrok (e.g., https://xxxx-xx-xx-xxx-xx.ngrok.io) + - Go to your Meta Developer Portal + - Set up Webhooks for your WhatsApp Business Account + - Use the ngrok URL + "/webhook" as your Callback URL + - Use your WHATSAPP_VERIFY_TOKEN as the Verify Token + - Subscribe to the messages webhook + +## Testing the Bot + +1. Send a message to your WhatsApp Business number +2. The bot should respond with stock market insights based on your query +3. You can ask questions about: + - Stock prices + - Company information + - Analyst recommendations + - Stock fundamentals + - Historical prices + - Company news + +## Troubleshooting + +- Make sure all environment variables are properly set +- Check the FastAPI logs for any errors +- Verify that ngrok is running and the webhook URL is correctly configured +- Ensure your WhatsApp Business API is properly set up and the phone number is verified + +## Important Notes + +- The ngrok URL changes every time you restart ngrok (unless you have a paid account) +- You'll need to update the Webhook URL in the Meta Developer Portal whenever the ngrok URL changes +- Keep your WHATSAPP_ACCESS_TOKEN and other credentials secure +- The bot stores conversation history in a SQLite database in the `tmp` directory diff --git a/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt b/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt new file mode 100644 index 0000000000..00e787fd52 --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt @@ -0,0 +1,10 @@ +fastapi>=0.68.0 +uvicorn>=0.15.0 +python-dotenv>=0.19.0 +requests>=2.26.0 +yfinance>=0.1.63 +openai>=1.0.0 +agno>=0.1.0 +python-multipart>=0.0.5 +aiohttp>=3.8.0 +SQLAlchemy>=1.4.0 From 9844b3b98430fd7c8d51335e874acbe29957c992 Mon Sep 17 00:00:00 2001 From: ansub Date: Sun, 9 Mar 2025 00:03:40 +0400 Subject: [PATCH 06/19] Fix WhatsApp tools type hint for template components --- libs/agno/agno/tools/whatsapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 22ec204e5a..88b41f20f9 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -192,7 +192,7 @@ def send_template_message_sync( } if components: - data["template"]["components"] = components + data["template"]["components"] = components # type: ignore[index] try: response = self._send_message_sync(data) From a8018af7f15e4dcbca9188cea780706904e67a72 Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 19:23:37 +0400 Subject: [PATCH 07/19] Update WhatsApp chat agent README and requirements - Clarify ngrok setup instructions as development testing only - Add missing environment variable for WhatsApp recipient phone number - Regenerate requirements.txt with updated dependencies - Minor formatting and clarity improvements in documentation --- .../generate_requirements.sh | 12 ++ .../apps/whatsapp_chat_agent/readme.md | 7 +- .../apps/whatsapp_chat_agent/requirements.in | 10 + .../apps/whatsapp_chat_agent/requirements.txt | 182 +++++++++++++++++- libs/agno/agno/tools/whatsapp.py | 2 +- 5 files changed, 200 insertions(+), 13 deletions(-) create mode 100755 cookbook/examples/apps/whatsapp_chat_agent/generate_requirements.sh create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/requirements.in diff --git a/cookbook/examples/apps/whatsapp_chat_agent/generate_requirements.sh b/cookbook/examples/apps/whatsapp_chat_agent/generate_requirements.sh new file mode 100755 index 0000000000..78f8c7cec9 --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/generate_requirements.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +############################################################################ +# Generate requirements.txt from requirements.in +############################################################################ + +echo "Generating requirements.txt" + +CURR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +UV_CUSTOM_COMPILE_COMMAND="./generate_requirements.sh" \ + uv pip compile ${CURR_DIR}/requirements.in --no-cache --upgrade -o ${CURR_DIR}/requirements.txt diff --git a/cookbook/examples/apps/whatsapp_chat_agent/readme.md b/cookbook/examples/apps/whatsapp_chat_agent/readme.md index ea77e21262..c0b0b96f40 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/readme.md +++ b/cookbook/examples/apps/whatsapp_chat_agent/readme.md @@ -18,10 +18,10 @@ This is a WhatsApp chatbot that provides stock market insights and financial adv pip install -r requirements.txt ``` -2. **Set up ngrok** +2. **Set up ngrok (for development testing only)** - Download and install ngrok from https://ngrok.com/download - - Sign up for a free account and get your authtoken + - Sign up for a free account and get your auth-token - Authenticate ngrok with your token: ```bash ngrok config add-authtoken YOUR_AUTH_TOKEN @@ -40,7 +40,10 @@ pip install -r requirements.txt ```env WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id +WHATSAPP_RECIPIENT_WAID=phone_number_with_country_code # e.g. +1234567890 +WHATSAPP_WEBHOOK_URL=your_webhook_url WHATSAPP_VERIFY_TOKEN=your_custom_verify_token # Can be any string you choose +WHATSAPP_WEBHOOK_URL=your_webhook_url OPENAI_API_KEY=your_openai_api_key ``` diff --git a/cookbook/examples/apps/whatsapp_chat_agent/requirements.in b/cookbook/examples/apps/whatsapp_chat_agent/requirements.in new file mode 100644 index 0000000000..7361cb95e7 --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/requirements.in @@ -0,0 +1,10 @@ +fastapi +uvicorn +python-dotenv +requests +yfinance +openai +agno +python-multipart +aiohttp +SQLAlchemy diff --git a/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt b/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt index 00e787fd52..1f75b7943d 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt +++ b/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt @@ -1,10 +1,172 @@ -fastapi>=0.68.0 -uvicorn>=0.15.0 -python-dotenv>=0.19.0 -requests>=2.26.0 -yfinance>=0.1.63 -openai>=1.0.0 -agno>=0.1.0 -python-multipart>=0.0.5 -aiohttp>=3.8.0 -SQLAlchemy>=1.4.0 +# This file was autogenerated by uv via the following command: +# ./generate_requirements.sh +agno==1.1.9 + # via -r requirements.in +aiohappyeyeballs==2.5.0 + # via aiohttp +aiohttp==3.11.13 + # via -r requirements.in +aiosignal==1.3.2 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.8.0 + # via + # httpx + # openai + # starlette +attrs==25.1.0 + # via aiohttp +beautifulsoup4==4.13.3 + # via yfinance +certifi==2025.1.31 + # via + # httpcore + # httpx + # requests +charset-normalizer==3.4.1 + # via requests +click==8.1.8 + # via + # typer + # uvicorn +distro==1.9.0 + # via openai +docstring-parser==0.16 + # via agno +fastapi==0.115.11 + # via -r requirements.in +frozendict==2.4.6 + # via yfinance +frozenlist==1.5.0 + # via + # aiohttp + # aiosignal +gitdb==4.0.12 + # via gitpython +gitpython==3.1.44 + # via agno +h11==0.14.0 + # via + # httpcore + # uvicorn +httpcore==1.0.7 + # via httpx +httpx==0.28.1 + # via + # agno + # openai +idna==3.10 + # via + # anyio + # httpx + # requests + # yarl +jiter==0.8.2 + # via openai +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +multidict==6.1.0 + # via + # aiohttp + # yarl +multitasking==0.0.11 + # via yfinance +numpy==2.2.3 + # via + # pandas + # yfinance +openai==1.65.5 + # via -r requirements.in +pandas==2.2.3 + # via yfinance +peewee==3.17.9 + # via yfinance +platformdirs==4.3.6 + # via yfinance +propcache==0.3.0 + # via + # aiohttp + # yarl +pydantic==2.10.6 + # via + # agno + # fastapi + # openai + # pydantic-settings +pydantic-core==2.27.2 + # via pydantic +pydantic-settings==2.8.1 + # via agno +pygments==2.19.1 + # via rich +python-dateutil==2.9.0.post0 + # via pandas +python-dotenv==1.0.1 + # via + # -r requirements.in + # agno + # pydantic-settings +python-multipart==0.0.20 + # via + # -r requirements.in + # agno +pytz==2025.1 + # via + # pandas + # yfinance +pyyaml==6.0.2 + # via agno +requests==2.32.3 + # via + # -r requirements.in + # yfinance +rich==13.9.4 + # via + # agno + # typer +shellingham==1.5.4 + # via typer +six==1.17.0 + # via python-dateutil +smmap==5.0.2 + # via gitdb +sniffio==1.3.1 + # via + # anyio + # openai +soupsieve==2.6 + # via beautifulsoup4 +sqlalchemy==2.0.38 + # via -r requirements.in +starlette==0.46.1 + # via fastapi +tomli==2.2.1 + # via agno +tqdm==4.67.1 + # via openai +typer==0.15.2 + # via agno +typing-extensions==4.12.2 + # via + # agno + # anyio + # beautifulsoup4 + # fastapi + # openai + # pydantic + # pydantic-core + # sqlalchemy + # typer +tzdata==2025.1 + # via pandas +urllib3==2.3.0 + # via requests +uvicorn==0.34.0 + # via -r requirements.in +yarl==1.18.3 + # via aiohttp +yfinance==0.2.54 + # via -r requirements.in diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 88b41f20f9..5fd2d9dffe 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -192,7 +192,7 @@ def send_template_message_sync( } if components: - data["template"]["components"] = components # type: ignore[index] + data["template"]["components"] = components # type: ignore[index] try: response = self._send_message_sync(data) From c4290b281f8555f2d16602449f2dbb18da46dec2 Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 19:30:00 +0400 Subject: [PATCH 08/19] Update WhatsApp chat agent README with `.envrc` configuration and ngrok domain details - Change environment configuration from `.env` to `.envrc` for better environment variable management - Add instructions for using a static ngrok domain - Remove duplicate `WHATSAPP_WEBHOOK_URL` environment variable - Improve documentation clarity for webhook and ngrok setup --- .../apps/whatsapp_chat_agent/readme.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/readme.md b/cookbook/examples/apps/whatsapp_chat_agent/readme.md index c0b0b96f40..7a35576faa 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/readme.md +++ b/cookbook/examples/apps/whatsapp_chat_agent/readme.md @@ -35,16 +35,16 @@ pip install -r requirements.txt - Get your WhatsApp Business Account ID and Phone Number ID 4. **Environment Variables** - Create a `.env` file in the project root with the following variables: - -```env -WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token -WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id -WHATSAPP_RECIPIENT_WAID=phone_number_with_country_code # e.g. +1234567890 -WHATSAPP_WEBHOOK_URL=your_webhook_url -WHATSAPP_VERIFY_TOKEN=your_custom_verify_token # Can be any string you choose -WHATSAPP_WEBHOOK_URL=your_webhook_url -OPENAI_API_KEY=your_openai_api_key + Create a `.envrc` file in the project root with the following variables: + +```bash +export WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token +export WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id +export WHATSAPP_RECIPIENT_WAID=phone_number_with_country_code # e.g. +1234567890 +export WHATSAPP_WEBHOOK_URL=your_webhook_url +export WHATSAPP_VERIFY_TOKEN=your_custom_verify_token # Can be any string you choose +export WHATSAPP_WEBHOOK_URL=your_webhook_url +export OPENAI_API_KEY=your_openai_api_key ``` ## Running the Application @@ -91,7 +91,7 @@ ngrok http 8000 ## Important Notes -- The ngrok URL changes every time you restart ngrok (unless you have a paid account) +- The ngrok URL changes every time you restart ngrok, You can also use a static ngrok URL by running `ngrok http 8000 --domain=your-custom-domain.com`, you can get a custom domain from [here](https://dashboard.ngrok.com/domains) - You'll need to update the Webhook URL in the Meta Developer Portal whenever the ngrok URL changes - Keep your WHATSAPP_ACCESS_TOKEN and other credentials secure - The bot stores conversation history in a SQLite database in the `tmp` directory From 3c005d06c20778196bce6f8d2b6cd82e9bf20908 Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 19:43:59 +0400 Subject: [PATCH 09/19] Refactor WhatsApp chat agent logging and environment setup - Remove verbose logging in webhook verification and server startup - Remove dotenv loading from WhatsApp tools - Minor formatting improvements in README and code --- .../examples/apps/whatsapp_chat_agent/readme.md | 2 +- .../apps/whatsapp_chat_agent/whatsapp_chat_agent.py | 8 -------- libs/agno/agno/tools/whatsapp.py | 13 ------------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/readme.md b/cookbook/examples/apps/whatsapp_chat_agent/readme.md index 7a35576faa..717e549c84 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/readme.md +++ b/cookbook/examples/apps/whatsapp_chat_agent/readme.md @@ -68,7 +68,7 @@ ngrok http 8000 - Set up Webhooks for your WhatsApp Business Account - Use the ngrok URL + "/webhook" as your Callback URL - Use your WHATSAPP_VERIFY_TOKEN as the Verify Token - - Subscribe to the messages webhook + - Subscribe to the `messages` webhook ## Testing the Bot diff --git a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py index d468e6e25c..26260e31a4 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py @@ -62,8 +62,6 @@ async def verify_webhook(request: Request): token = request.query_params.get("hub.verify_token") challenge = request.query_params.get("hub.challenge") - logger.info(f"Webhook verification request - Mode: {mode}, Token: {token}") - if mode == "subscribe" and token == VERIFY_TOKEN: if not challenge: raise HTTPException(status_code=400, detail="No challenge received") @@ -121,10 +119,4 @@ async def handle_message(request: Request): import uvicorn logger.info("Starting WhatsApp Bot Server") - logger.info(f"Webhook URL: {WEBHOOK_URL}") - logger.info(f"Verify Token: {VERIFY_TOKEN}") - logger.info( - "Make sure your .env file contains WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID" - ) - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 5fd2d9dffe..00e8766162 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -3,14 +3,10 @@ from typing import Any, Dict, List, Optional import httpx -from dotenv import load_dotenv from agno.tools import Toolkit from agno.utils.log import logger -# Try to load from both .env and .envrc -load_dotenv() - class WhatsAppTools(Toolkit): """WhatsApp Business API toolkit for sending messages.""" @@ -201,12 +197,3 @@ def send_template_message_sync( except httpx.HTTPStatusError as e: logger.error(f"Failed to send WhatsApp template message: {e}") raise - - # Keep the async methods for compatibility but mark them as internal - async def send_text_message(self, *args, **kwargs): - """Internal async version - use send_text_message_sync instead""" - return self.send_text_message_sync(*args, **kwargs) - - async def send_template_message(self, *args, **kwargs): - """Internal async version - use send_template_message_sync instead""" - return self.send_template_message_sync(*args, **kwargs) From ff4ea2e4c406fed6985111a42644ccb34bb69456 Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 19:49:28 +0400 Subject: [PATCH 10/19] Fix WhatsApp tools environment variable retrieval - Correct duplicate environment variable checks for access token, phone number ID, recipient, and version - Ensure consistent fallback to specific WhatsApp-prefixed environment variables --- libs/agno/agno/tools/whatsapp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 00e8766162..0c604c5f62 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -31,21 +31,21 @@ def __init__( super().__init__(name="whatsapp") # Core credentials - self.access_token = access_token or os.getenv("WHATSAPP_ACCESS_TOKEN") or os.getenv("ACCESS_TOKEN") + self.access_token = access_token or os.getenv("WHATSAPP_ACCESS_TOKEN") or os.getenv("WHATSAPP_ACCESS_TOKEN") if not self.access_token: logger.error("WHATSAPP_ACCESS_TOKEN not set. Please set the WHATSAPP_ACCESS_TOKEN environment variable.") - self.phone_number_id = phone_number_id or os.getenv("WHATSAPP_PHONE_NUMBER_ID") or os.getenv("PHONE_NUMBER_ID") + self.phone_number_id = phone_number_id or os.getenv("WHATSAPP_PHONE_NUMBER_ID") or os.getenv("WHATSAPP_PHONE_NUMBER_ID") if not self.phone_number_id: logger.error( "WHATSAPP_PHONE_NUMBER_ID not set. Please set the WHATSAPP_PHONE_NUMBER_ID environment variable." ) # Optional default recipient - self.default_recipient = recipient_waid or os.getenv("WHATSAPP_RECIPIENT_WAID") or os.getenv("RECIPIENT_WAID") + self.default_recipient = recipient_waid or os.getenv("WHATSAPP_RECIPIENT_WAID") or os.getenv("WHATSAPP_RECIPIENT_WAID") # API version - self.version = version or os.getenv("WHATSAPP_VERSION") or os.getenv("VERSION", "v22.0") + self.version = version or os.getenv("WHATSAPP_VERSION") or os.getenv("WHATSAPP_VERSION", "v22.0") # Register methods that can be used by the agent self.register(self.send_text_message_sync) From 3c8c7382594cd66c0c13c47160f34f0645e67733 Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 21:02:39 +0400 Subject: [PATCH 11/19] Rename WhatsApp chat agent script and update README - Rename `whatsapp_chat_agent.py` to `app.py` - Update README to reflect the new script name in startup instructions --- .../apps/whatsapp_chat_agent/__init__.py | 0 .../apps/whatsapp_chat_agent/agents.py | 58 +++++++++++++++++++ .../{whatsapp_chat_agent.py => app.py} | 52 ++--------------- .../apps/whatsapp_chat_agent/readme.md | 2 +- 4 files changed, 65 insertions(+), 47 deletions(-) create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/__init__.py create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/agents.py rename cookbook/examples/apps/whatsapp_chat_agent/{whatsapp_chat_agent.py => app.py} (60%) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/__init__.py b/cookbook/examples/apps/whatsapp_chat_agent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cookbook/examples/apps/whatsapp_chat_agent/agents.py b/cookbook/examples/apps/whatsapp_chat_agent/agents.py new file mode 100644 index 0000000000..6080229dc5 --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/agents.py @@ -0,0 +1,58 @@ +import logging +import os + +from agno.agent import Agent +from agno.models.openai import OpenAIChat +from agno.storage.agent.sqlite import SqliteAgentStorage +from agno.tools.whatsapp import WhatsAppTools +from agno.tools.yfinance import YFinanceTools +from dotenv import load_dotenv + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Load environment variables +load_dotenv() + +# Configure constants +VERIFY_TOKEN = os.getenv("WHATSAPP_VERIFY_TOKEN") +if not VERIFY_TOKEN: + raise ValueError("WHATSAPP_VERIFY_TOKEN must be set in .envrc") + +WEBHOOK_URL = os.getenv("WHATSAPP_WEBHOOK_URL") +if not WEBHOOK_URL: + raise ValueError("WHATSAPP_WEBHOOK_URL must be set in .envrc") + +AGENT_STORAGE_FILE = "tmp/whatsapp_agents.db" + +def get_whatsapp_agent() -> Agent: + """Returns an instance of the WhatsApp Agent. + + Returns: + Agent: The configured WhatsApp agent instance. + """ + # Initialize WhatsApp tools + whatsapp = WhatsAppTools() + + # Create and return the agent + return Agent( + name="WhatsApp Assistant", + model=OpenAIChat(id="gpt-4o"), + tools=[ + whatsapp, + YFinanceTools( + stock_price=True, + analyst_recommendations=True, + stock_fundamentals=True, + historical_prices=True, + company_info=True, + company_news=True, + ), + ], + storage=SqliteAgentStorage(table_name="whatsapp_agent", db_file=AGENT_STORAGE_FILE), + add_history_to_messages=True, + num_history_responses=3, + markdown=True, + description="You are a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses.", + ) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py b/cookbook/examples/apps/whatsapp_chat_agent/app.py similarity index 60% rename from cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py rename to cookbook/examples/apps/whatsapp_chat_agent/app.py index 26260e31a4..c8bddbf704 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/whatsapp_chat_agent.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/app.py @@ -1,55 +1,15 @@ import logging -import os - -from agno.agent import Agent -from agno.models.openai import OpenAIChat -from agno.storage.agent.sqlite import SqliteAgentStorage -from agno.tools.whatsapp import WhatsAppTools -from agno.tools.yfinance import YFinanceTools -from dotenv import load_dotenv + from fastapi import FastAPI, HTTPException, Request from fastapi.responses import PlainTextResponse +from agents import VERIFY_TOKEN, get_whatsapp_agent + # Configure logging -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Load environment variables -load_dotenv() - -# Configure constants -VERIFY_TOKEN = os.getenv("WHATSAPP_VERIFY_TOKEN") -if not VERIFY_TOKEN: - raise ValueError("WHATSAPP_VERIFY_TOKEN must be set in .envrc") - -WEBHOOK_URL = os.getenv("WHATSAPP_WEBHOOK_URL") -if not WEBHOOK_URL: - raise ValueError("WHATSAPP_WEBHOOK_URL must be set in .envrc") - -AGENT_STORAGE_FILE = "tmp/whatsapp_agents.db" - -# Initialize WhatsApp tools and agent -whatsapp = WhatsAppTools() -agent = Agent( - name="WhatsApp Assistant", - model=OpenAIChat(id="gpt-4o"), - tools=[ - whatsapp, - YFinanceTools( - stock_price=True, - analyst_recommendations=True, - stock_fundamentals=True, - historical_prices=True, - company_info=True, - company_news=True, - ), - ], - storage=SqliteAgentStorage(table_name="whatsapp_agent", db_file=AGENT_STORAGE_FILE), - add_history_to_messages=True, - num_history_responses=3, - markdown=True, - description="You are a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses.", -) +# Initialize agent +agent = get_whatsapp_agent() # Create FastAPI app app = FastAPI() @@ -103,7 +63,7 @@ async def handle_message(request: Request): # Generate and send response response = agent.run(message_text) - whatsapp.send_text_message_sync( + agent.tools[0].send_text_message_sync( recipient=phone_number, text=response.content ) logger.info(f"Response sent to {phone_number}") diff --git a/cookbook/examples/apps/whatsapp_chat_agent/readme.md b/cookbook/examples/apps/whatsapp_chat_agent/readme.md index 717e549c84..8e1c9753cf 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/readme.md +++ b/cookbook/examples/apps/whatsapp_chat_agent/readme.md @@ -52,7 +52,7 @@ export OPENAI_API_KEY=your_openai_api_key 1. **Start the FastAPI server** ```bash -python whatsapp_chat_agent.py +python app.py ``` 2. **Start ngrok** From 163a6d5a0c26b1db63e74b7ed9c8ad97723978ef Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 21:03:08 +0400 Subject: [PATCH 12/19] format --- cookbook/examples/apps/whatsapp_chat_agent/agents.py | 5 ++++- cookbook/examples/apps/whatsapp_chat_agent/app.py | 3 +-- libs/agno/agno/tools/whatsapp.py | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/agents.py b/cookbook/examples/apps/whatsapp_chat_agent/agents.py index 6080229dc5..594cd862a8 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/agents.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/agents.py @@ -26,6 +26,7 @@ AGENT_STORAGE_FILE = "tmp/whatsapp_agents.db" + def get_whatsapp_agent() -> Agent: """Returns an instance of the WhatsApp Agent. @@ -50,7 +51,9 @@ def get_whatsapp_agent() -> Agent: company_news=True, ), ], - storage=SqliteAgentStorage(table_name="whatsapp_agent", db_file=AGENT_STORAGE_FILE), + storage=SqliteAgentStorage( + table_name="whatsapp_agent", db_file=AGENT_STORAGE_FILE + ), add_history_to_messages=True, num_history_responses=3, markdown=True, diff --git a/cookbook/examples/apps/whatsapp_chat_agent/app.py b/cookbook/examples/apps/whatsapp_chat_agent/app.py index c8bddbf704..eac2eddea3 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/app.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/app.py @@ -1,10 +1,9 @@ import logging +from agents import VERIFY_TOKEN, get_whatsapp_agent from fastapi import FastAPI, HTTPException, Request from fastapi.responses import PlainTextResponse -from agents import VERIFY_TOKEN, get_whatsapp_agent - # Configure logging logger = logging.getLogger(__name__) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 0c604c5f62..8981d060ed 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -35,14 +35,18 @@ def __init__( if not self.access_token: logger.error("WHATSAPP_ACCESS_TOKEN not set. Please set the WHATSAPP_ACCESS_TOKEN environment variable.") - self.phone_number_id = phone_number_id or os.getenv("WHATSAPP_PHONE_NUMBER_ID") or os.getenv("WHATSAPP_PHONE_NUMBER_ID") + self.phone_number_id = ( + phone_number_id or os.getenv("WHATSAPP_PHONE_NUMBER_ID") or os.getenv("WHATSAPP_PHONE_NUMBER_ID") + ) if not self.phone_number_id: logger.error( "WHATSAPP_PHONE_NUMBER_ID not set. Please set the WHATSAPP_PHONE_NUMBER_ID environment variable." ) # Optional default recipient - self.default_recipient = recipient_waid or os.getenv("WHATSAPP_RECIPIENT_WAID") or os.getenv("WHATSAPP_RECIPIENT_WAID") + self.default_recipient = ( + recipient_waid or os.getenv("WHATSAPP_RECIPIENT_WAID") or os.getenv("WHATSAPP_RECIPIENT_WAID") + ) # API version self.version = version or os.getenv("WHATSAPP_VERSION") or os.getenv("WHATSAPP_VERSION", "v22.0") From 9fd42e0da241a87682146c4020bf4743bc23bf96 Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 21:27:30 +0400 Subject: [PATCH 13/19] Reorder parameters in WhatsApp send_text_message_sync method - Swap order of `recipient` and `text` parameters for better readability - Update method docstring to match new parameter order --- libs/agno/agno/tools/whatsapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 8981d060ed..feb92dc5fd 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -116,12 +116,12 @@ def _send_message_sync(self, data: Dict[str, Any]) -> Dict[str, Any]: response.raise_for_status() return response.json() - def send_text_message_sync(self, recipient: Optional[str] = None, text: str = "", preview_url: bool = False) -> str: + def send_text_message_sync(self, text: str = "", recipient: Optional[str] = None, preview_url: bool = False) -> str: """Send a text message to a WhatsApp user (synchronous version). Args: - recipient: Recipient's WhatsApp ID or phone number (e.g., "+1234567890"). If not provided, uses default_recipient text: The text message to send + recipient: Recipient's WhatsApp ID or phone number (e.g., "+1234567890"). If not provided, uses default_recipient preview_url: Whether to generate previews for links in the message Returns: From 8e78e1ee2ddd39dec770ae5daee9ac1f7d94440f Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 21:30:25 +0400 Subject: [PATCH 14/19] Add detailed logging to WhatsApp API request method - Enhance debugging by logging request and response details - Log URL, headers, request data, status code, response headers, and response body - Use json.dumps for better readability of logged data --- libs/agno/agno/tools/whatsapp.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index feb92dc5fd..3abc7cbe72 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -86,8 +86,20 @@ async def _send_message_async(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: API response as dictionary """ + url = self._get_messages_url() + headers = self._get_headers() + + logger.debug(f"Sending WhatsApp request to URL: {url}") + logger.debug(f"Request data: {json.dumps(data, indent=2)}") + logger.debug(f"Headers: {json.dumps(headers, indent=2)}") + async with httpx.AsyncClient() as client: - response = await client.post(self._get_messages_url(), headers=self._get_headers(), json=data) + response = await client.post(url, headers=headers, json=data) + + logger.debug(f"Response status code: {response.status_code}") + logger.debug(f"Response headers: {dict(response.headers)}") + logger.debug(f"Response body: {response.text}") + response.raise_for_status() return response.json() From f403242782d22f01c87bbf53fb063ac5f5e5371c Mon Sep 17 00:00:00 2001 From: ansub Date: Mon, 10 Mar 2025 21:32:54 +0400 Subject: [PATCH 15/19] Add async support to WhatsApp toolkit - Introduce `async_mode` parameter to enable async message sending methods - Implement `send_text_message_async` and `send_template_message_async` methods - Update toolkit initialization to conditionally register sync or async methods - Enhance configuration logging to include async mode status --- libs/agno/agno/tools/whatsapp.py | 107 +++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index 3abc7cbe72..d959dbadd5 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -19,6 +19,7 @@ def __init__( phone_number_id: Optional[str] = None, version: str = "v22.0", recipient_waid: Optional[str] = None, + async_mode: bool = False, ): """Initialize WhatsApp toolkit. @@ -27,6 +28,7 @@ def __init__( phone_number_id: WhatsApp Business Account phone number ID version: API version to use recipient_waid: Default recipient WhatsApp ID (optional) + async_mode: Whether to use async methods (default: False) """ super().__init__(name="whatsapp") @@ -48,12 +50,17 @@ def __init__( recipient_waid or os.getenv("WHATSAPP_RECIPIENT_WAID") or os.getenv("WHATSAPP_RECIPIENT_WAID") ) - # API version + # API version and mode self.version = version or os.getenv("WHATSAPP_VERSION") or os.getenv("WHATSAPP_VERSION", "v22.0") + self.async_mode = async_mode - # Register methods that can be used by the agent - self.register(self.send_text_message_sync) - self.register(self.send_template_message_sync) + # Register methods that can be used by the agent based on mode + if self.async_mode: + self.register(self.send_text_message_async) + self.register(self.send_template_message_async) + else: + self.register(self.send_text_message_sync) + self.register(self.send_template_message_sync) # Log configuration status self._log_config_status() @@ -65,7 +72,11 @@ def _log_config_status(self): "access_token": bool(self.access_token), "phone_number_id": bool(self.phone_number_id), }, - "Optional settings": {"default_recipient": bool(self.default_recipient), "api_version": self.version}, + "Optional settings": { + "default_recipient": bool(self.default_recipient), + "api_version": self.version, + "async_mode": self.async_mode + }, } logger.debug(f"WhatsApp toolkit configuration status: {json.dumps(config_status, indent=2)}") @@ -213,3 +224,89 @@ def send_template_message_sync( except httpx.HTTPStatusError as e: logger.error(f"Failed to send WhatsApp template message: {e}") raise + + async def send_text_message_async(self, text: str = "", recipient: Optional[str] = None, preview_url: bool = False) -> str: + """Send a text message to a WhatsApp user (asynchronous version). + + Args: + text: The text message to send + recipient: Recipient's WhatsApp ID or phone number (e.g., "+1234567890"). If not provided, uses default_recipient + preview_url: Whether to generate previews for links in the message + + Returns: + Success message with message ID + """ + # Use default recipient if none provided + if recipient is None: + if not self.default_recipient: + raise ValueError("No recipient provided and no default recipient set") + recipient = self.default_recipient + logger.debug(f"Using default recipient: {recipient}") + + logger.debug(f"Sending WhatsApp message to {recipient}: {text}") + logger.debug(f"Current config - Phone Number ID: {self.phone_number_id}, Version: {self.version}") + + data = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": recipient, + "type": "text", + "text": {"preview_url": preview_url, "body": text}, + } + + try: + response = await self._send_message_async(data) + message_id = response.get("messages", [{}])[0].get("id", "unknown") + logger.debug(f"Full API response: {json.dumps(response, indent=2)}") + return f"Message sent successfully! Message ID: {message_id}" + except httpx.HTTPStatusError as e: + logger.error(f"Failed to send WhatsApp message: {e}") + logger.error(f"Error response: {e.response.text if hasattr(e, 'response') else 'No response text'}") + raise + except Exception as e: + logger.error(f"Unexpected error sending WhatsApp message: {str(e)}") + raise + + async def send_template_message_async( + self, + recipient: Optional[str] = None, + template_name: str = "", + language_code: str = "en_US", + components: Optional[List[Dict[str, Any]]] = None, + ) -> str: + """Send a template message to a WhatsApp user (asynchronous version). + + Args: + recipient: Recipient's WhatsApp ID or phone number (e.g., "+1234567890"). If not provided, uses default_recipient + template_name: Name of the template to use + language_code: Language code for the template (e.g., "en_US") + components: Optional list of template components (header, body, buttons) + + Returns: + Success message with message ID + """ + # Use default recipient if none provided + if recipient is None: + if not self.default_recipient: + raise ValueError("No recipient provided and no default recipient set") + recipient = self.default_recipient + + logger.debug(f"Sending WhatsApp template message to {recipient}: {template_name}") + + data = { + "messaging_product": "whatsapp", + "to": recipient, + "type": "template", + "template": {"name": template_name, "language": {"code": language_code}}, + } + + if components: + data["template"]["components"] = components # type: ignore[index] + + try: + response = await self._send_message_async(data) + message_id = response.get("messages", [{}])[0].get("id", "unknown") + return f"Template message sent successfully! Message ID: {message_id}" + except httpx.HTTPStatusError as e: + logger.error(f"Failed to send WhatsApp template message: {e}") + raise From 237245ffbe8fc1bf76fc4c100da0308c1c97e38e Mon Sep 17 00:00:00 2001 From: ansub Date: Wed, 12 Mar 2025 20:14:19 +0400 Subject: [PATCH 16/19] Refactor WhatsApp chat agent to use DuckDuckGo for web searches - Replaced YFinanceTools with DuckDuckGoTools for improved search capabilities. - Updated agent description to reflect the new functionality and interaction style. - Removed duplicate webhook URL export from README for clarity. --- .../examples/apps/whatsapp_chat_agent/agents.py | 13 +++---------- .../examples/apps/whatsapp_chat_agent/readme.md | 1 - libs/agno/agno/tools/whatsapp.py | 6 ++++-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/agents.py b/cookbook/examples/apps/whatsapp_chat_agent/agents.py index 594cd862a8..4113771e35 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/agents.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/agents.py @@ -4,8 +4,8 @@ from agno.agent import Agent from agno.models.openai import OpenAIChat from agno.storage.agent.sqlite import SqliteAgentStorage +from agno.tools.duckduckgo import DuckDuckGoTools from agno.tools.whatsapp import WhatsAppTools -from agno.tools.yfinance import YFinanceTools from dotenv import load_dotenv # Configure logging @@ -42,14 +42,7 @@ def get_whatsapp_agent() -> Agent: model=OpenAIChat(id="gpt-4o"), tools=[ whatsapp, - YFinanceTools( - stock_price=True, - analyst_recommendations=True, - stock_fundamentals=True, - historical_prices=True, - company_info=True, - company_news=True, - ), + DuckDuckGoTools(), ], storage=SqliteAgentStorage( table_name="whatsapp_agent", db_file=AGENT_STORAGE_FILE @@ -57,5 +50,5 @@ def get_whatsapp_agent() -> Agent: add_history_to_messages=True, num_history_responses=3, markdown=True, - description="You are a financial advisor and can help with stock-related queries. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. don't add markdown to your responses.", + description="You are a whatsapp chat agent. You will respond like how people talk to each other on whatsapp, with short sentences and simple language. If user asks you something which requires a web search, use the DuckDuckGoTools to search the web.", ) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/readme.md b/cookbook/examples/apps/whatsapp_chat_agent/readme.md index 8e1c9753cf..db54763183 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/readme.md +++ b/cookbook/examples/apps/whatsapp_chat_agent/readme.md @@ -43,7 +43,6 @@ export WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id export WHATSAPP_RECIPIENT_WAID=phone_number_with_country_code # e.g. +1234567890 export WHATSAPP_WEBHOOK_URL=your_webhook_url export WHATSAPP_VERIFY_TOKEN=your_custom_verify_token # Can be any string you choose -export WHATSAPP_WEBHOOK_URL=your_webhook_url export OPENAI_API_KEY=your_openai_api_key ``` diff --git a/libs/agno/agno/tools/whatsapp.py b/libs/agno/agno/tools/whatsapp.py index d959dbadd5..247c820d84 100644 --- a/libs/agno/agno/tools/whatsapp.py +++ b/libs/agno/agno/tools/whatsapp.py @@ -75,7 +75,7 @@ def _log_config_status(self): "Optional settings": { "default_recipient": bool(self.default_recipient), "api_version": self.version, - "async_mode": self.async_mode + "async_mode": self.async_mode, }, } logger.debug(f"WhatsApp toolkit configuration status: {json.dumps(config_status, indent=2)}") @@ -225,7 +225,9 @@ def send_template_message_sync( logger.error(f"Failed to send WhatsApp template message: {e}") raise - async def send_text_message_async(self, text: str = "", recipient: Optional[str] = None, preview_url: bool = False) -> str: + async def send_text_message_async( + self, text: str = "", recipient: Optional[str] = None, preview_url: bool = False + ) -> str: """Send a text message to a WhatsApp user (asynchronous version). Args: From 2a0155380820a933531160da2a73387f4e0bd10d Mon Sep 17 00:00:00 2001 From: ansub Date: Wed, 12 Mar 2025 21:49:04 +0400 Subject: [PATCH 17/19] Update WhatsApp Chat Agent README and tools documentation - Revised README to reflect the new AI agent functionality and improved setup instructions. - Added detailed steps for obtaining WhatsApp credentials and configuring the environment. - Enhanced error handling and troubleshooting sections for better user guidance. - Updated `whatsapp_tools.py` to include links for easier navigation and clarified environment variable setup. --- .../apps/whatsapp_chat_agent/readme.md | 182 ++++++++++++------ cookbook/tools/whatsapp_tools.py | 23 +-- 2 files changed, 132 insertions(+), 73 deletions(-) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/readme.md b/cookbook/examples/apps/whatsapp_chat_agent/readme.md index db54763183..f2ed2e623b 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/readme.md +++ b/cookbook/examples/apps/whatsapp_chat_agent/readme.md @@ -1,96 +1,158 @@ -# WhatsApp Chat Agent with Stock Market Insights +# WhatsApp Business API Integration with AI Agent -This is a WhatsApp chatbot that provides stock market insights and financial advice using the WhatsApp Business API. The bot is built using FastAPI and can be run locally using ngrok for development and testing. +This is a WhatsApp chatbot that automatically responds to incoming messages using an AI agent. The bot runs on FastAPI and uses the WhatsApp Business API to handle message interactions. + +## Features + +- Automatically responds to any incoming WhatsApp messages +- Uses AI to generate contextual responses +- Handles webhook verification for WhatsApp Business API +- Supports secure HTTPS communication +- Logs all interactions for monitoring ## Prerequisites - Python 3.7+ -- ngrok account (free tier works fine) +- ngrok account (for development/testing) - WhatsApp Business API access - Meta Developer account - OpenAI API key -## Setup Instructions +## Getting WhatsApp Credentials -1. **Install Dependencies** +1. **Create Meta Developer Account**: -```bash -pip install -r requirements.txt -``` + - Go to [Meta Developer Portal](https://developers.facebook.com/) and create an account + - Create a new app at [Meta Apps Dashboard](https://developers.facebook.com/apps/) + - Enable WhatsApp integration for your app -2. **Set up ngrok (for development testing only)** +2. **Set Up WhatsApp Business API**: - - Download and install ngrok from https://ngrok.com/download - - Sign up for a free account and get your auth-token - - Authenticate ngrok with your token: - ```bash - ngrok config add-authtoken YOUR_AUTH_TOKEN - ``` + - Go to your app's WhatsApp Setup page + - Find your WhatsApp Business Account ID in Business Settings + - Get your Phone Number ID from the WhatsApp > Getting Started page + - Generate a permanent access token in App Dashboard > WhatsApp > API Setup -3. **Create a Meta Developer Account** +3. **Test Environment Setup**: + - Note: Initially, you can only send messages to numbers registered in your test environment + - For production, you'll need to submit your app for review - - Go to https://developers.facebook.com/ - - Create a new app - - Set up WhatsApp in your app - - Get your WhatsApp Business Account ID and Phone Number ID +## Environment Setup -4. **Environment Variables** - Create a `.envrc` file in the project root with the following variables: +Create a `.envrc` file in the project root with these variables: ```bash -export WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token -export WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id -export WHATSAPP_RECIPIENT_WAID=phone_number_with_country_code # e.g. +1234567890 -export WHATSAPP_WEBHOOK_URL=your_webhook_url -export WHATSAPP_VERIFY_TOKEN=your_custom_verify_token # Can be any string you choose +# From Meta Developer Portal +export WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token # From App Dashboard > WhatsApp > API Setup +export WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id # From WhatsApp > Getting Started +export WHATSAPP_WEBHOOK_URL=your_webhook_url # Your ngrok URL + /webhook +export WHATSAPP_VERIFY_TOKEN=your_verify_token # Any secure string you choose + +# For OpenAI integration export OPENAI_API_KEY=your_openai_api_key ``` +## Installation + +1. Install dependencies: + +```bash +pip install -r requirements.txt +``` + +2. Set up your environment variables in `.envrc`: + +```bash +source .envrc +``` + ## Running the Application -1. **Start the FastAPI server** +You need to run two components: + +1. **The ngrok tunnel** (in one terminal): ```bash -python app.py +ngrok http --domain=your-domain.ngrok-free.app 8000 ``` -2. **Start ngrok** - In a new terminal window: +2. **The FastAPI server** (in another terminal): ```bash -ngrok http 8000 +python app.py ``` -3. **Configure Webhook** - - Copy the HTTPS URL provided by ngrok (e.g., https://xxxx-xx-xx-xxx-xx.ngrok.io) - - Go to your Meta Developer Portal - - Set up Webhooks for your WhatsApp Business Account - - Use the ngrok URL + "/webhook" as your Callback URL - - Use your WHATSAPP_VERIFY_TOKEN as the Verify Token - - Subscribe to the `messages` webhook - -## Testing the Bot - -1. Send a message to your WhatsApp Business number -2. The bot should respond with stock market insights based on your query -3. You can ask questions about: - - Stock prices - - Company information - - Analyst recommendations - - Stock fundamentals - - Historical prices - - Company news +## WhatsApp Business Setup + +1. Go to Meta Developer Portal +2. Set up your WhatsApp Business account +3. Configure the webhook: + - URL: Your ngrok URL + "/webhook" (e.g., https://your-domain.ngrok-free.app/webhook) + - Verify Token: Same as WHATSAPP_VERIFY_TOKEN in your .envrc + - Subscribe to the 'messages' webhook field + +## How It Works + +1. When someone sends a message to your WhatsApp Business number: + + - The message is received via webhook + - The AI agent processes the message + - A response is automatically generated and sent back + +2. The agent can: + - Process incoming text messages + - Generate contextual responses + - Log all interactions + +## Monitoring + +The application logs important events: + +- Server start/stop +- Incoming messages +- Response generation +- Message delivery status + +Check the console output for logs. + +## Error Handling + +The application includes error handling for: + +- Invalid webhook verification +- Message processing errors +- API communication issues + +## Security Notes + +- Keep your environment variables secure +- Don't commit `.envrc` to version control +- Use HTTPS for all communications +- Regularly rotate your access tokens ## Troubleshooting -- Make sure all environment variables are properly set -- Check the FastAPI logs for any errors -- Verify that ngrok is running and the webhook URL is correctly configured -- Ensure your WhatsApp Business API is properly set up and the phone number is verified +Common issues: + +1. Webhook verification failing: + + - Check your VERIFY_TOKEN matches + - Ensure ngrok is running + - Verify webhook URL is correct + +2. Messages not being received: + + - Check webhook subscription status + - Verify WhatsApp Business API access + +3. No responses being sent: + - Verify OpenAI API key + - Check WhatsApp access token + +## Support -## Important Notes +For issues and questions: -- The ngrok URL changes every time you restart ngrok, You can also use a static ngrok URL by running `ngrok http 8000 --domain=your-custom-domain.com`, you can get a custom domain from [here](https://dashboard.ngrok.com/domains) -- You'll need to update the Webhook URL in the Meta Developer Portal whenever the ngrok URL changes -- Keep your WHATSAPP_ACCESS_TOKEN and other credentials secure -- The bot stores conversation history in a SQLite database in the `tmp` directory +1. Check the logs for error messages +2. Review Meta's WhatsApp Business API documentation +3. Verify your API credentials and tokens diff --git a/cookbook/tools/whatsapp_tools.py b/cookbook/tools/whatsapp_tools.py index 1d1342cc5f..71cdc4d6a2 100644 --- a/cookbook/tools/whatsapp_tools.py +++ b/cookbook/tools/whatsapp_tools.py @@ -6,31 +6,28 @@ you'll need to complete these setup steps: 1. Create Meta Developer Account - - Go to Meta Developer Portal (https://developers.facebook.com/) and create a new account - - Create a new app at Meta Apps Dashboard (https://developers.facebook.com/apps/) - - Enable WhatsApp integration for your app (https://developers.facebook.com/docs/whatsapp/cloud-api/get-started) + - Go to [Meta Developer Portal](https://developers.facebook.com/) and create a new account + - Create a new app at [Meta Apps Dashboard](https://developers.facebook.com/apps/) + - Enable WhatsApp integration for your app [here](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started) 2. Set Up WhatsApp Business API - - Get your WhatsApp Business Account ID from Business Settings (https://business.facebook.com/settings/) - - Generate a permanent access token in System Users (https://business.facebook.com/settings/system-users) - - Set up a test phone number (https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#testing-your-app) - - Create a message template in Meta Business Manager (https://business.facebook.com/wa/manage/message-templates/) + You can get your WhatsApp Business Account ID from [Business Settings](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started) 3. Configure Environment - Set these environment variables: - WHATSAPP_ACCESS_TOKEN=your_access_token # Permanent access token from System Users - WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id # Your WhatsApp test phone number ID + WHATSAPP_ACCESS_TOKEN=your_access_token # Access Token + WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id # Phone Number ID + WHATSAPP_RECIPIENT_WAID=your_recipient_waid # Recipient WhatsApp ID (e.g. 1234567890) + WHATSAPP_VERSION=your_whatsapp_version # WhatsApp API Version (e.g. v22.0) Important Notes: -- WhatsApp has a 24-hour messaging window policy -- You can only send free-form messages to users who have messaged you in the last 24 hours - For first-time outreach, you must use pre-approved message templates - (https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates) + [here](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates) - Test messages can only be sent to numbers that are registered in your test environment The example below shows how to send a template message using Agno's WhatsApp tools. For more complex use cases, check out the WhatsApp Cloud API documentation: -https://developers.facebook.com/docs/whatsapp/cloud-api/overview +[here](https://developers.facebook.com/docs/whatsapp/cloud-api/overview) """ from agno.agent import Agent From 1dd0ebca85128cdd2ec83d73d67b42d6a6cea630 Mon Sep 17 00:00:00 2001 From: ansub Date: Thu, 13 Mar 2025 11:17:28 +0400 Subject: [PATCH 18/19] fix --- cookbook/examples/apps/whatsapp_chat_agent/requirements.in | 2 +- cookbook/examples/apps/whatsapp_chat_agent/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/requirements.in b/cookbook/examples/apps/whatsapp_chat_agent/requirements.in index 7361cb95e7..c638ce6039 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/requirements.in +++ b/cookbook/examples/apps/whatsapp_chat_agent/requirements.in @@ -2,7 +2,7 @@ fastapi uvicorn python-dotenv requests -yfinance +duckduckgo-search openai agno python-multipart diff --git a/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt b/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt index 1f75b7943d..03a9354be1 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt +++ b/cookbook/examples/apps/whatsapp_chat_agent/requirements.txt @@ -168,5 +168,5 @@ uvicorn==0.34.0 # via -r requirements.in yarl==1.18.3 # via aiohttp -yfinance==0.2.54 +duckduckgo-search==7.3.2 # via -r requirements.in From 446ccbcab71be3de5df62396ecc54692b92a3349 Mon Sep 17 00:00:00 2001 From: ansub Date: Thu, 13 Mar 2025 15:34:38 +0400 Subject: [PATCH 19/19] Add webhook signature validation to WhatsApp chat agent --- .../examples/apps/whatsapp_chat_agent/app.py | 10 ++ .../apps/whatsapp_chat_agent/security.py | 59 ++++++++++++ .../tests/test_security.py | 94 +++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/security.py create mode 100644 cookbook/examples/apps/whatsapp_chat_agent/tests/test_security.py diff --git a/cookbook/examples/apps/whatsapp_chat_agent/app.py b/cookbook/examples/apps/whatsapp_chat_agent/app.py index eac2eddea3..392bfbe0b0 100644 --- a/cookbook/examples/apps/whatsapp_chat_agent/app.py +++ b/cookbook/examples/apps/whatsapp_chat_agent/app.py @@ -3,6 +3,7 @@ from agents import VERIFY_TOKEN, get_whatsapp_agent from fastapi import FastAPI, HTTPException, Request from fastapi.responses import PlainTextResponse +from security import validate_webhook_signature # Configure logging logger = logging.getLogger(__name__) @@ -33,6 +34,15 @@ async def verify_webhook(request: Request): async def handle_message(request: Request): """Handle incoming WhatsApp messages""" try: + # Get raw payload for signature validation + payload = await request.body() + signature = request.headers.get("X-Hub-Signature-256") + + # Validate webhook signature + if not validate_webhook_signature(payload, signature): + logger.warning("Invalid webhook signature") + raise HTTPException(status_code=403, detail="Invalid signature") + body = await request.json() # Validate webhook data diff --git a/cookbook/examples/apps/whatsapp_chat_agent/security.py b/cookbook/examples/apps/whatsapp_chat_agent/security.py new file mode 100644 index 0000000000..01f62fdfab --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/security.py @@ -0,0 +1,59 @@ +import hmac +import hashlib +import os +from typing import Optional + +def is_development_mode() -> bool: + """Check if the application is running in development mode.""" + return os.getenv('APP_ENV', 'development').lower() == 'development' + +def get_app_secret() -> str: + """ + Get the WhatsApp app secret from environment variables. + In development mode, returns a dummy secret if WHATSAPP_APP_SECRET is not set. + """ + app_secret = os.getenv('WHATSAPP_APP_SECRET') + + if not app_secret: + if is_development_mode(): + app_secret = 'dummy_secret_for_local_development' + print("WARNING: Using dummy secret for local development. Do not use in production!") + else: + raise ValueError('WHATSAPP_APP_SECRET environment variable is not set in production mode') + + return app_secret + +def validate_webhook_signature(payload: bytes, signature_header: Optional[str]) -> bool: + """ + Validate the webhook payload using SHA256 signature. + In development mode, signature validation can be bypassed. + + Args: + payload: The raw request payload bytes + signature_header: The X-Hub-Signature-256 header value + + Returns: + bool: True if signature is valid or in development mode, False otherwise + """ + # In development mode, we can bypass signature validation + if is_development_mode(): + if not signature_header: + print("WARNING: Bypassing signature validation in development mode") + return True + + if not signature_header or not signature_header.startswith('sha256='): + return False + + app_secret = get_app_secret() + expected_signature = signature_header.split('sha256=')[1] + + # Calculate signature + hmac_obj = hmac.new( + app_secret.encode('utf-8'), + payload, + hashlib.sha256 + ) + calculated_signature = hmac_obj.hexdigest() + + # Compare signatures using constant-time comparison + return hmac.compare_digest(calculated_signature, expected_signature) diff --git a/cookbook/examples/apps/whatsapp_chat_agent/tests/test_security.py b/cookbook/examples/apps/whatsapp_chat_agent/tests/test_security.py new file mode 100644 index 0000000000..58084ee087 --- /dev/null +++ b/cookbook/examples/apps/whatsapp_chat_agent/tests/test_security.py @@ -0,0 +1,94 @@ +import os +import hmac +import hashlib +from cookbook.examples.apps.whatsapp_chat_agent.security import is_development_mode, get_app_secret, validate_webhook_signature + +def generate_signature(payload: bytes, secret: str) -> str: + """Helper function to generate a valid signature.""" + hmac_obj = hmac.new( + secret.encode('utf-8'), + payload, + hashlib.sha256 + ) + return f"sha256={hmac_obj.hexdigest()}" + +def test_development_mode(): + """Test security behavior in development mode.""" + print("\n=== Testing Development Mode ===") + + # Ensure we're in development mode + os.environ['APP_ENV'] = 'development' + if 'WHATSAPP_APP_SECRET' in os.environ: + del os.environ['WHATSAPP_APP_SECRET'] + + print(f"Is development mode? {is_development_mode()}") + + # Test 1: Get app secret without setting WHATSAPP_APP_SECRET + try: + secret = get_app_secret() + print("✅ Got dummy secret in development mode:", secret) + except ValueError as e: + print("❌ Failed to get dummy secret:", e) + + # Test 2: Validate signature with no signature header + payload = b"test message" + result = validate_webhook_signature(payload, None) + print("✅ Signature bypass in dev mode:", result) + + # Test 3: Validate with invalid signature + result = validate_webhook_signature(payload, "sha256=invalid") + print("✅ Invalid signature check in dev mode:", result is False) + +def test_production_mode(): + """Test security behavior in production mode.""" + print("\n=== Testing Production Mode ===") + + # Switch to production mode + os.environ['APP_ENV'] = 'production' + test_secret = "test_production_secret" + os.environ['WHATSAPP_APP_SECRET'] = test_secret + + print(f"Is development mode? {is_development_mode()}") + + # Test 1: Get app secret + try: + secret = get_app_secret() + print("✅ Got production secret:", secret == test_secret) + except ValueError as e: + print("❌ Failed to get production secret:", e) + + # Test 2: Validate correct signature + payload = b"test message" + valid_signature = generate_signature(payload, test_secret) + result = validate_webhook_signature(payload, valid_signature) + print("✅ Valid signature check in prod mode:", result) + + # Test 3: Validate invalid signature + result = validate_webhook_signature(payload, "sha256=invalid") + print("✅ Invalid signature check in prod mode:", result is False) + + # Test 4: Try with no signature + result = validate_webhook_signature(payload, None) + print("✅ No signature check in prod mode:", result is False) + +def test_production_mode_no_secret(): + """Test production mode without secret configured.""" + print("\n=== Testing Production Mode (No Secret) ===") + + # Switch to production mode without secret + os.environ['APP_ENV'] = 'production' + if 'WHATSAPP_APP_SECRET' in os.environ: + del os.environ['WHATSAPP_APP_SECRET'] + + try: + secret = get_app_secret() + print("❌ Should not get secret without configuration") + except ValueError as e: + print("✅ Correctly failed without secret:", str(e)) + +if __name__ == "__main__": + # Run all tests + test_development_mode() + test_production_mode() + test_production_mode_no_secret() + print("\nTests completed!")