From 6c9468186cb9fd0f608539437b9b8717dba0a2eb Mon Sep 17 00:00:00 2001 From: Artsiom <115654725+ArtsiomWB@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:36:43 +0000 Subject: [PATCH] fixed Morgan's suggestions --- src/wandbot/apps/zendesk/__main__.py | 112 ++++++-------------- src/wandbot/apps/zendesk/config.py | 9 ++ src/wandbot/apps/zendesk/extract_by_type.py | 15 +-- 3 files changed, 51 insertions(+), 85 deletions(-) diff --git a/src/wandbot/apps/zendesk/__main__.py b/src/wandbot/apps/zendesk/__main__.py index dc208ef..5f38fc1 100644 --- a/src/wandbot/apps/zendesk/__main__.py +++ b/src/wandbot/apps/zendesk/__main__.py @@ -5,7 +5,7 @@ The system performs the following steps: 1. Creates a new ticket with a predefined question. -2. Enters a loop where it fetches new tickets every 120 seconds. +2. Enters a loop where it fetches new tickets every 600 seconds. 3. For each new ticket, it extracts the question, generates a response, formats the response, and updates the ticket with the response. @@ -41,9 +41,7 @@ def extract_question(ticket: Ticket) -> str: This function performs the following steps: 1. Extracts the description from the ticket. - 2. Converts the description to lower case and replaces newline and carriage return characters with a space. - 3. Removes the string "[discourse post]" from the description. - 4. Truncates the description to a maximum length of 4095 characters. + 2. Chooses what type of ticket we are looking at, and then extracts the ticket depending on the ticket type Args: ticket (Ticket): The ticket object from which the question is to be extracted. @@ -54,26 +52,23 @@ def extract_question(ticket: Ticket) -> str: description = ticket.description if "forum" in ticket.tags: - return discourseExt(description) + return discourse_ext(description) elif "zopim_offline_message" in ticket.tags: - return offlineMessageExt(description) + return offline_msg_ext(description) elif "add_cc_note" in ticket.tags: - return emailMsgExt(description) + return email_msg_ext(description) return question -# TODO: add the necessary format we want to depending on ticket type def format_response(response: str) -> str: """Formats the response to be sent as a ticket comment. This function performs the following steps: 1. Converts the response to a string. - 2. Truncates the response to a maximum length of 2000 characters. - 3. Appends an ellipsis (...) if the response was truncated. - 4. Appends a signature at the end of the response. + 2. Appends a signature at the end of the response. Args: response (str): The response to be formatted. @@ -82,13 +77,9 @@ def format_response(response: str) -> str: str: The formatted response. """ - response = str(response) - max_length = 2000 - # Truncate the response to a maximum length of 2000 characters - if len(response) > max_length: - response = response[:max_length] + "..." - # Append a signature at the end of the response - return response + "\n\n-WandBot 🤖" + responseStr = str(response) + finalResponse = config.DISCBOTINTRO + responseStr + return finalResponse + "\n\n-WandBot 🤖" class ZendeskAIResponseSystem: @@ -100,6 +91,7 @@ class ZendeskAIResponseSystem: Attributes: zenpy_client (Zenpy): The client for interacting with the Zendesk API. api_client (AsyncAPIClient): The client for interacting with the WandBot API. + semaphore (Semaphore): Use semaphore to control how many api calls to wandbot we make """ def __init__(self) -> None: @@ -117,47 +109,8 @@ def __init__(self) -> None: self.zenpy_client = Zenpy(**self.user_creds) self.api_client = AsyncAPIClient(url=config.WANDBOT_API_URL) - def create_new_ticket(self, question_text: str) -> None: - """Creates a new ticket in the Zendesk system. - - This method uses the Zenpy client to create a new ticket in the Zendesk system. The ticket is created with a - predefined subject, status, priority, and tags. The description of the ticket is set to the provided question - text. - - Args: - question_text (str): The text to be used as the description of the ticket. - - Returns: - None - """ - - self.zenpy_client.tickets.create( - Ticket( - subject="WandbotTest4", - description=question_text, - status="new", - priority="low", - tags=["botTest", "zopim_offline_message"], - ) - ) - self.zenpy_client.tickets.create( - Ticket( - subject="WandbotTest4", - description=question_text, - status="new", - priority="low", - tags=["botTest", "zopim_offline_message"], - ) - ) - self.zenpy_client.tickets.create( - Ticket( - subject="WandbotTest4", - description=question_text, - status="new", - priority="low", - tags=["botTest", "forum"], - ) - ) + self.semaphore = asyncio.Semaphore(config.MAX_WANDBOT_REQUESTS) + self.request_interval = config.REQUEST_INTERVAL def fetch_new_tickets(self) -> List[Ticket]: """Fetches new tickets from the Zendesk system. @@ -239,7 +192,6 @@ def gather_feedback(self, ticket: Ticket) -> None: Args: ticket (Ticket): The ticket for which feedback is to be gathered. - Returns: None """ @@ -251,14 +203,11 @@ def gather_feedback(self, ticket: Ticket) -> None: logger.error(f"Error: {e}") async def run(self) -> None: - logger.info(f"WandBot + zendesk is running") - """Runs the Zendesk AI Response System. This method performs the following steps: - 1. Creates a new ticket with a predefined question. - 2. Enters a loop where it fetches new tickets every 120 seconds. - 3. For each new ticket, it extracts the question, generates a response, formats the response, + 1. Enters a loop where it fetches new tickets every 600 seconds. + 2. For each new ticket, it extracts the question, generates a response, formats the response, and updates the ticket with the response. This method is asynchronous and should be run in an event loop. @@ -267,14 +216,13 @@ async def run(self) -> None: None """ - # Create a new ticket with a predefined question - # self.create_new_ticket( - # "Is there a way to programmatically list all projects for a given entity?" - # ) + # after semLimit number of tickets, have a timeout + semLimit = config.MAX_WANDBOT_REQUESTS + logger.info(f"WandBot + zendesk is running") + sem = asyncio.Semaphore(semLimit) - # Enter a loop where it fetches new tickets every 600 seconds while True: - await asyncio.sleep(600) + await asyncio.sleep(config.INTERVAL_TO_FETCH_TICKETS) # restart the zenpy client because it times out after 3 minutes self.zenpy_client = Zenpy(**self.user_creds) @@ -282,14 +230,22 @@ async def run(self) -> None: # Fetch new tickets new_tickets = list(self.fetch_new_tickets()) logger.info(f"New unanswered Zendesk tickets: {new_tickets}") - # For each new ticket, extract the question, generate a response, format the response, - # and update the ticket with the response - for ticket in new_tickets: - question = extract_question(ticket) - response = await self.generate_response(question) - formatted_response = format_response(response) - self.update_ticket(ticket, formatted_response) + # For every semLimit new tickets, extract the question, generate a response, format the response, + # and update the ticket with the response + for i in range(0, len(new_tickets), semLimit): + batch = new_tickets[i : i + semLimit] + for ticket in batch: + async with sem: + question = extract_question(ticket) + response = await self.generate_response(question) + + formatted_response = format_response(response) + self.update_ticket(ticket, formatted_response) + + # Timeout after a certain amount of tickets, 5 in this case + if i + semLimit < len(new_tickets): + await asyncio.sleep(config.REQUEST_INTERVAL) if len(new_tickets) > 0: logger.info(f"Done processing tickets: {len(new_tickets)}") diff --git a/src/wandbot/apps/zendesk/config.py b/src/wandbot/apps/zendesk/config.py index 0ca60c4..1f4f38f 100644 --- a/src/wandbot/apps/zendesk/config.py +++ b/src/wandbot/apps/zendesk/config.py @@ -5,11 +5,20 @@ class ZendeskAppConfig(BaseSettings): + DISCBOTINTRO: str = ( + "Thank you for reaching out to W&B Technical Support!\n\n" + f"This is an automated reply from our support bot designed to assist you with your WandB-related queries.\n" + f"If you find the solution unsatisfactory or have additional questions, we encourage you to contact our support team at support@wandb.com, or continue replying in this thread\n\n" + ) + ZENDESK_EMAIL: str = (Field(..., env="ZENDESK_EMAIL"),) ZENDESK_PASSWORD: str = (Field(..., env="ZENDESK_PASSWORD"),) ZENDESK_SUBDOMAIN: str = (Field(..., env="ZENDESK_SUBDOMAIN"),) ZDGROUPID: str = ZDGROUPID WANDBOT_API_URL: AnyHttpUrl = Field(..., env="WANDBOT_API_URL") + MAX_WANDBOT_REQUESTS: int = 5 + REQUEST_INTERVAL: int = 60 + INTERVAL_TO_FETCH_TICKETS: int = 600 include_sources: bool = True bot_language: str = "en" diff --git a/src/wandbot/apps/zendesk/extract_by_type.py b/src/wandbot/apps/zendesk/extract_by_type.py index 232ccb9..0979ba5 100644 --- a/src/wandbot/apps/zendesk/extract_by_type.py +++ b/src/wandbot/apps/zendesk/extract_by_type.py @@ -1,13 +1,14 @@ -def discourseExt(description) -> str: - question = description.lower().replace('\n', ' ').replace('\r', '') - question = question.replace('[discourse post]','') - question = question[:4095] +def discourse_ext(description) -> str: + question = description.lower().replace("\n", " ").replace("\r", "") + question = question.replace("[discourse post]", "") return question -def offlineMessageExt(description) -> str: + +def offline_msg_ext(description) -> str: question = description.partition("Offline transcript:")[2] return question -def emailMsgExt(description) -> str: + +def email_msg_ext(description) -> str: # its already clean but returning this for consistency - return description \ No newline at end of file + return description