From 8f4038f807e301ee8719afeaec7498e987138b36 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Fri, 25 Oct 2024 17:18:59 +0530 Subject: [PATCH 01/23] Added Twilio as a tool --- cookbook/tools/twilio_tools.py | 36 ++++++ phi/tools/twilio.py | 211 +++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 cookbook/tools/twilio_tools.py create mode 100644 phi/tools/twilio.py diff --git a/cookbook/tools/twilio_tools.py b/cookbook/tools/twilio_tools.py new file mode 100644 index 000000000..ce55b22ea --- /dev/null +++ b/cookbook/tools/twilio_tools.py @@ -0,0 +1,36 @@ +from phi.agent import Agent +from phi.tools.twilio import TwilioTools + +""" +Example showing how to use the Twilio Tools with Phi. + +Requirements: +- Twilio Account SID and Auth Token (get from console.twilio.com) +- A Twilio phone number +- pip install twilio + +Usage: +- Set the following environment variables: + export TWILIO_ACCOUNT_SID="your_account_sid" + export TWILIO_AUTH_TOKEN="your_auth_token" + +- Or provide them when creating the TwilioTools instance +""" + +twilio_tools = TwilioTools() + +agent = Agent( + instructions=[ + """You can help users by: + - Sending SMS messages + - Making phone calls + - Checking message history + - Generating TwiML responses using twilio_tools + + Always confirm before sending messages or making calls.""" + ], + tools=[twilio_tools], + show_tool_calls=True, +) + +agent.print_response("Can you send an SMS saying 'Your package has arrived' to +1234567890?") diff --git a/phi/tools/twilio.py b/phi/tools/twilio.py new file mode 100644 index 000000000..c3a2d7210 --- /dev/null +++ b/phi/tools/twilio.py @@ -0,0 +1,211 @@ +from os import getenv +import re +from typing import Optional, Dict, Any, List +from phi.tools import Toolkit +from phi.utils.log import logger + + +try: + from twilio.rest import Client + from twilio.base.exceptions import TwilioRestException + from twilio.twiml.voice_response import VoiceResponse +except ImportError: + raise ImportError("`twilio` not installed.") + + +class TwilioTools(Toolkit): + def __init__( + self, + account_sid: Optional[str] = None, + auth_token: Optional[str] = None, + api_key: Optional[str] = None, + api_secret: Optional[str] = None, + region: Optional[str] = None, + edge: Optional[str] = None, + debug: bool = False, + ): + """Initialize the Twilio toolkit. + + Args: + account_sid: Twilio Account SID + auth_token: Twilio Auth Token + api_key: Twilio API Key (alternative to auth_token) + api_secret: Twilio API Secret (alternative to auth_token) + region: Twilio region (e.g. 'au1') + edge: Twilio edge location (e.g. 'sydney') + debug: Enable debug logging + """ + super().__init__(name="twilio") + self.account_sid = account_sid or getenv("TWILIO_ACCOUNT_SID") + self.auth_token = auth_token or getenv("TWILIO_AUTH_TOKEN") + self.api_key = api_key or getenv("TWILIO_API_KEY") + self.api_secret = api_secret or getenv("TWILIO_API_SECRET") + self.region = region or getenv("TWILIO_REGION") + self.edge = edge or getenv("TWILIO_EDGE") + + if not self.account_sid: + logger.warning("No Twilio Account SID provided") + + if self.api_key and self.api_secret: + self.client = Client( + self.api_key, + self.api_secret, + self.account_sid, + region=self.region, + edge=self.edge, + ) + else: + self.client = Client( + self.account_sid, + self.auth_token, + region=self.region, + edge=self.edge, + ) + + logger.info("Twilio client initialized") + + if debug: + import logging + + logging.basicConfig() + self.client.http_client.logger.setLevel(logging.INFO) + + self.register(self.send_sms) + self.register(self.make_call) + self.register(self.get_call_details) + self.register(self.list_messages) + self.register(self.generate_twiml) + + @staticmethod + def validate_phone_number(phone: str) -> bool: + """Validate E.164 phone number format""" + return bool(re.match(r"^\+[1-9]\d{1,14}$", phone)) + + def send_sms(self, to: str, from_: str, body: str) -> str: + """ + Send an SMS message using Twilio. + + Args: + to: Recipient phone number (E.164 format) + from_: Sender phone number (must be a Twilio number) + body: Message content + + Returns: + str: Message SID if successful, error message if failed + """ + try: + if not self.validate_phone_number(to): + return "Error: 'to' number must be in E.164 format (e.g., +1234567890)" + if not self.validate_phone_number(from_): + return "Error: 'from_' number must be in E.164 format (e.g., +1234567890)" + if not body or len(body.strip()) == 0: + return "Error: Message body cannot be empty" + + message = self.client.messages.create(to=to, from_=from_, body=body) + logger.info(f"SMS sent. SID: {message.sid}, to: {to}") + return f"Message sent successfully. SID: {message.sid}" + except TwilioRestException as e: + logger.error(f"Failed to send SMS to {to}: {e}") + return f"Error sending message: {str(e)}" + + def make_call(self, to: str, from_: str, url: str) -> str: + """ + Initiate a phone call using Twilio. + + Args: + to: Recipient phone number (E.164 format) + from_: Caller phone number (must be a Twilio number) + url: TwiML URL for call handling + + Returns: + str: Call SID if successful, error message if failed + """ + try: + if not self.validate_phone_number(to): + return "Error: 'to' number must be in E.164 format (e.g., +1234567890)" + if not self.validate_phone_number(from_): + return "Error: 'from_' number must be in E.164 format (e.g., +1234567890)" + if not url or len(url.strip()) == 0: + return "Error: TwiML URL cannot be empty" + + call = self.client.calls.create(to=to, from_=from_, url=url) + logger.info(f"Call initiated. SID: {call.sid}, to: {to}") + return f"Call initiated successfully. SID: {call.sid}" + except TwilioRestException as e: + logger.error(f"Failed to make call to {to}: {e}") + return f"Error making call: {str(e)}" + + def get_call_details(self, call_sid: str) -> Dict[str, Any]: + """ + Get details about a specific call. + + Args: + call_sid: The SID of the call to lookup + + Returns: + Dict: Call details including status, duration, etc. + """ + try: + call = self.client.calls(call_sid).fetch() + logger.info(f"Fetched details for call SID: {call_sid}") + return { + "to": call.to, + "from": call.from_, + "status": call.status, + "duration": call.duration, + "direction": call.direction, + "price": call.price, + "start_time": str(call.start_time), + "end_time": str(call.end_time), + } + except TwilioRestException as e: + logger.error(f"Failed to fetch call details for SID {call_sid}: {e}") + return {"error": str(e)} + + def list_messages(self, limit: int = 20) -> List[Dict[str, Any]]: + """ + List recent SMS messages. + + Args: + limit: Maximum number of messages to return + + Returns: + List[Dict]: List of message details + """ + try: + messages = [] + for message in self.client.messages.list(limit=limit): + messages.append( + { + "sid": message.sid, + "to": message.to, + "from": message.from_, + "body": message.body, + "status": message.status, + "date_sent": str(message.date_sent), + } + ) + logger.info(f"Retrieved {len(messages)} messages") + return messages + except TwilioRestException as e: + logger.error(f"Failed to list messages: {e}") + return [{"error": str(e)}] + + def generate_twiml(self, message: str) -> str: + """ + Generate TwiML for voice responses. + + Args: + message: Message to be spoken + + Returns: + str: TwiML XML string + """ + try: + response = VoiceResponse() + response.say(message) + logger.info("Generated TwiML response") + return str(response) + except Exception as e: + logger.error(f"Failed to generate TwiML: {e}") + return f"Error generating TwiML: {str(e)}" From c69a360e7bb8debfcd6cf5e5b3303f33edfaa286 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Fri, 25 Oct 2024 19:12:23 +0530 Subject: [PATCH 02/23] added requested changes --- cookbook/tools/twilio_tools.py | 6 +++-- phi/tools/twilio.py | 46 +++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/cookbook/tools/twilio_tools.py b/cookbook/tools/twilio_tools.py index ce55b22ea..75727a516 100644 --- a/cookbook/tools/twilio_tools.py +++ b/cookbook/tools/twilio_tools.py @@ -1,5 +1,6 @@ from phi.agent import Agent from phi.tools.twilio import TwilioTools +from phi.model.openai import OpenAIChat """ Example showing how to use the Twilio Tools with Phi. @@ -17,7 +18,6 @@ - Or provide them when creating the TwilioTools instance """ -twilio_tools = TwilioTools() agent = Agent( instructions=[ @@ -29,8 +29,10 @@ Always confirm before sending messages or making calls.""" ], - tools=[twilio_tools], + model=OpenAIChat(id="gpt-4o-mini"), + tools=[TwilioTools()], show_tool_calls=True, + markdown=True, ) agent.print_response("Can you send an SMS saying 'Your package has arrived' to +1234567890?") diff --git a/phi/tools/twilio.py b/phi/tools/twilio.py index c3a2d7210..999370e36 100644 --- a/phi/tools/twilio.py +++ b/phi/tools/twilio.py @@ -10,7 +10,7 @@ from twilio.base.exceptions import TwilioRestException from twilio.twiml.voice_response import VoiceResponse except ImportError: - raise ImportError("`twilio` not installed.") + raise ImportError("`twilio` not installed. Please install it using `pip install twilio`.") class TwilioTools(Toolkit): @@ -26,41 +26,63 @@ def __init__( ): """Initialize the Twilio toolkit. + Two authentication methods are supported: + 1. Account SID + Auth Token + 2. Account SID + API Key + API Secret + Args: account_sid: Twilio Account SID - auth_token: Twilio Auth Token - api_key: Twilio API Key (alternative to auth_token) - api_secret: Twilio API Secret (alternative to auth_token) - region: Twilio region (e.g. 'au1') - edge: Twilio edge location (e.g. 'sydney') + auth_token: Twilio Auth Token (Method 1) + api_key: Twilio API Key (Method 2) + api_secret: Twilio API Secret (Method 2) + region: Optional Twilio region (e.g. 'au1') + edge: Optional Twilio edge location (e.g. 'sydney') debug: Enable debug logging """ super().__init__(name="twilio") + + # Get credentials from environment if not provided self.account_sid = account_sid or getenv("TWILIO_ACCOUNT_SID") self.auth_token = auth_token or getenv("TWILIO_AUTH_TOKEN") self.api_key = api_key or getenv("TWILIO_API_KEY") self.api_secret = api_secret or getenv("TWILIO_API_SECRET") + + # Optional region and edge self.region = region or getenv("TWILIO_REGION") self.edge = edge or getenv("TWILIO_EDGE") + # Validate required credentials if not self.account_sid: - logger.warning("No Twilio Account SID provided") + logger.error("TWILIO_ACCOUNT_SID not set. Please set the TWILIO_ACCOUNT_SID environment variable.") + raise ValueError("Twilio Account SID is required") + # Initialize client based on provided authentication method if self.api_key and self.api_secret: + # Method 2: API Key + Secret + logger.info("Initializing Twilio client with API Key authentication") self.client = Client( self.api_key, self.api_secret, self.account_sid, - region=self.region, - edge=self.edge, + region=self.region or None, + edge=self.edge or None, ) - else: + elif self.auth_token: + # Method 1: Auth Token + logger.info("Initializing Twilio client with Auth Token authentication") self.client = Client( self.account_sid, self.auth_token, - region=self.region, - edge=self.edge, + region=self.region or None, + edge=self.edge or None, ) + else: + logger.error( + "Neither (auth_token) nor (api_key and api_secret) provided. " + "Please set either TWILIO_AUTH_TOKEN or both TWILIO_API_KEY and TWILIO_API_SECRET environment variables." + ) + raise ValueError("No valid authentication method provided") + logger.info("Twilio client initialized") From cd590ee89840e14b43d937a6165faf79a7caacc5 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Fri, 25 Oct 2024 19:14:42 +0530 Subject: [PATCH 03/23] added name --- cookbook/tools/twilio_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cookbook/tools/twilio_tools.py b/cookbook/tools/twilio_tools.py index 75727a516..c60ec460a 100644 --- a/cookbook/tools/twilio_tools.py +++ b/cookbook/tools/twilio_tools.py @@ -20,6 +20,7 @@ agent = Agent( + name="Twilio Agent", instructions=[ """You can help users by: - Sending SMS messages From 5d9b7181e98e9ffbf6fb01c1074f1e11000f4714 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Fri, 25 Oct 2024 19:33:56 +0530 Subject: [PATCH 04/23] removed call logic and valueerror --- cookbook/tools/twilio_tools.py | 7 ++-- phi/tools/twilio.py | 58 +--------------------------------- 2 files changed, 4 insertions(+), 61 deletions(-) diff --git a/cookbook/tools/twilio_tools.py b/cookbook/tools/twilio_tools.py index c60ec460a..2d7aeb678 100644 --- a/cookbook/tools/twilio_tools.py +++ b/cookbook/tools/twilio_tools.py @@ -24,13 +24,12 @@ instructions=[ """You can help users by: - Sending SMS messages - - Making phone calls - Checking message history - - Generating TwiML responses using twilio_tools + - getting call details - Always confirm before sending messages or making calls.""" + Always confirm before sending messages.""" ], - model=OpenAIChat(id="gpt-4o-mini"), + model=OpenAIChat(id="gpt-4o"), tools=[TwilioTools()], show_tool_calls=True, markdown=True, diff --git a/phi/tools/twilio.py b/phi/tools/twilio.py index 999370e36..d8ec97ddd 100644 --- a/phi/tools/twilio.py +++ b/phi/tools/twilio.py @@ -8,7 +8,6 @@ try: from twilio.rest import Client from twilio.base.exceptions import TwilioRestException - from twilio.twiml.voice_response import VoiceResponse except ImportError: raise ImportError("`twilio` not installed. Please install it using `pip install twilio`.") @@ -46,7 +45,7 @@ def __init__( self.auth_token = auth_token or getenv("TWILIO_AUTH_TOKEN") self.api_key = api_key or getenv("TWILIO_API_KEY") self.api_secret = api_secret or getenv("TWILIO_API_SECRET") - + # Optional region and edge self.region = region or getenv("TWILIO_REGION") self.edge = edge or getenv("TWILIO_EDGE") @@ -54,12 +53,10 @@ def __init__( # Validate required credentials if not self.account_sid: logger.error("TWILIO_ACCOUNT_SID not set. Please set the TWILIO_ACCOUNT_SID environment variable.") - raise ValueError("Twilio Account SID is required") # Initialize client based on provided authentication method if self.api_key and self.api_secret: # Method 2: API Key + Secret - logger.info("Initializing Twilio client with API Key authentication") self.client = Client( self.api_key, self.api_secret, @@ -69,7 +66,6 @@ def __init__( ) elif self.auth_token: # Method 1: Auth Token - logger.info("Initializing Twilio client with Auth Token authentication") self.client = Client( self.account_sid, self.auth_token, @@ -81,10 +77,6 @@ def __init__( "Neither (auth_token) nor (api_key and api_secret) provided. " "Please set either TWILIO_AUTH_TOKEN or both TWILIO_API_KEY and TWILIO_API_SECRET environment variables." ) - raise ValueError("No valid authentication method provided") - - - logger.info("Twilio client initialized") if debug: import logging @@ -93,10 +85,8 @@ def __init__( self.client.http_client.logger.setLevel(logging.INFO) self.register(self.send_sms) - self.register(self.make_call) self.register(self.get_call_details) self.register(self.list_messages) - self.register(self.generate_twiml) @staticmethod def validate_phone_number(phone: str) -> bool: @@ -130,33 +120,6 @@ def send_sms(self, to: str, from_: str, body: str) -> str: logger.error(f"Failed to send SMS to {to}: {e}") return f"Error sending message: {str(e)}" - def make_call(self, to: str, from_: str, url: str) -> str: - """ - Initiate a phone call using Twilio. - - Args: - to: Recipient phone number (E.164 format) - from_: Caller phone number (must be a Twilio number) - url: TwiML URL for call handling - - Returns: - str: Call SID if successful, error message if failed - """ - try: - if not self.validate_phone_number(to): - return "Error: 'to' number must be in E.164 format (e.g., +1234567890)" - if not self.validate_phone_number(from_): - return "Error: 'from_' number must be in E.164 format (e.g., +1234567890)" - if not url or len(url.strip()) == 0: - return "Error: TwiML URL cannot be empty" - - call = self.client.calls.create(to=to, from_=from_, url=url) - logger.info(f"Call initiated. SID: {call.sid}, to: {to}") - return f"Call initiated successfully. SID: {call.sid}" - except TwilioRestException as e: - logger.error(f"Failed to make call to {to}: {e}") - return f"Error making call: {str(e)}" - def get_call_details(self, call_sid: str) -> Dict[str, Any]: """ Get details about a specific call. @@ -212,22 +175,3 @@ def list_messages(self, limit: int = 20) -> List[Dict[str, Any]]: except TwilioRestException as e: logger.error(f"Failed to list messages: {e}") return [{"error": str(e)}] - - def generate_twiml(self, message: str) -> str: - """ - Generate TwiML for voice responses. - - Args: - message: Message to be spoken - - Returns: - str: TwiML XML string - """ - try: - response = VoiceResponse() - response.say(message) - logger.info("Generated TwiML response") - return str(response) - except Exception as e: - logger.error(f"Failed to generate TwiML: {e}") - return f"Error generating TwiML: {str(e)}" From 55b98f29f4bde92ba10dc737a89e61b66893a125 Mon Sep 17 00:00:00 2001 From: SMIT PATEL <147633120+smit23patel@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:59:09 +0530 Subject: [PATCH 05/23] Update assistant.py 1. Error Handling: Wrapped the message prompt and response printing in a try-except block to catch and display any errors that may occur during user interaction. 2. User Feedback: Added a print statement to inform the user if an error occurs, improving the user experience. --- cookbook/assistants/examples/pdf/assistant.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cookbook/assistants/examples/pdf/assistant.py b/cookbook/assistants/examples/pdf/assistant.py index f9d0bd310..7f83e1a98 100644 --- a/cookbook/assistants/examples/pdf/assistant.py +++ b/cookbook/assistants/examples/pdf/assistant.py @@ -46,10 +46,13 @@ def pdf_assistant(new: bool = False, user: str = "user"): print(f"Continuing Run: {run_id}\n") while True: - message = Prompt.ask(f"[bold] :sunglasses: {user} [/bold]") - if message in ("exit", "bye"): - break - assistant.print_response(message, markdown=True) + try: + message = Prompt.ask(f"[bold] :sunglasses: {user} [/bold]") + if message in ("exit", "bye"): + break + assistant.print_response(message, markdown=True) + except Exception as e: + print(f"[red]Error: {e}[/red]") # Added error handling if __name__ == "__main__": From aaf5e6e13bc2a2aaac9d2044da00a0f78eb438b3 Mon Sep 17 00:00:00 2001 From: Shreyas0410 <70795867+Shreyas0410@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:02:29 +0530 Subject: [PATCH 06/23] Update README.md --- cookbook/integrations/singlestore/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/integrations/singlestore/README.md b/cookbook/integrations/singlestore/README.md index 2e5abe67b..cf21aeeff 100644 --- a/cookbook/integrations/singlestore/README.md +++ b/cookbook/integrations/singlestore/README.md @@ -17,7 +17,7 @@ pip install -U pymysql sqlalchemy pypdf openai phidata - For SingleStore -> Note: If using a shared tier, please provide a certificate file for SSL connection [Read more](https://docs.singlestore.com/cloud/connect-to-your-workspace/connect-with-mysql/connect-with-mysql-client/connect-to-singlestore-helios-using-tls-ssl/) +> Note: If using a shared tier, please provide a certificate file for SSL connection [Read more](https://docs.singlestore.com/cloud/connect-to-singlestore/connect-with-mysql/connect-with-mysql-client/connect-to-singlestore-helios-using-tls-ssl/) ```shell export SINGLESTORE_HOST="host" From fd55e3162b8e7cf0fb7c2eab51d0b7c663f63ea8 Mon Sep 17 00:00:00 2001 From: Shreyas0410 <70795867+Shreyas0410@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:04:01 +0530 Subject: [PATCH 07/23] Update README.md --- cookbook/assistants/integrations/singlestore/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/assistants/integrations/singlestore/README.md b/cookbook/assistants/integrations/singlestore/README.md index a4b04b351..5db0c37c1 100644 --- a/cookbook/assistants/integrations/singlestore/README.md +++ b/cookbook/assistants/integrations/singlestore/README.md @@ -17,7 +17,7 @@ pip install -U pymysql sqlalchemy pypdf openai phidata - For SingleStore -> Note: If using a shared tier, please provide a certificate file for SSL connection [Read more](https://docs.singlestore.com/cloud/connect-to-your-workspace/connect-with-mysql/connect-with-mysql-client/connect-to-singlestore-helios-using-tls-ssl/) +> Note: If using a shared tier, please provide a certificate file for SSL connection [Read more](https://docs.singlestore.com/cloud/connect-to-singlestore/connect-with-mysql/connect-with-mysql-client/connect-to-singlestore-helios-using-tls-ssl/) ```shell export SINGLESTORE_HOST="host" From 46ac586b70a81facb5f4897dc43c1b1486e89bda Mon Sep 17 00:00:00 2001 From: Shreyas0410 <70795867+Shreyas0410@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:04:34 +0530 Subject: [PATCH 08/23] Update README.md --- cookbook/assistants/integrations/singlestore/ai_apps/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/assistants/integrations/singlestore/ai_apps/README.md b/cookbook/assistants/integrations/singlestore/ai_apps/README.md index 3234a8b63..236705515 100644 --- a/cookbook/assistants/integrations/singlestore/ai_apps/README.md +++ b/cookbook/assistants/integrations/singlestore/ai_apps/README.md @@ -30,7 +30,7 @@ pip install -r cookbook/integrations/singlestore/ai_apps/requirements.txt - For SingleStore -> Note: If using a shared tier, please provide a certificate file for SSL connection [Read more](https://docs.singlestore.com/cloud/connect-to-your-workspace/connect-with-mysql/connect-with-mysql-client/connect-to-singlestore-helios-using-tls-ssl/) +> Note: If using a shared tier, please provide a certificate file for SSL connection [Read more](https://docs.singlestore.com/cloud/connect-to-singlestore/connect-with-mysql/connect-with-mysql-client/connect-to-singlestore-helios-using-tls-ssl/) ```shell export SINGLESTORE_HOST="host" From 18bbd83898e72582130c93578155fe0a5796157b Mon Sep 17 00:00:00 2001 From: Chaitanya110703 <116812461+Chaitanya110703@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:50:26 +0530 Subject: [PATCH 09/23] Docs: Typo Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrected "Checkout" to "Check out" in [README.md]. This pull request addresses a minor typo found in repository. The typo has been corrected to improve clarity and maintain the quality of the documentation. This change is purely cosmetic and does not affect functionality. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 66e0a3994..a4bebbcbf 100644 --- a/README.md +++ b/README.md @@ -489,7 +489,7 @@ MovieScript( -### Checkout the [cookbook](https://github.com/phidatahq/phidata/tree/main/cookbook) for more examples. +### Check out the [cookbook](https://github.com/phidatahq/phidata/tree/main/cookbook) for more examples. ## Contributions From a010e9bd2e905a745fba8e08dba9080e694862ba Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 29 Oct 2024 21:33:50 +0530 Subject: [PATCH 10/23] added calcom as a tool --- cookbook/tools/calcom_tools.py | 45 ++++++ phi/tools/calcom.py | 241 +++++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 cookbook/tools/calcom_tools.py create mode 100644 phi/tools/calcom.py diff --git a/cookbook/tools/calcom_tools.py b/cookbook/tools/calcom_tools.py new file mode 100644 index 000000000..6f1644a5c --- /dev/null +++ b/cookbook/tools/calcom_tools.py @@ -0,0 +1,45 @@ +from phi.agent import Agent +from phi.tools.calcom import CalCom +from phi.model.openai import OpenAIChat +from datetime import datetime + +""" +Example showing how to use the Cal.com Tools with Phi. + +Requirements: +- Cal.com API key (get from cal.com/settings/developer/api-keys) +- Event Type ID from Cal.com +- pip install requests pytz + +Usage: +- Set the following environment variables: + export CALCOM_API_KEY="your_api_key" + export CALCOM_EVENT_TYPE_ID="your_event_type_id" + +- Or provide them when creating the CalComTools instance +""" + +INSTRUCTONS = f"""You're scheduing assistant. Today is {datetime.now()}. +You can help users by: + - Finding available time slots + - Creating new bookings + - Managing existing bookings (view, reschedule, cancel) + - Getting booking details + - IMPORTANT: In case of rescheduling or cancelling booking, call the get_upcoming_bookings function to get the booking uid. check available slots before making a booking for given time + Always confirm important details before making bookings or changes. +""" + + +agent = Agent( + name="Calendar Assistant", + instructions=[ + INSTRUCTONS + ], + model=OpenAIChat(id="gpt-4"), + tools=[CalCom()], + show_tool_calls=True, + markdown=True, +) + +# Example usage hiddenperson.jp@gmail.com +agent.print_response("Can you please make a booking for tomorrow at 2pm?, my name is Jp and email is jayeshparmar9829@gmail.com") diff --git a/phi/tools/calcom.py b/phi/tools/calcom.py new file mode 100644 index 000000000..d38baee16 --- /dev/null +++ b/phi/tools/calcom.py @@ -0,0 +1,241 @@ +from datetime import datetime +import logging +from os import getenv +from typing import Optional, Dict, Any, List +from phi.tools import Toolkit +from phi.utils.log import logger +try: + import requests + import pytz +except ImportError: + raise ImportError("`requests` and `pytz` not installed. Please install using `pip install requests pytz`") + +class CalCom(Toolkit): + def __init__( + self, + api_key: Optional[str] = None, + event_type_id: Optional[str] = None, + user_timezone: Optional[str] = None, + ): + """Initialize the Cal.com toolkit. + + Args: + api_key: Cal.com API key + event_type_id: Default event type ID for bookings + user_timezone: User's timezone in IANA format (e.g., 'Asia/Kolkata') + """ + super().__init__(name="calcom") + + # Get credentials from environment if not provided + self.api_key = api_key or getenv("CALCOM_API_KEY") + self.event_type_id = event_type_id or getenv("CALCOM_EVENT_TYPE_ID") + + if not self.api_key: + logger.error("CALCOM_API_KEY not set. Please set the CALCOM_API_KEY environment variable.") + if not self.event_type_id: + logger.error("CALCOM_EVENT_TYPE_ID not set. Please set the CALCOM_EVENT_TYPE_ID environment variable.") + + self.event_type_id = int(self.event_type_id) + self.user_timezone = user_timezone or "Asia/Kolkata" + + # Register all methods + self.register(self.get_available_slots) + self.register(self.create_booking) + self.register(self.get_upcoming_bookings) + self.register(self.reschedule_booking) + self.register(self.cancel_booking) + + def _convert_to_user_timezone(self, utc_time: str) -> str: + """Convert UTC time to user's timezone. + + Args: + utc_time: UTC time string + user_timezone: User's timezone (e.g., 'Asia/Kolkata') + + Returns: + str: Formatted time in user's timezone + """ + utc_dt = datetime.fromisoformat(utc_time.replace("Z", "+00:00")) + user_tz = pytz.timezone(self.user_timezone) + user_dt = utc_dt.astimezone(user_tz) + return user_dt.strftime("%Y-%m-%d %H:%M %Z") + + def _get_headers(self, api_version: str = "2024-08-13") -> Dict[str, str]: + """Get headers for Cal.com API requests. + + Args: + api_version: Cal.com API version + + Returns: + Dict[str, str]: Headers dictionary + """ + return { + "Authorization": f"Bearer {self.api_key}", + "cal-api-version": api_version, + "Content-Type": "application/json" + } + + def get_available_slots( + self, + start_date: str, + end_date: str, + ) -> str: + """Get available time slots for booking. + + Args: + start_date: Start date in YYYY-MM-DD format + end_date: End date in YYYY-MM-DD format + user_timezone: User's timezone + event_type_id: Optional specific event type ID + + Returns: + str: Available slots or error message + """ + try: + url = "https://api.cal.com/v2/slots/available" + querystring = { + "startTime": f"{start_date}T00:00:00Z", + "endTime": f"{end_date}T23:59:59Z", + "eventTypeId": self.event_type_id + } + + response = requests.get(url, headers=self._get_headers(), params=querystring) + if response.status_code == 200: + slots = response.json()["data"]["slots"] + available_slots = [] + for date, times in slots.items(): + for slot in times: + user_time = self._convert_to_user_timezone(slot["time"]) + available_slots.append(user_time) + return f"Available slots: {', '.join(available_slots)}" + return f"Failed to fetch slots: {response.text}" + except Exception as e: + logger.error(f"Error fetching available slots: {e}") + return f"Error: {str(e)}" + + def create_booking( + self, + start_time: str, + name: str, + email: str, + ) -> str: + """Create a new booking. + + Args: + start_time: Start time in YYYY-MM-DDTHH:MM:SSZ format + name: Attendee's name + email: Attendee's email + + Returns: + str: Booking confirmation or error message + """ + try: + url = "https://api.cal.com/v2/bookings" + start_time = datetime.fromisoformat(start_time).astimezone(pytz.utc).isoformat(timespec='seconds') + payload = { + "start": start_time, + "eventTypeId": self.event_type_id, + "attendee": { + "name": name, + "email": email, + "timeZone": self.user_timezone + } + } + + response = requests.post(url, json=payload, headers=self._get_headers()) + if response.status_code == 201: + booking_data = response.json()["data"] + user_time = self._convert_to_user_timezone(booking_data["start"]) + return f"Booking created successfully for {user_time}. Booking uid: {booking_data['uid']}" + return f"Failed to create booking: {response.text}" + except Exception as e: + logger.error(f"Error creating booking: {e}") + return f"Error: {str(e)}" + + def get_upcoming_bookings(self, email: str) -> str: + """Get all upcoming bookings for an attendee. + + Args: + email: Attendee's email + + Returns: + str: List of upcoming bookings or error message + """ + try: + url = "https://api.cal.com/v2/bookings" + querystring = {"status": "upcoming", "attendeeEmail": email} + + response = requests.get(url, headers=self._get_headers(), params=querystring) + if response.status_code == 200: + bookings = response.json()["data"] + if not bookings: + return "No upcoming bookings found." + + booking_info = [] + for booking in bookings: + user_time = self._convert_to_user_timezone(booking["start"]) + booking_info.append( + f"uid: {booking['uid']}, Title: {booking['title']}, Time: {user_time}, Status: {booking['status']}" + ) + return "Upcoming bookings:\n" + "\n".join(booking_info) + return f"Failed to fetch bookings: {response.text}" + except Exception as e: + logger.error(f"Error fetching upcoming bookings: {e}") + return f"Error: {str(e)}" + + def reschedule_booking( + self, + booking_uid: str, + new_start_time: str, + reason: str, + ) -> str: + """Reschedule an existing booking. + + Args: + booking_uid: Booking UID to reschedule + new_start_time: New start time in YYYY-MM-DDTHH:MM:SSZ format + reason: Reason for rescheduling + user_timezone: User's timezone + + Returns: + str: Rescheduling confirmation or error message + """ + try: + url = f"https://api.cal.com/v2/bookings/{booking_uid}/reschedule" + new_start_time = datetime.fromisoformat(new_start_time).astimezone(pytz.utc).isoformat(timespec='seconds') + payload = { + "start": new_start_time, + "reschedulingReason": reason + } + + response = requests.post(url, json=payload, headers=self._get_headers()) + if response.status_code == 200: + booking_data = response.json()["data"] + user_time = self._convert_to_user_timezone(booking_data["start"]) + return f"Booking rescheduled to {user_time}. New booking uid: {booking_data['uid']}" + return f"Failed to reschedule booking: {response.text}" + except Exception as e: + logger.error(f"Error rescheduling booking: {e}") + return f"Error: {str(e)}" + + def cancel_booking(self, booking_uid: str, reason: str) -> str: + """Cancel an existing booking. + + Args: + booking_uid: Booking UID to cancel + reason: Reason for cancellation + + Returns: + str: Cancellation confirmation or error message + """ + try: + url = f"https://api.cal.com/v2/bookings/{booking_uid}/cancel" + payload = {"cancellationReason": reason} + + response = requests.post(url, json=payload, headers=self._get_headers()) + if response.status_code == 200: + return "Booking cancelled successfully." + return f"Failed to cancel booking: {response.text}" + except Exception as e: + logger.error(f"Error cancelling booking: {e}") + return f"Error: {str(e)}" From 56c24b4f80849c6314ffc584550009e98f4a6303 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 29 Oct 2024 21:34:08 +0530 Subject: [PATCH 11/23] added calcom as a tool --- phi/tools/calcom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phi/tools/calcom.py b/phi/tools/calcom.py index d38baee16..874db19fc 100644 --- a/phi/tools/calcom.py +++ b/phi/tools/calcom.py @@ -209,7 +209,7 @@ def reschedule_booking( } response = requests.post(url, json=payload, headers=self._get_headers()) - if response.status_code == 200: + if response.status_code == 201: booking_data = response.json()["data"] user_time = self._convert_to_user_timezone(booking_data["start"]) return f"Booking rescheduled to {user_time}. New booking uid: {booking_data['uid']}" From f39377a49af29d67420d5214770207564815df33 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 29 Oct 2024 21:35:23 +0530 Subject: [PATCH 12/23] added calcom as a tool --- cookbook/tools/calcom_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/tools/calcom_tools.py b/cookbook/tools/calcom_tools.py index 6f1644a5c..395b897dd 100644 --- a/cookbook/tools/calcom_tools.py +++ b/cookbook/tools/calcom_tools.py @@ -41,5 +41,5 @@ markdown=True, ) -# Example usage hiddenperson.jp@gmail.com +# Example usage agent.print_response("Can you please make a booking for tomorrow at 2pm?, my name is Jp and email is jayeshparmar9829@gmail.com") From 86d279f8658e3c64093ee03786a1bee35b097c59 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 29 Oct 2024 21:36:27 +0530 Subject: [PATCH 13/23] added calcom as a tool --- cookbook/tools/calcom_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/tools/calcom_tools.py b/cookbook/tools/calcom_tools.py index 395b897dd..d4c6b371f 100644 --- a/cookbook/tools/calcom_tools.py +++ b/cookbook/tools/calcom_tools.py @@ -42,4 +42,4 @@ ) # Example usage -agent.print_response("Can you please make a booking for tomorrow at 2pm?, my name is Jp and email is jayeshparmar9829@gmail.com") +agent.print_response("What are my bookings for tomorrow?") From 608f1ca8faaf588dfec339f0826f5d2865becf4e Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 29 Oct 2024 21:48:35 +0530 Subject: [PATCH 14/23] added calcom as a tool --- phi/tools/calcom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phi/tools/calcom.py b/phi/tools/calcom.py index 874db19fc..b7d617612 100644 --- a/phi/tools/calcom.py +++ b/phi/tools/calcom.py @@ -36,7 +36,7 @@ def __init__( logger.error("CALCOM_EVENT_TYPE_ID not set. Please set the CALCOM_EVENT_TYPE_ID environment variable.") self.event_type_id = int(self.event_type_id) - self.user_timezone = user_timezone or "Asia/Kolkata" + self.user_timezone = user_timezone or "America/New_York" # Register all methods self.register(self.get_available_slots) From 1085f6c636f11d40e175d478b76fd9b77ca767f7 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 29 Oct 2024 21:57:39 +0530 Subject: [PATCH 15/23] format --- cookbook/tools/calcom_tools.py | 6 ++---- phi/tools/calcom.py | 28 +++++++++++----------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/cookbook/tools/calcom_tools.py b/cookbook/tools/calcom_tools.py index d4c6b371f..b2e9cbb40 100644 --- a/cookbook/tools/calcom_tools.py +++ b/cookbook/tools/calcom_tools.py @@ -32,14 +32,12 @@ agent = Agent( name="Calendar Assistant", - instructions=[ - INSTRUCTONS - ], + instructions=[INSTRUCTONS], model=OpenAIChat(id="gpt-4"), tools=[CalCom()], show_tool_calls=True, markdown=True, ) -# Example usage +# Example usage agent.print_response("What are my bookings for tomorrow?") diff --git a/phi/tools/calcom.py b/phi/tools/calcom.py index b7d617612..427b15707 100644 --- a/phi/tools/calcom.py +++ b/phi/tools/calcom.py @@ -1,15 +1,16 @@ from datetime import datetime -import logging from os import getenv -from typing import Optional, Dict, Any, List +from typing import Optional, Dict from phi.tools import Toolkit from phi.utils.log import logger + try: import requests import pytz except ImportError: raise ImportError("`requests` and `pytz` not installed. Please install using `pip install requests pytz`") + class CalCom(Toolkit): def __init__( self, @@ -34,7 +35,7 @@ def __init__( logger.error("CALCOM_API_KEY not set. Please set the CALCOM_API_KEY environment variable.") if not self.event_type_id: logger.error("CALCOM_EVENT_TYPE_ID not set. Please set the CALCOM_EVENT_TYPE_ID environment variable.") - + self.event_type_id = int(self.event_type_id) self.user_timezone = user_timezone or "America/New_York" @@ -72,7 +73,7 @@ def _get_headers(self, api_version: str = "2024-08-13") -> Dict[str, str]: return { "Authorization": f"Bearer {self.api_key}", "cal-api-version": api_version, - "Content-Type": "application/json" + "Content-Type": "application/json", } def get_available_slots( @@ -96,7 +97,7 @@ def get_available_slots( querystring = { "startTime": f"{start_date}T00:00:00Z", "endTime": f"{end_date}T23:59:59Z", - "eventTypeId": self.event_type_id + "eventTypeId": self.event_type_id, } response = requests.get(url, headers=self._get_headers(), params=querystring) @@ -131,15 +132,11 @@ def create_booking( """ try: url = "https://api.cal.com/v2/bookings" - start_time = datetime.fromisoformat(start_time).astimezone(pytz.utc).isoformat(timespec='seconds') + start_time = datetime.fromisoformat(start_time).astimezone(pytz.utc).isoformat(timespec="seconds") payload = { "start": start_time, "eventTypeId": self.event_type_id, - "attendee": { - "name": name, - "email": email, - "timeZone": self.user_timezone - } + "attendee": {"name": name, "email": email, "timeZone": self.user_timezone}, } response = requests.post(url, json=payload, headers=self._get_headers()) @@ -170,7 +167,7 @@ def get_upcoming_bookings(self, email: str) -> str: bookings = response.json()["data"] if not bookings: return "No upcoming bookings found." - + booking_info = [] for booking in bookings: user_time = self._convert_to_user_timezone(booking["start"]) @@ -202,11 +199,8 @@ def reschedule_booking( """ try: url = f"https://api.cal.com/v2/bookings/{booking_uid}/reschedule" - new_start_time = datetime.fromisoformat(new_start_time).astimezone(pytz.utc).isoformat(timespec='seconds') - payload = { - "start": new_start_time, - "reschedulingReason": reason - } + new_start_time = datetime.fromisoformat(new_start_time).astimezone(pytz.utc).isoformat(timespec="seconds") + payload = {"start": new_start_time, "reschedulingReason": reason} response = requests.post(url, json=payload, headers=self._get_headers()) if response.status_code == 201: From 70eca7ba7ebb9fb17db282f491822e338a9a2809 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 29 Oct 2024 22:22:46 +0530 Subject: [PATCH 16/23] format --- phi/tools/calcom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phi/tools/calcom.py b/phi/tools/calcom.py index 427b15707..a8feca8e4 100644 --- a/phi/tools/calcom.py +++ b/phi/tools/calcom.py @@ -15,7 +15,7 @@ class CalCom(Toolkit): def __init__( self, api_key: Optional[str] = None, - event_type_id: Optional[str] = None, + event_type_id: Optional[int] = None, user_timezone: Optional[str] = None, ): """Initialize the Cal.com toolkit. @@ -29,14 +29,14 @@ def __init__( # Get credentials from environment if not provided self.api_key = api_key or getenv("CALCOM_API_KEY") - self.event_type_id = event_type_id or getenv("CALCOM_EVENT_TYPE_ID") + event_type_str = getenv("CALCOM_EVENT_TYPE_ID") + self.event_type_id = event_type_id or int(event_type_str) if event_type_str is not None else 0 if not self.api_key: logger.error("CALCOM_API_KEY not set. Please set the CALCOM_API_KEY environment variable.") if not self.event_type_id: logger.error("CALCOM_EVENT_TYPE_ID not set. Please set the CALCOM_EVENT_TYPE_ID environment variable.") - self.event_type_id = int(self.event_type_id) self.user_timezone = user_timezone or "America/New_York" # Register all methods From 8612e709f420516bf15917556da625b823621582 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Mon, 4 Nov 2024 18:38:21 +0530 Subject: [PATCH 17/23] condition based feature adding --- phi/tools/calcom.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/phi/tools/calcom.py b/phi/tools/calcom.py index a8feca8e4..b3407e767 100644 --- a/phi/tools/calcom.py +++ b/phi/tools/calcom.py @@ -17,6 +17,11 @@ def __init__( api_key: Optional[str] = None, event_type_id: Optional[int] = None, user_timezone: Optional[str] = None, + get_available_slots: bool = True, + create_booking: bool = True, + get_upcoming_bookings: bool = True, + reschedule_booking: bool = True, + cancel_booking: bool = True, ): """Initialize the Cal.com toolkit. @@ -40,11 +45,16 @@ def __init__( self.user_timezone = user_timezone or "America/New_York" # Register all methods - self.register(self.get_available_slots) - self.register(self.create_booking) - self.register(self.get_upcoming_bookings) - self.register(self.reschedule_booking) - self.register(self.cancel_booking) + if get_available_slots: + self.register(self.get_available_slots) + if create_booking: + self.register(self.create_booking) + if get_upcoming_bookings: + self.register(self.get_upcoming_bookings) + if reschedule_booking: + self.register(self.reschedule_booking) + if cancel_booking: + self.register(self.cancel_booking) def _convert_to_user_timezone(self, utc_time: str) -> str: """Convert UTC time to user's timezone. From 38d6e1a08e4354c3fd9afeb7eca29e5f9e7fd8f8 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Mon, 4 Nov 2024 18:41:38 +0530 Subject: [PATCH 18/23] minor requested change --- cookbook/tools/twilio_tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cookbook/tools/twilio_tools.py b/cookbook/tools/twilio_tools.py index 2d7aeb678..19e7f1aec 100644 --- a/cookbook/tools/twilio_tools.py +++ b/cookbook/tools/twilio_tools.py @@ -26,8 +26,7 @@ - Sending SMS messages - Checking message history - getting call details - - Always confirm before sending messages.""" + """ ], model=OpenAIChat(id="gpt-4o"), tools=[TwilioTools()], From f2f35517e3a1dc24913916ea27ad4aa62089cb70 Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 12 Nov 2024 16:51:02 +0530 Subject: [PATCH 19/23] added variables for sender and receiver phone number --- cookbook/tools/twilio_tools.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cookbook/tools/twilio_tools.py b/cookbook/tools/twilio_tools.py index 19e7f1aec..070866d70 100644 --- a/cookbook/tools/twilio_tools.py +++ b/cookbook/tools/twilio_tools.py @@ -1,6 +1,6 @@ from phi.agent import Agent -from phi.tools.twilio import TwilioTools from phi.model.openai import OpenAIChat +from phi.tools.twilio import TwilioTools """ Example showing how to use the Twilio Tools with Phi. @@ -34,4 +34,9 @@ markdown=True, ) -agent.print_response("Can you send an SMS saying 'Your package has arrived' to +1234567890?") +sender_phone_number = "+1234567890" +receiver_phone_number = "+1234567890" + +agent.print_response( + f"Can you send an SMS saying 'Your package has arrived' to {receiver_phone_number} from {sender_phone_number}?" +) From 806322b858ef077fb4cf833615dfb819d26c35fb Mon Sep 17 00:00:00 2001 From: Jayesh Date: Tue, 12 Nov 2024 18:24:36 +0530 Subject: [PATCH 20/23] added timezone in cookbook example --- cookbook/tools/calcom_tools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cookbook/tools/calcom_tools.py b/cookbook/tools/calcom_tools.py index b2e9cbb40..6e2c99762 100644 --- a/cookbook/tools/calcom_tools.py +++ b/cookbook/tools/calcom_tools.py @@ -1,7 +1,8 @@ +from datetime import datetime + from phi.agent import Agent -from phi.tools.calcom import CalCom from phi.model.openai import OpenAIChat -from datetime import datetime +from phi.tools.calcom import CalCom """ Example showing how to use the Cal.com Tools with Phi. @@ -34,7 +35,7 @@ name="Calendar Assistant", instructions=[INSTRUCTONS], model=OpenAIChat(id="gpt-4"), - tools=[CalCom()], + tools=[CalCom(user_timezone="America/New_York")], show_tool_calls=True, markdown=True, ) From 2a753fee69e5d02e87b3cc5eb289cb7fd61b6524 Mon Sep 17 00:00:00 2001 From: ysolanky Date: Wed, 13 Nov 2024 18:07:06 -0500 Subject: [PATCH 21/23] fix-logging-for-gemini-phi-1989 --- phi/model/google/gemini.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/phi/model/google/gemini.py b/phi/model/google/gemini.py index 0f0e5c230..586d58d63 100644 --- a/phi/model/google/gemini.py +++ b/phi/model/google/gemini.py @@ -497,6 +497,9 @@ def response(self, messages: List[Message]) -> ModelResponse: if assistant_message.content is not None: model_response.content = assistant_message.get_content_string() + # -*- Remove parts from messages + [setattr(m, 'parts', None) for m in messages if hasattr(m, 'parts')] + logger.debug("---------- Gemini Response End ----------") return model_response @@ -551,13 +554,14 @@ def response_stream(self, messages: List[Message]) -> Iterator[ModelResponse]: for response in self.invoke_stream(messages=messages): message_data.response_block = response.candidates[0].content message_data.response_role = message_data.response_block.role - message_data.response_parts = message_data.response_block.parts + if message_data.response_block.parts: + message_data.response_parts = message_data.response_block.parts if message_data.response_parts is not None: for part in message_data.response_parts: part_dict = type(part).to_dict(part) - # Yield text if present + # -*- Yield text if present if "text" in part_dict: text = part_dict.get("text") yield ModelResponse(content=text) @@ -567,7 +571,10 @@ def response_stream(self, messages: List[Message]) -> Iterator[ModelResponse]: stream_usage_data.time_to_first_token = response_timer.elapsed logger.debug(f"Time to first token: {stream_usage_data.time_to_first_token:.4f}s") - # Parse function calls + # -*- Skip function calls if there are no parts + if not message_data.response_block.parts and message_data.response_parts: + continue + # -*- Parse function calls if "function_call" in part_dict: message_data.response_tool_calls.append( { @@ -610,4 +617,7 @@ def response_stream(self, messages: List[Message]) -> Iterator[ModelResponse]: if assistant_message.tool_calls is not None and len(assistant_message.tool_calls) > 0 and self.run_tools: yield from self._handle_stream_tool_calls(assistant_message, messages) yield from self.response_stream(messages=messages) + + # -*- Remove parts from messages + [setattr(m, 'parts', None) for m in messages if hasattr(m, 'parts')] logger.debug("---------- Gemini Response End ----------") From 986aee5bbfb24a4f1625d3c34273fea9d3ea5ed9 Mon Sep 17 00:00:00 2001 From: ysolanky Date: Wed, 13 Nov 2024 18:09:36 -0500 Subject: [PATCH 22/23] update --- phi/model/google/gemini.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/phi/model/google/gemini.py b/phi/model/google/gemini.py index 586d58d63..cf5454ebb 100644 --- a/phi/model/google/gemini.py +++ b/phi/model/google/gemini.py @@ -498,7 +498,7 @@ def response(self, messages: List[Message]) -> ModelResponse: model_response.content = assistant_message.get_content_string() # -*- Remove parts from messages - [setattr(m, 'parts', None) for m in messages if hasattr(m, 'parts')] + [setattr(m, "parts", None) for m in messages if hasattr(m, "parts")] # type: ignore logger.debug("---------- Gemini Response End ----------") return model_response @@ -619,5 +619,6 @@ def response_stream(self, messages: List[Message]) -> Iterator[ModelResponse]: yield from self.response_stream(messages=messages) # -*- Remove parts from messages - [setattr(m, 'parts', None) for m in messages if hasattr(m, 'parts')] + [setattr(m, "parts", None) for m in messages if hasattr(m, "parts")] # type: ignore + logger.debug("---------- Gemini Response End ----------") From 0c3bd029799dfe7db23ee85b35c4e8624fb0bbff Mon Sep 17 00:00:00 2001 From: ysolanky Date: Wed, 13 Nov 2024 18:14:10 -0500 Subject: [PATCH 23/23] update --- phi/model/google/gemini.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/phi/model/google/gemini.py b/phi/model/google/gemini.py index cf5454ebb..c81239af0 100644 --- a/phi/model/google/gemini.py +++ b/phi/model/google/gemini.py @@ -498,7 +498,9 @@ def response(self, messages: List[Message]) -> ModelResponse: model_response.content = assistant_message.get_content_string() # -*- Remove parts from messages - [setattr(m, "parts", None) for m in messages if hasattr(m, "parts")] # type: ignore + for m in messages: + if hasattr(m, "parts"): + m.parts = None logger.debug("---------- Gemini Response End ----------") return model_response @@ -619,6 +621,8 @@ def response_stream(self, messages: List[Message]) -> Iterator[ModelResponse]: yield from self.response_stream(messages=messages) # -*- Remove parts from messages - [setattr(m, "parts", None) for m in messages if hasattr(m, "parts")] # type: ignore + for m in messages: + if hasattr(m, "parts"): + m.parts = None logger.debug("---------- Gemini Response End ----------")