diff --git a/extensions/slack-bot/.gitignore b/extensions/slack-bot/.gitignore new file mode 100644 index 000000000..1d8e58b2a --- /dev/null +++ b/extensions/slack-bot/.gitignore @@ -0,0 +1,3 @@ +.env +.venv/ +get-pip.py \ No newline at end of file diff --git a/extensions/slack-bot/Readme.md b/extensions/slack-bot/Readme.md new file mode 100644 index 000000000..704184a25 --- /dev/null +++ b/extensions/slack-bot/Readme.md @@ -0,0 +1,84 @@ + +# Slack Bot Configuration Guide + +> **Note:** The following guidelines must be followed on the [Slack API website](https://api.slack.com/) for setting up your Slack app and generating the necessary tokens. + +## Step-by-Step Instructions + +### 1. Navigate to Your Apps +- Go to the Slack API page for apps and select **Create an App** from the “From Scratch” option. + +### 2. App Creation +- Name your app and choose the workspace where you wish to add the assistant. + +### 3. Enabling Socket Mode +- Navigate to **Settings > Socket Mode** and enable **Socket Mode**. +- This action will generate an App-level token. Select the `connections:write` scope and copy the App-level token for future use. + +### 4. Socket Naming +- Assign a name to your socket as per your preference. + +### 5. Basic Information Setup +- Go to **Basic Information** (under **Settings**) and configure the following: + - Assistant name + - App icon + - Background color + +### 6. Bot Token and Permissions +- In the **OAuth & Permissions** option found under the **Features** section, retrieve the Bot Token. Save it for future usage. +- You will also need to add specific bot token scopes: + - `app_mentions:read` + - `assistant:write` + - `chat:write` + - `chat:write.public` + - `im:history` + +### 7. Enable Events +- From **Event Subscriptions**, enable events and add the following Bot User events: + - `app_mention` + - `assistant_thread_context_changed` + - `assistant_thread_started` + - `message.im` + +### 8. Agent/Assistant Toggle +- In the **Features > Agent & Assistants** section, toggle on the Agent or Assistant option. +- In the **Suggested Prompts** setting, leave it as `dynamic` (this is the default setting). + +--- + +## Code-Side Configuration Guide + +This section focuses on generating and setting up the necessary tokens in the `.env` file, using the `.env-example` as a template. + +### Step 1: Generating Required Keys + +1. **SLACK_APP_TOKEN** + - Navigate to **Settings > Socket Mode** in the Slack API and enable **Socket Mode**. + - Copy the App-level token generated (usually starts with `xapp-`). + +2. **SLACK_BOT_TOKEN** + - Go to **OAuth & Permissions** (under the **Features** section in Slack API). + - Retrieve the **Bot Token** (starts with `xoxb-`). + +3. **DOCSGPT_API_KEY** + - Go to the **DocsGPT website**. + - Navigate to **Settings > Chatbots > Create New** to generate a DocsGPT API Key. + - Copy the generated key for use. + +### Step 2: Creating and Updating the `.env` File + +1. Create a new `.env` file in the root of your project (if it doesn’t already exist). +2. Use the `.env-example` as a reference and update the file with the following keys and values: + +```bash +# .env file +SLACK_APP_TOKEN=xapp-your-generated-app-token +SLACK_BOT_TOKEN=xoxb-your-generated-bot-token +DOCSGPT_API_KEY=your-docsgpt-generated-api-key +``` + +Replace the placeholder values with the actual tokens generated from the Slack API and DocsGPT as per the steps outlined above. + +--- + +This concludes the guide for both setting up the Slack API and configuring the `.env` file on the code side. diff --git a/extensions/slack-bot/app.py b/extensions/slack-bot/app.py new file mode 100644 index 000000000..d4f522fdb --- /dev/null +++ b/extensions/slack-bot/app.py @@ -0,0 +1,112 @@ +import os +import hashlib +import httpx +import re +from slack_bolt.async_app import AsyncApp +from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler +from dotenv import load_dotenv + +load_dotenv() +API_BASE = os.getenv("API_BASE", "https://gptcloud.arc53.com") +API_URL = API_BASE + "/api/answer" + +# Slack bot token and signing secret +SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN") +SLACK_APP_TOKEN = os.getenv("SLACK_APP_TOKEN") + +# OpenAI API key for DocsGPT (replace this with your actual API key) +DOCSGPT_API_KEY = os.getenv("DOCSGPT_API_KEY") + +# Initialize Slack app +app = AsyncApp(token=SLACK_BOT_TOKEN) + +def encode_conversation_id(conversation_id: str) -> str: + """ + Encodes 11 length Slack conversation_id to 12 length string + Args: + conversation_id (str): The 11 digit slack conversation_id. + Returns: + str: Hashed id. + """ + # Create a SHA-256 hash of the string + hashed_id = hashlib.sha256(conversation_id.encode()).hexdigest() + + # Take the first 24 characters of the hash + hashed_24_char_id = hashed_id[:24] + return hashed_24_char_id + +async def generate_answer(question: str, messages: list, conversation_id: str | None) -> dict: + """Generates an answer using the external API.""" + payload = { + "question": question, + "api_key": DOCSGPT_API_KEY, + "history": messages, + "conversation_id": conversation_id, + } + headers = { + "Content-Type": "application/json; charset=utf-8" + } + timeout = 60.0 + async with httpx.AsyncClient() as client: + response = await client.post(API_URL, json=payload, headers=headers, timeout=timeout) + + if response.status_code == 200: + data = response.json() + conversation_id = data.get("conversation_id") + answer = data.get("answer", "Sorry, I couldn't find an answer.") + return {"answer": answer, "conversation_id": conversation_id} + else: + print(response.json()) + return {"answer": "Sorry, I couldn't find an answer.", "conversation_id": None} + +@app.message(".*") +async def message_docs(message, say): + client = app.client + channel = message['channel'] + thread_ts = message['thread_ts'] + user_query = message['text'] + await client.assistant_threads_setStatus( + channel_id = channel, + thread_ts = thread_ts, + status = "is generating your answer...", + ) + + docs_gpt_channel_id = encode_conversation_id(thread_ts) + + # Get response from DocsGPT + response = await generate_answer(user_query,[], docs_gpt_channel_id) + answer = convert_to_slack_markdown(response['answer']) + + # Respond in Slack + await client.chat_postMessage(text = answer, mrkdwn= True, channel= message['channel'], + thread_ts = message['thread_ts'],) + +def convert_to_slack_markdown(markdown_text: str): + # Convert bold **text** to *text* for Slack + slack_text = re.sub(r'\*\*(.*?)\*\*', r'*\1*', markdown_text) # **text** to *text* + + # Convert italics _text_ to _text_ for Slack + slack_text = re.sub(r'_(.*?)_', r'_\1_', slack_text) # _text_ to _text_ + + # Convert inline code `code` to `code` (Slack supports backticks for inline code) + slack_text = re.sub(r'`(.*?)`', r'`\1`', slack_text) + + # Convert bullet points with single or no spaces to filled bullets (•) + slack_text = re.sub(r'^\s{0,1}[-*]\s+', ' • ', slack_text, flags=re.MULTILINE) + + # Convert bullet points with multiple spaces to hollow bullets (◦) + slack_text = re.sub(r'^\s{2,}[-*]\s+', '\t◦ ', slack_text, flags=re.MULTILINE) + + # Convert headers (##) to bold in Slack + slack_text = re.sub(r'^\s*#{1,6}\s*(.*?)$', r'*\1*', slack_text, flags=re.MULTILINE) + + return slack_text + +async def main(): + handler = AsyncSocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]) + await handler.start_async() + +# Start the app +if __name__ == "__main__": + import asyncio + asyncio.run(main()) \ No newline at end of file diff --git a/extensions/slack-bot/requirements.txt b/extensions/slack-bot/requirements.txt new file mode 100644 index 000000000..0c588b43c --- /dev/null +++ b/extensions/slack-bot/requirements.txt @@ -0,0 +1,10 @@ +aiohttp>=3,<4 +certifi==2024.7.4 +h11==0.14.0 +httpcore==1.0.5 +httpx==0.27.0 +idna==3.7 +python-dotenv==1.0.1 +sniffio==1.3.1 +slack-bolt==1.21.0 +bson==0.5.10