From 9e42a7dae4d46c9bbff28c7e783a598883602ea5 Mon Sep 17 00:00:00 2001 From: Jesse Cranney Date: Tue, 6 Feb 2024 19:38:51 +1100 Subject: [PATCH 1/7] first pass --- .gitignore | 3 +++ .justfile | 8 ++++++++ requirements.txt | 34 ++++++++++++++++++++++++++++++++++ virtualpi.py | 25 ++++++++++++++++--------- 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 .justfile create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5665ad0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv/* +pdfs/* +.env \ No newline at end of file diff --git a/.justfile b/.justfile new file mode 100644 index 0000000..be6c6a6 --- /dev/null +++ b/.justfile @@ -0,0 +1,8 @@ +run: + source ./venv/bin/activate + source ./.env && python virtualpi.py pdfs + +reset: + rm ./pdfs/docs.pkl + just run + just run \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f85f461 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,34 @@ +annotated-types==0.6.0 +anyio==4.2.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +distro==1.9.0 +h11==0.14.0 +html2text==2020.1.16 +httpcore==1.0.2 +httpx==0.26.0 +idna==3.6 +jsonpatch==1.33 +jsonpointer==2.4 +langchain-core==0.1.18 +langchain-openai==0.0.5 +langsmith==0.0.86 +numpy==1.26.4 +openai==1.11.1 +packaging==23.2 +paper-qa==4.0.0rc7 +pycryptodome==3.20.0 +pydantic==2.6.1 +pydantic_core==2.16.2 +pypdf==4.0.1 +PyYAML==6.0.1 +regex==2023.12.25 +requests==2.31.0 +slack-bolt==1.18.1 +slack_sdk==3.26.2 +sniffio==1.3.0 +tenacity==8.2.3 +tiktoken==0.5.2 +tqdm==4.66.1 +typing_extensions==4.9.0 +urllib3==2.2.0 diff --git a/virtualpi.py b/virtualpi.py index 59bbe11..c13b956 100644 --- a/virtualpi.py +++ b/virtualpi.py @@ -13,6 +13,8 @@ from paperqa import Docs from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler +from openai import AsyncOpenAI +chat = AsyncOpenAI() #Create handle to Slack app = App(token=os.environ["SLACK_BOT_TOKEN"]) @@ -21,6 +23,7 @@ #This function is called when a Slack user mentions the bot @app.event("app_mention") def event_test(say, body): + print("received question, working on answer.") try: #This gets the question text from the user user_question=body["event"]["blocks"][0]["elements"][0]["elements"][1]["text"] @@ -29,8 +32,8 @@ def event_test(say, body): answer = docs.query(user_question, k=30, max_sources=10) #Print some stuff locally print(answer.formatted_answer) - for p in answer.passages: - print("* %s: %s\n"%(p, answer.passages[p])) + #for context in answer.contexts: + # print("* %s: %s\n"%(context.text.name, context.text.text)) print("\n\n\n") #Send the answer to Slack say(answer.formatted_answer) @@ -52,10 +55,14 @@ def event_test(say, body): try: #Load the pre-pickled document vector if it exists with open("%s/docs.pkl"%PAPERDIR, "rb") as f: - docs = pickle.load(f) + docs = pickle.loads(pickle.load(f)) + docs.set_client(chat) print("Loaded previous state from %s/docs.pkl"%PAPERDIR) print(" - remove this file if you change the set of PDFs\n") -except: +except FileNotFoundError: + docs = None + +if docs is None: #Couldn't load a pre-picked version papers=[] filesfound=glob.glob("%s/*"%PAPERDIR) @@ -70,7 +77,7 @@ def event_test(say, body): print("Found %d PDFs in %s"%(len(papers),PAPERDIR)) #Add each paper in turn to paper-qa/FAISS/OpenAI embedding - docs = Docs(llm='gpt-3.5-turbo', summary_llm="davinci") + docs = Docs(llm="gpt-3.5-turbo",client=chat) for p in papers: try: #Get the base file name to use as the citation @@ -79,23 +86,23 @@ def event_test(say, body): citation=citation[0:citation.rfind(".")] #Embed this doc print("Embedding %s"%citation) - docs.add(p, citation=citation, key=citation) + docs.add(p) except Exception as e: print("Error processing %s: %s"%(p,e)) try: with open("%s/docs.pkl"%PAPERDIR, "wb") as f: #Save this state for next time print("\nSaving state to file %s/docs.pkl - this may take some time."%PAPERDIR) - pickle.dump(docs, f) + pickle.dump(pickle.dumps(docs), f) except Exception as e: print("Couldn't save state into %s - is it writeable?"%PAPERDIR) print("Error was: %s"%e) - sys.exit(1) + sys.exit(2) finally: #This is only necessary as the Slack handle created above seems to break #during the long delay of embedding and pickling. Some kind of bug? print("State saved okay - please restart program.") - sys.exit(1) + sys.exit(0) #Set up the Slack interface to start servicing requests print("Starting Slack handler - bot is ready to answer your questions!") From 0e8153730d927d1447e22b88ddcaa2c651df2cfa Mon Sep 17 00:00:00 2001 From: Jesse Cranney Date: Tue, 6 Feb 2024 19:46:09 +1100 Subject: [PATCH 2/7] update readme --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bca6329..d568d75 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,7 @@ This work was first inspired by a conversation with the authors of [Galactic Chi To run the script, you require: * A directory with the PDFs you wish the expert system to ingest; * A working Python3 environment with the following packages available: - * `pip3 install slack_bolt paper-qa==1.2` - * NB: At the time of writing the default pip version of paper-qa and its langchain dependency are out of sync, hence requesting version 1.2. + * `pip3 install -r requirements.txt` * An OpenAI [API key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key). * You can [Create a new Slack app](https://api.slack.com/tutorials/tracks/responding-to-app-mentions) that is preconfigured with the neccessary permissions by pressing the green 'Create App' button on that link. * You can change the name of your app/bot (you'll use this to interact with it on Slack, by editing the 'manifest' file when the option is presented. @@ -30,7 +29,14 @@ export SLACK_BOT_TOKEN="xoxb-2...C" Then you can start the app as follows. -`python3 virtualpi.py /path/to/your/PDF/directory/` +```bash +python3 virtualpi.py /path/to/your/PDF/directory/ +``` + +or if using [just](https://github.com/casey/just), and with pdfs in `./pdfs/`: +```bash +just run +``` After you run it the first time (when it embeds all of the documents), the script will exit and ask you to restart it (to avoid what appears to be a timeout issue in the Slack libraries). @@ -40,7 +46,13 @@ When the script starts it will check if a pickled version of the dense vector co NB: If you add/remove PDFs you will need to remove the state file! -`rm /path/to/your/PDF/directory/docs.pkl` +```bash +rm /path/to/your/PDF/directory/docs.pkl +``` +or +```bash +just reset +``` ## Add to Slack Workspace From a5bb1d0f2d8d2e16075e3a2429b6139d49fdb3fd Mon Sep 17 00:00:00 2001 From: Jesse Cranney Date: Wed, 7 Feb 2024 12:05:59 +1100 Subject: [PATCH 3/7] added example .env --- .env-example | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .env-example diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..5253d99 --- /dev/null +++ b/.env-example @@ -0,0 +1,5 @@ +export SLACK_APP_TOKEN="xapp-..." +export SLACK_BOT_TOKEN="xoxb-..." +export OPENAI_API_KEY="sk-..." +export SLACK_BOT_USERID = "U01234567ABC" # member ID of bot in slack workspace +export SLACK_CHANNEL_NAME = "bot-channel-name" # name of channel to scan within slack workspace \ No newline at end of file From e055b361b48d4d8c9f1bbdec44f77cb2ffc76ed9 Mon Sep 17 00:00:00 2001 From: Jesse Cranney Date: Wed, 7 Feb 2024 14:13:14 +1100 Subject: [PATCH 4/7] clean up, and added scanning feature --- .env-example | 10 ++++++-- .justfile | 22 +++++++++++----- README.md | 43 ++++++++++++++++++++++++++++---- images/vpiuid.png | Bin 0 -> 43864 bytes requirements.txt | 1 + scan_messages.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++ virtualpi.py | 5 ---- 7 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 images/vpiuid.png create mode 100644 scan_messages.py diff --git a/.env-example b/.env-example index 5253d99..6345fb2 100644 --- a/.env-example +++ b/.env-example @@ -1,5 +1,11 @@ +# slack app token (must be set) export SLACK_APP_TOKEN="xapp-..." +# slack bot token (must be set) export SLACK_BOT_TOKEN="xoxb-..." +# openai api key (set if using openai api) export OPENAI_API_KEY="sk-..." -export SLACK_BOT_USERID = "U01234567ABC" # member ID of bot in slack workspace -export SLACK_CHANNEL_NAME = "bot-channel-name" # name of channel to scan within slack workspace \ No newline at end of file + +# slack bot user id (used when scanning for messages) +export SLACK_BOT_USERID = "U01234567ABC" +# channel name on slack for scanning +export SLACK_CHANNEL_NAME = "bot-channel-name" \ No newline at end of file diff --git a/.justfile b/.justfile index be6c6a6..8889c1d 100644 --- a/.justfile +++ b/.justfile @@ -1,8 +1,18 @@ -run: +run *FLAGS: activate + source ./.env && ipython {{FLAGS}} virtualpi.py pdfs + +scan *FLAGS: activate + source ./.env && ipython {{FLAGS}} scan_messages.py + +activate: source ./venv/bin/activate - source ./.env && python virtualpi.py pdfs -reset: - rm ./pdfs/docs.pkl - just run - just run \ No newline at end of file +clean: + rm -f ./pdfs/docs.pkl + +setup: clean + rm -rf ./venv + python -m venv venv + just activate + pip install -r requirements.txt + mkdir -p pdfs \ No newline at end of file diff --git a/README.md b/README.md index d568d75..e178a73 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,17 @@ Why the name? When your Principal Investigator goes on holidays, you need a *Vir This work was first inspired by a conversation with the authors of [Galactic ChitChat: Using Large Language Models to Converse with Astronomy Literature](https://arxiv.org/abs/2304.05406), who implemented a similar tool, using a similar software stack. Virtual PI was first implemented and used for querying documentation for an astronomical instrument, [MAVIS](https://mavis-ao.org/). ## Configuration +#### API keys +```bash +# create your .env file: +cp .env-example .env +# set your environment variables: +vim .env +``` +#### Launching the bot To run the script, you require: - * A directory with the PDFs you wish the expert system to ingest; + * A directory with the PDFs you wish the expert system to ingest (e.g., `./pdfs/*.pdfs`) * A working Python3 environment with the following packages available: * `pip3 install -r requirements.txt` * An OpenAI [API key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key). @@ -21,11 +29,12 @@ To run the script, you require: The three API tokens you have generated should be exported to your shell environment at runtime: -``` +```bash export OPENAI_API_KEY="sk-M...M" export SLACK_APP_TOKEN="xapp-1...d" export SLACK_BOT_TOKEN="xoxb-2...C" ``` +e.g., by `source`ing the `.env` file after modifying it. Then you can start the app as follows. @@ -33,12 +42,35 @@ Then you can start the app as follows. python3 virtualpi.py /path/to/your/PDF/directory/ ``` -or if using [just](https://github.com/casey/just), and with pdfs in `./pdfs/`: +#### Recording Reactions +In some cases, you may wish to gather the reactions to bot messages (e.g., for further optimisation of the bot) by scanning a channel. +Assuming the `.env` is setup correctly, you can save this data to disk `bot_messages.json` using by running the `scan_messages.py` script: +```bash +python scan_messages.py +``` + +To get the bot's user id (required in `.env`), find the bot's profile on your slack channel, and copy the id shown (starting with `U...`), e.g.: + + + + +### Using [just](https://github.com/casey/just): +`just` allows the abstraction of a few of these setup tasks, see the full set of tasks in the `.justfile`. + +After setting API keys (as above), you can create a virtual environment, install dependencies, and create a `./pdfs/` directory, by running: +```bash +just setup +``` + +Then (after adding your PDFs to `./pdfs/` you can start the slackbot using: ```bash just run ``` -After you run it the first time (when it embeds all of the documents), the script will exit and ask you to restart it (to avoid what appears to be a timeout issue in the Slack libraries). +To record the reactions by scanning a slack channel, set the appropriate `.env` variables and run: +```bash +just scan +``` ## Saving State @@ -51,7 +83,7 @@ rm /path/to/your/PDF/directory/docs.pkl ``` or ```bash -just reset +just clean ``` ## Add to Slack Workspace @@ -67,3 +99,4 @@ By now your app should be happily running. The final step is to actually add it An example interaction is shown below: ![alt text](images/MAVIS-IMBH.png "Example Slack interaction") + diff --git a/images/vpiuid.png b/images/vpiuid.png new file mode 100644 index 0000000000000000000000000000000000000000..cfff331fa6e50b3343a3c5a83b3c18d1a5b08934 GIT binary patch literal 43864 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 CBM+hg literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index f85f461..77e33e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,3 +32,4 @@ tiktoken==0.5.2 tqdm==4.66.1 typing_extensions==4.9.0 urllib3==2.2.0 +ipython \ No newline at end of file diff --git a/scan_messages.py b/scan_messages.py new file mode 100644 index 0000000..3524389 --- /dev/null +++ b/scan_messages.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 + +import os +from slack_bolt import App +import json + +#Create handle to Slack +app = App(token=os.environ["SLACK_BOT_TOKEN"]) +bot_userid = os.environ["SLACK_BOT_USERID"] +channel_name = os.environ["SLACK_CHANNEL_NAME"] + +save_to_file = "./bot_messages.json" + +channel_id = None +# Call the conversations.list method using the WebClient +for result in app.client.conversations_list(): + if channel_id is not None: + break + for channel in result["channels"]: + if channel["name"] == channel_name: + channel_id = channel["id"] + #Print result + print(f"Found conversation ID: {channel_id}") + break + +if channel_id is None: + raise ValueError(f"Unable to find channel named: {channel_name:s}") + +# Store conversation history +conversation_history = [] + +# Call the conversations.history method using the WebClient +# conversations.history returns the first 100 messages by default +# These results are paginated, see: https://api.slack.com/methods/conversations.history$pagination +result = app.client.conversations_history(channel=channel_id) +while True: + conversation_history += result["messages"] + if not result.data["has_more"]: + break + cursor = result.data["response_metadata"]["next_cursor"] + result = app.client.conversations_history(channel=channel_id,cursor=cursor) + +# Print results +print(f"{len(conversation_history):d} messages found in {channel_id:s}") + +bot_messages = [] +for message in conversation_history: + if message["user"]!=bot_userid: + continue + if "subtype" in message and message["subtype"] == "channel_join": + continue + message.pop("blocks") + message.pop("bot_profile") + bot_messages.append(message) + +# Print results +print(f"{len(bot_messages):d} bot messages found in {channel_id:s}") + +with open(save_to_file,"w") as f: + json.dump(bot_messages,f,indent=4) + +print(f"finished successfully, saved to {save_to_file:s}") \ No newline at end of file diff --git a/virtualpi.py b/virtualpi.py index c13b956..c21a7dc 100644 --- a/virtualpi.py +++ b/virtualpi.py @@ -98,11 +98,6 @@ def event_test(say, body): print("Couldn't save state into %s - is it writeable?"%PAPERDIR) print("Error was: %s"%e) sys.exit(2) - finally: - #This is only necessary as the Slack handle created above seems to break - #during the long delay of embedding and pickling. Some kind of bug? - print("State saved okay - please restart program.") - sys.exit(0) #Set up the Slack interface to start servicing requests print("Starting Slack handler - bot is ready to answer your questions!") From ecc4c883d024a0394ac35f2ca68446264ab5a727 Mon Sep 17 00:00:00 2001 From: Jesse Cranney Date: Wed, 7 Feb 2024 17:09:43 +1100 Subject: [PATCH 5/7] cleaned formatting and added progress bar to embedding --- requirements.txt | 3 ++- virtualpi.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 77e33e3..8874df8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,4 +32,5 @@ tiktoken==0.5.2 tqdm==4.66.1 typing_extensions==4.9.0 urllib3==2.2.0 -ipython \ No newline at end of file +ipython +tqdm \ No newline at end of file diff --git a/virtualpi.py b/virtualpi.py index c21a7dc..26b36c0 100644 --- a/virtualpi.py +++ b/virtualpi.py @@ -14,6 +14,7 @@ from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler from openai import AsyncOpenAI +from tqdm import tqdm chat = AsyncOpenAI() #Create handle to Slack @@ -32,11 +33,10 @@ def event_test(say, body): answer = docs.query(user_question, k=30, max_sources=10) #Print some stuff locally print(answer.formatted_answer) - #for context in answer.contexts: - # print("* %s: %s\n"%(context.text.name, context.text.text)) print("\n\n\n") - #Send the answer to Slack - say(answer.formatted_answer) + #Send the (minimal) answer to Slack + formatted_minimal = f"Question: {answer.question}\n\n{answer.answer}" + say(formatted_minimal) except Exception as e: print("Error: %s"%e) @@ -78,14 +78,14 @@ def event_test(say, body): #Add each paper in turn to paper-qa/FAISS/OpenAI embedding docs = Docs(llm="gpt-3.5-turbo",client=chat) - for p in papers: + print("Embedding documents") + pbar = tqdm(papers,leave=True,desc="") + for p in pbar: try: #Get the base file name to use as the citation citation=os.path.split(p)[-1] - #Strip off the ".pdf" or ".PDF" - citation=citation[0:citation.rfind(".")] #Embed this doc - print("Embedding %s"%citation) + pbar.set_description(f"doc={citation:s}") docs.add(p) except Exception as e: print("Error processing %s: %s"%(p,e)) From 7b8efcfb7b5b086d36b07369161f8ed945153c20 Mon Sep 17 00:00:00 2001 From: jcranney Date: Thu, 8 Feb 2024 17:05:24 +1100 Subject: [PATCH 6/7] dockerised --- Dockerfile | 6 ++++++ README.md | 9 +++++++++ virtualpi.py | 20 ++++++++++++++++---- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e7ebae9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY virtualpi.py . +CMD ["ipython","virtualpi.py","./pdfs"] diff --git a/README.md b/README.md index e178a73..ef558fc 100644 --- a/README.md +++ b/README.md @@ -100,3 +100,12 @@ By now your app should be happily running. The final step is to actually add it An example interaction is shown below: ![alt text](images/MAVIS-IMBH.png "Example Slack interaction") +#### Docker +Running with Docker is probably the easiest all round solution, but can make debugging a bit more tedious. To run with docker, use: +```bash +docker build -t virtualpi:latest +docker run --restart=unless-stopped -d -v ./pdfs:/app/pdfs --env-file=./.env virtualpi +``` +This has the benefit of allowing multiple bots running on varied pdf sources. You can build the image once, then spin up a new container (changing the `./pdfs` directory and probably `.env`. + +Note that for now, the `.env` format is not compatible between `just run` and `docker run`. For Docker, remove the `export` and quotation marks from the `.env` file. TODO: fix this. diff --git a/virtualpi.py b/virtualpi.py index 26b36c0..38422c6 100644 --- a/virtualpi.py +++ b/virtualpi.py @@ -35,8 +35,7 @@ def event_test(say, body): print(answer.formatted_answer) print("\n\n\n") #Send the (minimal) answer to Slack - formatted_minimal = f"Question: {answer.question}\n\n{answer.answer}" - say(formatted_minimal) + say(answer.answer) except Exception as e: print("Error: %s"%e) @@ -84,9 +83,10 @@ def event_test(say, body): try: #Get the base file name to use as the citation citation=os.path.split(p)[-1] + citation=citation[0:citation.rfind(".")] #Embed this doc pbar.set_description(f"doc={citation:s}") - docs.add(p) + docs.add(p,docname=citation,citation=citation) except Exception as e: print("Error processing %s: %s"%(p,e)) try: @@ -99,7 +99,19 @@ def event_test(say, body): print("Error was: %s"%e) sys.exit(2) +docs.prompts.qa = ("Write an answer ({answer_length}) " + "for the question below based on the provided context. " + "If the context provides insufficient information, " + 'reply "I cannot answer". ' + "For each part of your answer, indicate which sources most support it " + "via valid citation markers at the end of sentences, like (Example2012). " + "Answer in an unbiased, comprehensive, and scholarly tone. " + "If the question is subjective, provide an opinionated answer in the concluding 1-2 sentences. " + "Use Markdown for formatting code or text, and try to use direct quotes to support arguments.\n\n" + "{context}\n" + "Question: {question}\n" + "Answer: ") + #Set up the Slack interface to start servicing requests print("Starting Slack handler - bot is ready to answer your questions!") SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() - From 53bb4d18dd61c7a910a4212b2d662d65063b2208 Mon Sep 17 00:00:00 2001 From: Jesse Cranney Date: Thu, 8 Feb 2024 16:57:37 +1100 Subject: [PATCH 7/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef558fc..7651ff1 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ By now your app should be happily running. The final step is to actually add it An example interaction is shown below: ![alt text](images/MAVIS-IMBH.png "Example Slack interaction") -#### Docker +## Docker Running with Docker is probably the easiest all round solution, but can make debugging a bit more tedious. To run with docker, use: ```bash docker build -t virtualpi:latest