diff --git a/README.md b/README.md index 5f82b9c..92b05fd 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Before running the Q&A bot, ensure the following environment variables are set: ```bash OPENAI_API_KEY +COHERE_API_KEY SLACK_APP_TOKEN SLACK_BOT_TOKEN SLACK_SIGNING_SECRET diff --git a/poetry.lock b/poetry.lock index 39a435f..0d85ce9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -247,6 +247,18 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "cachetools" +version = "5.3.2" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, +] + [[package]] name = "certifi" version = "2023.7.22" @@ -388,14 +400,14 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "cohere" -version = "4.33" +version = "4.34" description = "" category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "cohere-4.33-py3-none-any.whl", hash = "sha256:2f2b07beae9a9c89aa003340bf5f9fad9394ada51ed206d1856a1241f6a86165"}, - {file = "cohere-4.33.tar.gz", hash = "sha256:43ba4e825462707102e9c65c506cf38affd7b929d367c22e5c6e36b2d3248d46"}, + {file = "cohere-4.34-py3-none-any.whl", hash = "sha256:1003b27f1eefe83be9d9c4b76fbd0949bdb4bd30aaaebb53534d77291da5f02d"}, + {file = "cohere-4.34.tar.gz", hash = "sha256:597bb4ea490a8873ba8166b1bd491380595f4bc22b9e1ff8b3bbe3a4e6fd74bb"}, ] [package.dependencies] @@ -909,18 +921,6 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - [[package]] name = "joblib" version = "1.3.2" @@ -962,14 +962,14 @@ files = [ [[package]] name = "langchain" -version = "0.0.332" +version = "0.0.333" description = "Building applications with LLMs through composability" category = "main" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain-0.0.332-py3-none-any.whl", hash = "sha256:4cbf183b8a385483907192efea2f55d34c0f0c441b0a02f41af1eeec4526677c"}, - {file = "langchain-0.0.332.tar.gz", hash = "sha256:8356b6c0073680d66d5ee2d9e54c23c90198ee74ab2431a0256934a69a511c1f"}, + {file = "langchain-0.0.333-py3-none-any.whl", hash = "sha256:7ee619bbdccfe15bcc4e255a30b5f2e75f9d230cdbaf572f4063dc59d4b03af6"}, + {file = "langchain-0.0.333.tar.gz", hash = "sha256:64e00ecee3dd316a97f825429e286a1b207315a0cc753bcc1cd5cfcb92abbc39"}, ] [package.dependencies] @@ -978,7 +978,7 @@ anyio = "<4.0" async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""} dataclasses-json = ">=0.5.7,<0.7" jsonpatch = ">=1.33,<2.0" -langsmith = ">=0.0.52,<0.1.0" +langsmith = ">=0.0.62,<0.1.0" numpy = ">=1,<2" pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -1018,19 +1018,18 @@ six = "*" [[package]] name = "langsmith" -version = "0.0.62" +version = "0.0.63" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." category = "main" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.62-py3-none-any.whl", hash = "sha256:d50fe00eee06001ac5f705d4ff09c6b4a2f20688d57de5fbd6974a510da672fa"}, - {file = "langsmith-0.0.62.tar.gz", hash = "sha256:196b16bea6856a83c8d95f3f709beebc6c72ea82c113021d3f62ac7cbde51bfe"}, + {file = "langsmith-0.0.63-py3-none-any.whl", hash = "sha256:43a521dd10d8405ac21a0b959e3de33e2270e4abe6c73cc4036232a6990a0793"}, + {file = "langsmith-0.0.63.tar.gz", hash = "sha256:ddb2dfadfad3e05151ed8ba1643d1c516024b80fbd0c6263024400ced06a3768"}, ] [package.dependencies] pydantic = ">=1,<3" -pytest-subtests = ">=0.11.0,<0.12.0" requests = ">=2,<3" [[package]] @@ -1516,22 +1515,6 @@ files = [ {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, ] -[[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - [[package]] name = "protobuf" version = "4.25.0" @@ -1737,45 +1720,6 @@ files = [ pydantic = ">=2.0.1" python-dotenv = ">=0.21.0" -[[package]] -name = "pytest" -version = "7.4.3" -description = "pytest: simple powerful testing with Python" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-subtests" -version = "0.11.0" -description = "unittest subTest() support and subtests fixture" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-subtests-0.11.0.tar.gz", hash = "sha256:51865c88457545f51fb72011942f0a3c6901ee9e24cbfb6d1b9dc1348bafbe37"}, - {file = "pytest_subtests-0.11.0-py3-none-any.whl", hash = "sha256:453389984952eec85ab0ce0c4f026337153df79587048271c7fd0f49119c07e4"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -pytest = ">=7.0" - [[package]] name = "python-dateutil" version = "2.8.2" @@ -2663,18 +2607,6 @@ requests = ">=2.26.0" [package.extras] blobfile = ["blobfile (>=2)"] -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "tqdm" version = "4.66.1" @@ -3199,6 +3131,24 @@ files = [ idna = ">=2.0" multidict = ">=4.0" +[[package]] +name = "zenpy" +version = "2.0.41" +description = "Python wrapper for the Zendesk API" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "zenpy-2.0.41.tar.gz", hash = "sha256:c55c84a85ebdbe949c2cb54ca2436ac6be05e8e2992e1d9a8e6dfb12056edef5"}, +] + +[package.dependencies] +cachetools = ">=3.1.0" +python-dateutil = ">=2.7.5" +pytz = ">=2018.9" +requests = ">=2.14.2" +six = ">=1.14.0" + [[package]] name = "zipp" version = "3.17.0" @@ -3218,4 +3168,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.10.0,<3.11" -content-hash = "a03356cd2f9c38ba657c562593239e8fc8c3119bcc619a2449061e260e585d54" +content-hash = "47fc9d87d06aa9e307128582d9bd91522b345b65e05741f11d9c9031bf0be626" diff --git a/pyproject.toml b/pyproject.toml index cf9c873..3aa1292 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ tree-sitter-languages = "^1.7.1" cohere = "^4.32" markdownify = "^0.11.6" uvicorn = "^0.23.2" +zenpy = { version = "^2.0.41", optional = true } [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/src/wandbot/apps/ZD/ZDWandbot.py b/src/wandbot/apps/ZD/ZDWandbot.py new file mode 100644 index 0000000..88cc695 --- /dev/null +++ b/src/wandbot/apps/ZD/ZDWandbot.py @@ -0,0 +1,115 @@ +import asyncio +from zenpy import Zenpy +from zenpy.lib.api_objects import Comment +from zenpy.lib.api_objects import Ticket +import wandb +from wandbot.chat.config import ChatConfig +from wandbot.chat.schemas import ChatRepsonse, ChatRequest +import os +from datetime import datetime + +from functools import partial +from wandbot.api.client import APIClient +from wandbot.api.schemas import APIQueryResponse +from wandbot.utils import get_logger +from wandbot.apps.ZD.config import ZDAppConfig +import pandas as pd + +logger = get_logger(__name__) +config = ZDAppConfig() + +class ZendeskAIResponseSystem: + + def __init__(self): + userCreds = { + 'email' : config.ZENDESK_EMAIL, + 'password' : config.ZENDESK_PASSWORD, + 'subdomain': config.ZENDESK_SUBDOMAIN + } + self.zenpy_client = Zenpy(**userCreds) + self.api_client = APIClient(url=config.WANDBOT_API_URL) + + def create_new_ticket(self, questionText): + self.zenpy_client.tickets.create(Ticket(subject="WandbotTest4", description=questionText, status = 'new', priority = 'low', tags=["botTest","forum"])) + + def fetch_new_tickets(self): + new_tickets = self.zenpy_client.search(type='ticket', status='new', group_id=config.ZDGROUPID) + # Filtering based on specific requirements + filtered_tickets = [ticket for ticket in new_tickets if 'forum' in ticket.tags] + # filtered_tickets = [ticket for ticket in new_tickets if 'bottest' in ticket.tags] # for testing purposes only + filtered_ticketsNotAnswered = [ticket for ticket in filtered_tickets if 'answered_by_bot' not in ticket.tags] # for testing purposes only + + return filtered_ticketsNotAnswered + + def extract_question(self, ticket): + description = ticket.description + # Preprocessing + question = description.lower().replace('\n', ' ').replace('\r', '') + question = question.replace('[discourse post]','') + question = question[:4095] + + return question + + async def generate_response(self, question, ticket_id): + try: + chat_history = [] + + response = self.api_client.query(question=question, chat_history=[]) + if response == None: + raise Exception("Recieved no response") + + except Exception as e: + logger.error(f"Error: {e}") + response = 'Something went wrong!' + return response + + return response.answer + + #TODO: add the necessary format we want to depending on ticket type + def format_response(self, response): + response = str(response) + max_length = 2000 + if len(response) > max_length: + response = response[:max_length] + '...' + return response+"\n\n-WandBot 🤖" + + def update_ticket(self, ticket, response): + try: + comment = Comment(body=response) + ticket.comment = Comment(body=response, public=False) + + ticket.status="open" + ticket.tags.append('answered_by_bot') + self.zenpy_client.tickets.update(ticket) + except Exception as e: + logger.error(f"Error: {e}") + + #TODO add feedback gathering + def gather_feedback(self, ticket): + try: + ticket.comment = Comment(body="How did we do?", public=False) + self.zenpy_client.tickets.update(ticket) + except Exception as e: + logger.error(f"Error: {e}") + + async def run(self): + # test tickets + # self.create_new_ticket("How Do I start a run?") + self.create_new_ticket("Is there a way to programatically list all projects for a given entity?") + while True: + await asyncio.sleep(120) + + new_tickets = self.fetch_new_tickets() + logger.info(f"New unanswered Tickets: {len(new_tickets)}") + for ticket in new_tickets: + question = self.extract_question(ticket) + response = await self.generate_response(question, ticket) + + formatted_response = self.format_response(response) + self.update_ticket(ticket, formatted_response) + # self.gather_feedback(ticket) + +if __name__ == "__main__": + + zd = ZendeskAIResponseSystem() + asyncio.run(zd.run()) \ No newline at end of file diff --git a/src/wandbot/apps/ZD/config.py b/src/wandbot/apps/ZD/config.py new file mode 100644 index 0000000..a69497b --- /dev/null +++ b/src/wandbot/apps/ZD/config.py @@ -0,0 +1,19 @@ +from pydantic import AnyHttpUrl, Field +from pydantic_settings import BaseSettings + +ZDGROUPID = "360016040851" + +class ZDAppConfig(BaseSettings): + ZENDESK_EMAIL: str = Field(..., env="ZENDESK_EMAIL"), + ZENDESK_PASSWORD: str = Field(..., env="ZENDESK_PASSWORD"), + ZENDESK_SUBDOMAIN: str = Field(..., env="ZENDESK_SUBDOMAIN"), + + WANDB_API_KEY: str = Field(..., env="WANDB_API_KEY") + ZDGROUPID: str = ZDGROUPID + WANDBOT_API_URL: AnyHttpUrl = Field(..., env="WANDBOT_API_URL") + include_sources: bool = True + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + extra = "allow" \ No newline at end of file