Add slack example
This commit is contained in:
21
examples/slack/.dockerenv.example
Normal file
21
examples/slack/.dockerenv.example
Normal file
@@ -0,0 +1,21 @@
|
||||
SLACK_APP_TOKEN=xapp-
|
||||
SLACK_BOT_TOKEN=xoxb-
|
||||
OPENAI_API_KEY=fake
|
||||
|
||||
OPENAI_SYSTEM_TEXT=Default System Text
|
||||
OPENAI_TIMEOUT_SECONDS=30
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
USE_SLACK_LANGUAGE=true
|
||||
SLACK_APP_LOG_LEVEL=DEBUG
|
||||
TRANSLATE_MARKDOWN=false
|
||||
OPENAI_API_BASE=http://localhost:8080/v1
|
||||
EMBEDDINGS_MODEL=all-MiniLM-L6-v2
|
||||
EMBEDDINGS_API_BASE=http://localhost:8080/v1
|
||||
LOCALAI_API_BASE=http://localhost:8080/v1
|
||||
TTS_API_BASE=http://localhost:8080/v1
|
||||
IMAGES_API_BASE=http://localhost:8080/v1
|
||||
STABLEDIFFUSION_MODEL=dreamshaper
|
||||
FUNCTIONS_MODEL=gpt-3.5-turbo
|
||||
LLM_MODEL=gpt-3.5-turbo
|
||||
TTS_MODEL=en-us-kathleen-low.onnx
|
||||
PERSISTENT_DIR=/data
|
||||
17
examples/slack/Dockerfile
Normal file
17
examples/slack/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM python:3.11.3-slim-buster
|
||||
WORKDIR /app/
|
||||
COPY requirements.txt /app/
|
||||
|
||||
RUN apt-get update && apt-get install build-essential git -y
|
||||
RUN pip install -U pip && pip install -r requirements.txt
|
||||
COPY *.py /app/
|
||||
COPY *.sh /app/
|
||||
RUN mkdir /app/app/
|
||||
COPY app/*.py /app/app/
|
||||
ENTRYPOINT /app/entrypoint.sh
|
||||
|
||||
# docker build . -t your-repo/chat-gpt-in-slack
|
||||
# export SLACK_APP_TOKEN=xapp-...
|
||||
# export SLACK_BOT_TOKEN=xoxb-...
|
||||
# export OPENAI_API_KEY=sk-...
|
||||
# docker run -e SLACK_APP_TOKEN=$SLACK_APP_TOKEN -e SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN -e OPENAI_API_KEY=$OPENAI_API_KEY -it your-repo/chat-gpt-in-slack
|
||||
21
examples/slack/LICENSE
Normal file
21
examples/slack/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Slack Technologies, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
0
examples/slack/app/__init__.py
Normal file
0
examples/slack/app/__init__.py
Normal file
396
examples/slack/app/agent.py
Normal file
396
examples/slack/app/agent.py
Normal file
@@ -0,0 +1,396 @@
|
||||
import openai
|
||||
#from langchain.embeddings import HuggingFaceEmbeddings
|
||||
from langchain.embeddings import LocalAIEmbeddings
|
||||
|
||||
from langchain.document_loaders import (
|
||||
SitemapLoader,
|
||||
# GitHubIssuesLoader,
|
||||
# GitLoader,
|
||||
)
|
||||
|
||||
import uuid
|
||||
import sys
|
||||
|
||||
from app.env import *
|
||||
from queue import Queue
|
||||
import asyncio
|
||||
import threading
|
||||
from localagi import LocalAGI
|
||||
|
||||
from ascii_magic import AsciiArt
|
||||
from duckduckgo_search import DDGS
|
||||
from typing import Dict, List
|
||||
import os
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
import openai
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from io import StringIO
|
||||
FILE_NAME_FORMAT = '%Y_%m_%d_%H_%M_%S'
|
||||
|
||||
|
||||
|
||||
if not os.environ.get("PYSQL_HACK", "false") == "false":
|
||||
# these three lines swap the stdlib sqlite3 lib with the pysqlite3 package for chroma
|
||||
__import__('pysqlite3')
|
||||
import sys
|
||||
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
|
||||
if MILVUS_HOST == "":
|
||||
from langchain.vectorstores import Chroma
|
||||
else:
|
||||
from langchain.vectorstores import Milvus
|
||||
|
||||
embeddings = LocalAIEmbeddings(model=EMBEDDINGS_MODEL,openai_api_base=EMBEDDINGS_API_BASE)
|
||||
|
||||
loop = None
|
||||
channel = None
|
||||
def call(thing):
|
||||
return asyncio.run_coroutine_threadsafe(thing,loop).result()
|
||||
|
||||
def ingest(a, agent_actions={}, localagi=None):
|
||||
q = json.loads(a)
|
||||
chunk_size = MEMORY_CHUNK_SIZE
|
||||
chunk_overlap = MEMORY_CHUNK_OVERLAP
|
||||
print(">>> ingesting: ")
|
||||
print(q)
|
||||
documents = []
|
||||
sitemap_loader = SitemapLoader(web_path=q["url"])
|
||||
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
|
||||
documents.extend(sitemap_loader.load())
|
||||
texts = text_splitter.split_documents(documents)
|
||||
if MILVUS_HOST == "":
|
||||
db = Chroma.from_documents(texts,embeddings,collection_name=MEMORY_COLLECTION, persist_directory=PERSISTENT_DIR)
|
||||
db.persist()
|
||||
db = None
|
||||
else:
|
||||
Milvus.from_documents(texts,embeddings,collection_name=MEMORY_COLLECTION, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
|
||||
return f"Documents ingested"
|
||||
# def create_image(a, agent_actions={}, localagi=None):
|
||||
# """
|
||||
# Create an image based on a description using OpenAI's API.
|
||||
|
||||
# Args:
|
||||
# a (str): A JSON string containing the description, width, and height for the image to be created.
|
||||
# agent_actions (dict, optional): A dictionary of agent actions. Defaults to {}.
|
||||
# localagi (LocalAGI, optional): An instance of the LocalAGI class. Defaults to None.
|
||||
|
||||
# Returns:
|
||||
# str: A string containing the URL of the created image.
|
||||
# """
|
||||
# q = json.loads(a)
|
||||
# print(">>> creating image: ")
|
||||
# print(q["description"])
|
||||
# size=f"{q['width']}x{q['height']}"
|
||||
# response = openai.Image.create(prompt=q["description"], n=1, size=size)
|
||||
# image_url = response["data"][0]["url"]
|
||||
# image_name = download_image(image_url)
|
||||
# image_path = f"{PERSISTENT_DIR}{image_name}"
|
||||
|
||||
# file = discord.File(image_path, filename=image_name)
|
||||
# embed = discord.Embed(title="Generated image")
|
||||
# embed.set_image(url=f"attachment://{image_name}")
|
||||
|
||||
# call(channel.send(file=file, content=f"Here is what I have generated", embed=embed))
|
||||
|
||||
# return f"Image created: {response['data'][0]['url']}"
|
||||
def download_image(url: str):
|
||||
file_name = f"{datetime.now().strftime(FILE_NAME_FORMAT)}.jpg"
|
||||
full_path = f"{PERSISTENT_DIR}{file_name}"
|
||||
urllib.request.urlretrieve(url, full_path)
|
||||
return file_name
|
||||
|
||||
|
||||
### Agent capabilities
|
||||
### These functions are called by the agent to perform actions
|
||||
###
|
||||
def save(memory, agent_actions={}, localagi=None):
|
||||
q = json.loads(memory)
|
||||
print(">>> saving to memories: ")
|
||||
print(q["content"])
|
||||
if MILVUS_HOST == "":
|
||||
chroma_client = Chroma(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, persist_directory=PERSISTENT_DIR)
|
||||
else:
|
||||
chroma_client = Milvus(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
|
||||
chroma_client.add_texts([q["content"]],[{"id": str(uuid.uuid4())}])
|
||||
if MILVUS_HOST == "":
|
||||
chroma_client.persist()
|
||||
chroma_client = None
|
||||
return f"The object was saved permanently to memory."
|
||||
|
||||
def search_memory(query, agent_actions={}, localagi=None):
|
||||
q = json.loads(query)
|
||||
if MILVUS_HOST == "":
|
||||
chroma_client = Chroma(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, persist_directory=PERSISTENT_DIR)
|
||||
else:
|
||||
chroma_client = Milvus(collection_name=MEMORY_COLLECTION,embedding_function=embeddings, connection_args={"host": MILVUS_HOST, "port": MILVUS_PORT})
|
||||
#docs = chroma_client.search(q["keywords"], "mmr")
|
||||
retriever = chroma_client.as_retriever(search_type=MEMORY_SEARCH_TYPE, search_kwargs={"k": MEMORY_RESULTS})
|
||||
|
||||
docs = retriever.get_relevant_documents(q["keywords"])
|
||||
text_res="Memories found in the database:\n"
|
||||
|
||||
sources = set() # To store unique sources
|
||||
|
||||
# Collect unique sources
|
||||
for document in docs:
|
||||
if "source" in document.metadata:
|
||||
sources.add(document.metadata["source"])
|
||||
|
||||
for doc in docs:
|
||||
# drop newlines from page_content
|
||||
content = doc.page_content.replace("\n", " ")
|
||||
content = " ".join(content.split())
|
||||
text_res+="- "+content+"\n"
|
||||
|
||||
# Print the relevant sources used for the answer
|
||||
for source in sources:
|
||||
if source.startswith("http"):
|
||||
text_res += "" + source + "\n"
|
||||
|
||||
chroma_client = None
|
||||
#if args.postprocess:
|
||||
# return post_process(text_res)
|
||||
return text_res
|
||||
#return localagi.post_process(text_res)
|
||||
|
||||
# write file to disk with content
|
||||
def save_file(arg, agent_actions={}, localagi=None):
|
||||
arg = json.loads(arg)
|
||||
file = filename = arg["filename"]
|
||||
content = arg["content"]
|
||||
# create persistent dir if does not exist
|
||||
if not os.path.exists(PERSISTENT_DIR):
|
||||
os.makedirs(PERSISTENT_DIR)
|
||||
# write the file in the directory specified
|
||||
file = os.path.join(PERSISTENT_DIR, filename)
|
||||
|
||||
# Check if the file already exists
|
||||
if os.path.exists(file):
|
||||
mode = 'a' # Append mode
|
||||
else:
|
||||
mode = 'w' # Write mode
|
||||
|
||||
with open(file, mode) as f:
|
||||
f.write(content)
|
||||
|
||||
file = discord.File(file, filename=filename)
|
||||
call(channel.send(file=file, content=f"Here is what I have generated"))
|
||||
return f"File {file} saved successfully."
|
||||
|
||||
def ddg(query: str, num_results: int, backend: str = "api") -> List[Dict[str, str]]:
|
||||
"""Run query through DuckDuckGo and return metadata.
|
||||
|
||||
Args:
|
||||
query: The query to search for.
|
||||
num_results: The number of results to return.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries with the following keys:
|
||||
snippet - The description of the result.
|
||||
title - The title of the result.
|
||||
link - The link to the result.
|
||||
"""
|
||||
ddgs = DDGS()
|
||||
try:
|
||||
results = ddgs.text(
|
||||
query,
|
||||
backend=backend,
|
||||
)
|
||||
if results is None:
|
||||
return [{"Result": "No good DuckDuckGo Search Result was found"}]
|
||||
|
||||
def to_metadata(result: Dict) -> Dict[str, str]:
|
||||
if backend == "news":
|
||||
return {
|
||||
"date": result["date"],
|
||||
"title": result["title"],
|
||||
"snippet": result["body"],
|
||||
"source": result["source"],
|
||||
"link": result["url"],
|
||||
}
|
||||
return {
|
||||
"snippet": result["body"],
|
||||
"title": result["title"],
|
||||
"link": result["href"],
|
||||
}
|
||||
|
||||
formatted_results = []
|
||||
for i, res in enumerate(results, 1):
|
||||
if res is not None:
|
||||
formatted_results.append(to_metadata(res))
|
||||
if len(formatted_results) == num_results:
|
||||
break
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return []
|
||||
return formatted_results
|
||||
|
||||
## Search on duckduckgo
|
||||
def search_duckduckgo(a, agent_actions={}, localagi=None):
|
||||
a = json.loads(a)
|
||||
list=ddg(a["query"], 2)
|
||||
|
||||
text_res=""
|
||||
for doc in list:
|
||||
text_res+=f"""{doc["link"]}: {doc["title"]} {doc["snippet"]}\n"""
|
||||
print("Found")
|
||||
print(text_res)
|
||||
#if args.postprocess:
|
||||
# return post_process(text_res)
|
||||
return text_res
|
||||
#l = json.dumps(list)
|
||||
#return l
|
||||
|
||||
### End Agent capabilities
|
||||
###
|
||||
|
||||
### Agent action definitions
|
||||
agent_actions = {
|
||||
# "generate_picture": {
|
||||
# "function": create_image,
|
||||
# "plannable": True,
|
||||
# "description": 'For creating a picture, the assistant replies with "generate_picture" and a detailed description, enhancing it with as much detail as possible.',
|
||||
# "signature": {
|
||||
# "name": "generate_picture",
|
||||
# "parameters": {
|
||||
# "type": "object",
|
||||
# "properties": {
|
||||
# "description": {
|
||||
# "type": "string",
|
||||
# },
|
||||
# "width": {
|
||||
# "type": "number",
|
||||
# },
|
||||
# "height": {
|
||||
# "type": "number",
|
||||
# },
|
||||
# },
|
||||
# }
|
||||
# },
|
||||
# },
|
||||
"search_internet": {
|
||||
"function": search_duckduckgo,
|
||||
"plannable": True,
|
||||
"description": 'For searching the internet with a query, the assistant replies with the action "search_internet" and the query to search.',
|
||||
"signature": {
|
||||
"name": "search_internet",
|
||||
"description": """For searching internet.""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"save_file": {
|
||||
"function": save_file,
|
||||
"plannable": True,
|
||||
"description": 'The assistant replies with the action "save_file", the filename and content to save for writing a file to disk permanently. This can be used to store the result of complex actions locally.',
|
||||
"signature": {
|
||||
"name": "save_file",
|
||||
"description": """For saving a file to disk with content.""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"ingest": {
|
||||
"function": ingest,
|
||||
"plannable": True,
|
||||
"description": 'The assistant replies with the action "ingest" when there is an url to a sitemap to ingest memories from.',
|
||||
"signature": {
|
||||
"name": "ingest",
|
||||
"description": """Save or store informations into memory.""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
},
|
||||
"save_memory": {
|
||||
"function": save,
|
||||
"plannable": True,
|
||||
"description": 'The assistant replies with the action "save_memory" and the string to remember or store an information that thinks it is relevant permanently.',
|
||||
"signature": {
|
||||
"name": "save_memory",
|
||||
"description": """Save or store informations into memory.""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "information to save"
|
||||
},
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
},
|
||||
},
|
||||
"search_memory": {
|
||||
"function": search_memory,
|
||||
"plannable": True,
|
||||
"description": 'The assistant replies with the action "search_memory" for searching between its memories with a query term.',
|
||||
"signature": {
|
||||
"name": "search_memory",
|
||||
"description": """Search in memory""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keywords": {
|
||||
"type": "string",
|
||||
"description": "reasoning behind the intent"
|
||||
},
|
||||
},
|
||||
"required": ["keywords"]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
def localagi(q):
|
||||
localagi = LocalAGI(
|
||||
agent_actions=agent_actions,
|
||||
llm_model=LLM_MODEL,
|
||||
tts_model=VOICE_MODEL,
|
||||
tts_api_base=TTS_API_BASE,
|
||||
functions_model=FUNCTIONS_MODEL,
|
||||
api_base=LOCALAI_API_BASE,
|
||||
stablediffusion_api_base=IMAGE_API_BASE,
|
||||
stablediffusion_model=STABLEDIFFUSION_MODEL,
|
||||
)
|
||||
conversation_history = []
|
||||
|
||||
conversation_history=localagi.evaluate(
|
||||
q,
|
||||
conversation_history,
|
||||
critic=False,
|
||||
re_evaluate=False,
|
||||
# Enable to lower context usage but increases LLM calls
|
||||
postprocess=False,
|
||||
subtaskContext=True,
|
||||
)
|
||||
return conversation_history[-1]["content"]
|
||||
403
examples/slack/app/bolt_listeners.py
Normal file
403
examples/slack/app/bolt_listeners.py
Normal file
@@ -0,0 +1,403 @@
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from openai.error import Timeout
|
||||
from slack_bolt import App, Ack, BoltContext, BoltResponse
|
||||
from slack_bolt.request.payload_utils import is_event
|
||||
from slack_sdk.web import WebClient
|
||||
|
||||
from app.env import (
|
||||
OPENAI_TIMEOUT_SECONDS,
|
||||
SYSTEM_TEXT,
|
||||
TRANSLATE_MARKDOWN,
|
||||
)
|
||||
|
||||
|
||||
from app.i18n import translate
|
||||
from app.openai_ops import (
|
||||
ask_llm,
|
||||
format_openai_message_content,
|
||||
build_system_text,
|
||||
)
|
||||
from app.slack_ops import find_parent_message, is_no_mention_thread, post_wip_message, update_wip_message
|
||||
|
||||
|
||||
#
|
||||
# Listener functions
|
||||
#
|
||||
|
||||
|
||||
def just_ack(ack: Ack):
|
||||
ack()
|
||||
|
||||
|
||||
TIMEOUT_ERROR_MESSAGE = (
|
||||
f":warning: Sorry! It looks like OpenAI didn't respond within {OPENAI_TIMEOUT_SECONDS} seconds. "
|
||||
"Please try again later. :bow:"
|
||||
)
|
||||
DEFAULT_LOADING_TEXT = ":hourglass_flowing_sand: Wait a second, please ..."
|
||||
|
||||
|
||||
def respond_to_app_mention(
|
||||
context: BoltContext,
|
||||
payload: dict,
|
||||
client: WebClient,
|
||||
logger: logging.Logger,
|
||||
):
|
||||
if payload.get("thread_ts") is not None:
|
||||
parent_message = find_parent_message(
|
||||
client, context.channel_id, payload.get("thread_ts")
|
||||
)
|
||||
if parent_message is not None:
|
||||
if is_no_mention_thread(context, parent_message):
|
||||
# The message event handler will reply to this
|
||||
return
|
||||
|
||||
wip_reply = None
|
||||
# Replace placeholder for Slack user ID in the system prompt
|
||||
system_text = build_system_text(SYSTEM_TEXT, TRANSLATE_MARKDOWN, context)
|
||||
messages = [{"role": "system", "content": system_text}]
|
||||
|
||||
print("system text:"+system_text, flush=True)
|
||||
|
||||
openai_api_key = context.get("OPENAI_API_KEY")
|
||||
try:
|
||||
if openai_api_key is None:
|
||||
client.chat_postMessage(
|
||||
channel=context.channel_id,
|
||||
text="To use this app, please configure your OpenAI API key first",
|
||||
)
|
||||
return
|
||||
|
||||
user_id = context.actor_user_id or context.user_id
|
||||
content = ""
|
||||
if payload.get("thread_ts") is not None:
|
||||
# Mentioning the bot user in a thread
|
||||
replies_in_thread = client.conversations_replies(
|
||||
channel=context.channel_id,
|
||||
ts=payload.get("thread_ts"),
|
||||
include_all_metadata=True,
|
||||
limit=1000,
|
||||
).get("messages", [])
|
||||
reply = replies_in_thread[-1]
|
||||
#for reply in replies_in_thread:
|
||||
c = reply["text"]+"\n\n"
|
||||
content += c
|
||||
role = "assistant" if reply["user"] == context.bot_user_id else "user"
|
||||
messages.append(
|
||||
{
|
||||
"role": role,
|
||||
"content": (
|
||||
format_openai_message_content(
|
||||
reply["text"], TRANSLATE_MARKDOWN
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Strip bot Slack user ID from initial message
|
||||
msg_text = re.sub(f"<@{context.bot_user_id}>\\s*", "", payload["text"])
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": format_openai_message_content(msg_text, TRANSLATE_MARKDOWN),
|
||||
}
|
||||
)
|
||||
|
||||
loading_text = translate(
|
||||
openai_api_key=openai_api_key, context=context, text=DEFAULT_LOADING_TEXT
|
||||
)
|
||||
wip_reply = post_wip_message(
|
||||
client=client,
|
||||
channel=context.channel_id,
|
||||
thread_ts=payload["ts"],
|
||||
loading_text=loading_text,
|
||||
messages=messages,
|
||||
user=context.user_id,
|
||||
)
|
||||
|
||||
resp = ask_llm(messages=messages)
|
||||
print("Reply "+resp)
|
||||
|
||||
update_wip_message(
|
||||
client=client,
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=resp,
|
||||
messages=messages,
|
||||
user=user_id,
|
||||
)
|
||||
|
||||
except Timeout:
|
||||
if wip_reply is not None:
|
||||
text = (
|
||||
(
|
||||
wip_reply.get("message", {}).get("text", "")
|
||||
if wip_reply is not None
|
||||
else ""
|
||||
)
|
||||
+ "\n\n"
|
||||
+ translate(
|
||||
openai_api_key=openai_api_key,
|
||||
context=context,
|
||||
text=TIMEOUT_ERROR_MESSAGE,
|
||||
)
|
||||
)
|
||||
client.chat_update(
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=text,
|
||||
)
|
||||
except Exception as e:
|
||||
text = (
|
||||
(
|
||||
wip_reply.get("message", {}).get("text", "")
|
||||
if wip_reply is not None
|
||||
else ""
|
||||
)
|
||||
+ "\n\n"
|
||||
+ translate(
|
||||
openai_api_key=openai_api_key,
|
||||
context=context,
|
||||
text=f":warning: Failed to start a conversation with ChatGPT: {e}",
|
||||
)
|
||||
)
|
||||
logger.exception(text, e)
|
||||
if wip_reply is not None:
|
||||
client.chat_update(
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=text,
|
||||
)
|
||||
|
||||
|
||||
def respond_to_new_message(
|
||||
context: BoltContext,
|
||||
payload: dict,
|
||||
client: WebClient,
|
||||
logger: logging.Logger,
|
||||
):
|
||||
if payload.get("bot_id") is not None and payload.get("bot_id") != context.bot_id:
|
||||
# Skip a new message by a different app
|
||||
return
|
||||
|
||||
wip_reply = None
|
||||
try:
|
||||
is_in_dm_with_bot = payload.get("channel_type") == "im"
|
||||
is_no_mention_required = False
|
||||
thread_ts = payload.get("thread_ts")
|
||||
if is_in_dm_with_bot is False and thread_ts is None:
|
||||
return
|
||||
|
||||
openai_api_key = context.get("OPENAI_API_KEY")
|
||||
if openai_api_key is None:
|
||||
return
|
||||
|
||||
messages_in_context = []
|
||||
if is_in_dm_with_bot is True and thread_ts is None:
|
||||
# In the DM with the bot
|
||||
past_messages = client.conversations_history(
|
||||
channel=context.channel_id,
|
||||
include_all_metadata=True,
|
||||
limit=100,
|
||||
).get("messages", [])
|
||||
past_messages.reverse()
|
||||
# Remove old messages
|
||||
for message in past_messages:
|
||||
seconds = time.time() - float(message.get("ts"))
|
||||
if seconds < 86400: # less than 1 day
|
||||
messages_in_context.append(message)
|
||||
is_no_mention_required = True
|
||||
else:
|
||||
# In a thread with the bot in a channel
|
||||
messages_in_context = client.conversations_replies(
|
||||
channel=context.channel_id,
|
||||
ts=thread_ts,
|
||||
include_all_metadata=True,
|
||||
limit=1000,
|
||||
).get("messages", [])
|
||||
if is_in_dm_with_bot is True:
|
||||
is_no_mention_required = True
|
||||
else:
|
||||
the_parent_message_found = False
|
||||
for message in messages_in_context:
|
||||
if message.get("ts") == thread_ts:
|
||||
the_parent_message_found = True
|
||||
is_no_mention_required = is_no_mention_thread(context, message)
|
||||
break
|
||||
if the_parent_message_found is False:
|
||||
parent_message = find_parent_message(
|
||||
client, context.channel_id, thread_ts
|
||||
)
|
||||
if parent_message is not None:
|
||||
is_no_mention_required = is_no_mention_thread(
|
||||
context, parent_message
|
||||
)
|
||||
|
||||
messages = []
|
||||
user_id = context.actor_user_id or context.user_id
|
||||
last_assistant_idx = -1
|
||||
indices_to_remove = []
|
||||
for idx, reply in enumerate(messages_in_context):
|
||||
maybe_event_type = reply.get("metadata", {}).get("event_type")
|
||||
if maybe_event_type == "chat-gpt-convo":
|
||||
if context.bot_id != reply.get("bot_id"):
|
||||
# Remove messages by a different app
|
||||
indices_to_remove.append(idx)
|
||||
continue
|
||||
maybe_new_messages = (
|
||||
reply.get("metadata", {}).get("event_payload", {}).get("messages")
|
||||
)
|
||||
if maybe_new_messages is not None:
|
||||
if len(messages) == 0 or user_id is None:
|
||||
new_user_id = (
|
||||
reply.get("metadata", {})
|
||||
.get("event_payload", {})
|
||||
.get("user")
|
||||
)
|
||||
if new_user_id is not None:
|
||||
user_id = new_user_id
|
||||
messages = maybe_new_messages
|
||||
last_assistant_idx = idx
|
||||
|
||||
if is_no_mention_required is False:
|
||||
return
|
||||
if is_in_dm_with_bot is False and last_assistant_idx == -1:
|
||||
return
|
||||
|
||||
if is_in_dm_with_bot is True:
|
||||
# To know whether this app needs to start a new convo
|
||||
if not next(filter(lambda msg: msg["role"] == "system", messages), None):
|
||||
# Replace placeholder for Slack user ID in the system prompt
|
||||
system_text = build_system_text(
|
||||
SYSTEM_TEXT, TRANSLATE_MARKDOWN, context
|
||||
)
|
||||
messages.insert(0, {"role": "system", "content": system_text})
|
||||
|
||||
filtered_messages_in_context = []
|
||||
for idx, reply in enumerate(messages_in_context):
|
||||
# Strip bot Slack user ID from initial message
|
||||
if idx == 0:
|
||||
reply["text"] = re.sub(
|
||||
f"<@{context.bot_user_id}>\\s*", "", reply["text"]
|
||||
)
|
||||
if idx not in indices_to_remove:
|
||||
filtered_messages_in_context.append(reply)
|
||||
if len(filtered_messages_in_context) == 0:
|
||||
return
|
||||
|
||||
for reply in filtered_messages_in_context:
|
||||
msg_user_id = reply.get("user")
|
||||
messages.append(
|
||||
{
|
||||
"content": format_openai_message_content(
|
||||
reply.get("text"), TRANSLATE_MARKDOWN
|
||||
),
|
||||
"role": "user",
|
||||
}
|
||||
)
|
||||
|
||||
loading_text = translate(
|
||||
openai_api_key=openai_api_key, context=context, text=DEFAULT_LOADING_TEXT
|
||||
)
|
||||
wip_reply = post_wip_message(
|
||||
client=client,
|
||||
channel=context.channel_id,
|
||||
thread_ts=payload.get("thread_ts") if is_in_dm_with_bot else payload["ts"],
|
||||
loading_text=loading_text,
|
||||
messages=messages,
|
||||
user=user_id,
|
||||
)
|
||||
|
||||
latest_replies = client.conversations_replies(
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply.get("ts"),
|
||||
include_all_metadata=True,
|
||||
limit=1000,
|
||||
)
|
||||
if latest_replies.get("messages", [])[-1]["ts"] != wip_reply["message"]["ts"]:
|
||||
# Since a new reply will come soon, this app abandons this reply
|
||||
client.chat_delete(
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
)
|
||||
return
|
||||
|
||||
resp = ask_llm(messages=messages)
|
||||
print("Reply "+resp)
|
||||
update_wip_message(
|
||||
client=client,
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=resp,
|
||||
messages=messages,
|
||||
user=user_id,
|
||||
)
|
||||
except Timeout:
|
||||
if wip_reply is not None:
|
||||
text = (
|
||||
(
|
||||
wip_reply.get("message", {}).get("text", "")
|
||||
if wip_reply is not None
|
||||
else ""
|
||||
)
|
||||
+ "\n\n"
|
||||
+ translate(
|
||||
openai_api_key=openai_api_key,
|
||||
context=context,
|
||||
text=TIMEOUT_ERROR_MESSAGE,
|
||||
)
|
||||
)
|
||||
client.chat_update(
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=text,
|
||||
)
|
||||
except Exception as e:
|
||||
text = (
|
||||
(
|
||||
wip_reply.get("message", {}).get("text", "")
|
||||
if wip_reply is not None
|
||||
else ""
|
||||
)
|
||||
+ "\n\n"
|
||||
+ f":warning: Failed to reply: {e}"
|
||||
)
|
||||
logger.exception(text, e)
|
||||
if wip_reply is not None:
|
||||
client.chat_update(
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=text,
|
||||
)
|
||||
|
||||
|
||||
def register_listeners(app: App):
|
||||
app.event("app_mention")(ack=just_ack, lazy=[respond_to_app_mention])
|
||||
# app.event("message")(ack=just_ack, lazy=[respond_to_new_message])
|
||||
|
||||
|
||||
MESSAGE_SUBTYPES_TO_SKIP = ["message_changed", "message_deleted"]
|
||||
|
||||
|
||||
# To reduce unnecessary workload in this app,
|
||||
# this before_authorize function skips message changed/deleted events.
|
||||
# Especially, "message_changed" events can be triggered many times when the app rapidly updates its reply.
|
||||
def before_authorize(
|
||||
body: dict,
|
||||
payload: dict,
|
||||
logger: logging.Logger,
|
||||
next_,
|
||||
):
|
||||
if (
|
||||
is_event(body)
|
||||
and payload.get("type") == "message"
|
||||
and payload.get("subtype") in MESSAGE_SUBTYPES_TO_SKIP
|
||||
):
|
||||
logger.debug(
|
||||
"Skipped the following middleware and listeners "
|
||||
f"for this message event (subtype: {payload.get('subtype')})"
|
||||
)
|
||||
return BoltResponse(status=200, body="")
|
||||
next_()
|
||||
43
examples/slack/app/env.py
Normal file
43
examples/slack/app/env.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
|
||||
DEFAULT_SYSTEM_TEXT = """
|
||||
"""
|
||||
|
||||
SYSTEM_TEXT = os.environ.get("OPENAI_SYSTEM_TEXT", DEFAULT_SYSTEM_TEXT)
|
||||
|
||||
DEFAULT_OPENAI_TIMEOUT_SECONDS = 30
|
||||
OPENAI_TIMEOUT_SECONDS = int(
|
||||
os.environ.get("OPENAI_TIMEOUT_SECONDS", DEFAULT_OPENAI_TIMEOUT_SECONDS)
|
||||
)
|
||||
|
||||
DEFAULT_OPENAI_MODEL = "gpt-3.5-turbo"
|
||||
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", DEFAULT_OPENAI_MODEL)
|
||||
|
||||
USE_SLACK_LANGUAGE = os.environ.get("USE_SLACK_LANGUAGE", "true") == "true"
|
||||
|
||||
SLACK_APP_LOG_LEVEL = os.environ.get("SLACK_APP_LOG_LEVEL", "DEBUG")
|
||||
|
||||
TRANSLATE_MARKDOWN = os.environ.get("TRANSLATE_MARKDOWN", "false") == "true"
|
||||
|
||||
BASE_PATH = os.environ.get('OPENAI_API_BASE', 'http://localhost:8080/v1')
|
||||
|
||||
EMBEDDINGS_MODEL = os.environ.get('EMBEDDINGS_MODEL', "all-MiniLM-L6-v2")
|
||||
|
||||
|
||||
EMBEDDINGS_API_BASE = os.environ.get("EMBEDDINGS_API_BASE", BASE_PATH)
|
||||
LOCALAI_API_BASE = os.environ.get("LOCALAI_API_BASE", BASE_PATH)
|
||||
TTS_API_BASE = os.environ.get("TTS_API_BASE", BASE_PATH)
|
||||
IMAGE_API_BASE = os.environ.get("IMAGES_API_BASE", BASE_PATH)
|
||||
|
||||
STABLEDIFFUSION_MODEL = os.environ.get("STABLEDIFFUSION_MODEL", "dreamshaper")
|
||||
FUNCTIONS_MODEL = os.environ.get("FUNCTIONS_MODEL", OPENAI_MODEL)
|
||||
LLM_MODEL = os.environ.get("LLM_MODEL", OPENAI_MODEL)
|
||||
VOICE_MODEL= os.environ.get("TTS_MODEL", "en-us-kathleen-low.onnx" )
|
||||
PERSISTENT_DIR = os.environ.get("PERSISTENT_DIR", "/data")
|
||||
MILVUS_HOST = os.environ.get("MILVUS_HOST", "")
|
||||
MILVUS_PORT = os.environ.get("MILVUS_PORT", 0)
|
||||
MEMORY_COLLECTION = os.environ.get("MEMORY_COLLECTION", "local")
|
||||
MEMORY_CHUNK_SIZE = os.environ.get("MEMORY_CHUNK_SIZE", 600)
|
||||
MEMORY_CHUNK_OVERLAP = os.environ.get("MEMORY_RESULTS", 110)
|
||||
MEMORY_RESULTS = os.environ.get("MEMORY_RESULTS", 3)
|
||||
MEMORY_SEARCH_TYPE = os.environ.get("MEMORY_SEARCH_TYPE", "mmr")
|
||||
75
examples/slack/app/i18n.py
Normal file
75
examples/slack/app/i18n.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from typing import Optional
|
||||
|
||||
import openai
|
||||
from slack_bolt import BoltContext
|
||||
|
||||
from .openai_ops import GPT_3_5_TURBO_0301_MODEL
|
||||
|
||||
# All the supported languages for Slack app as of March 2023
|
||||
_locale_to_lang = {
|
||||
"en-US": "English",
|
||||
"en-GB": "English",
|
||||
"de-DE": "German",
|
||||
"es-ES": "Spanish",
|
||||
"es-LA": "Spanish",
|
||||
"fr-FR": "French",
|
||||
"it-IT": "Italian",
|
||||
"pt-BR": "Portuguese",
|
||||
"ru-RU": "Russian",
|
||||
"ja-JP": "Japanese",
|
||||
"zh-CN": "Chinese",
|
||||
"zh-TW": "Chinese",
|
||||
"ko-KR": "Korean",
|
||||
}
|
||||
|
||||
|
||||
def from_locale_to_lang(locale: Optional[str]) -> Optional[str]:
|
||||
if locale is None:
|
||||
return None
|
||||
return _locale_to_lang.get(locale)
|
||||
|
||||
|
||||
_translation_result_cache = {}
|
||||
|
||||
|
||||
def translate(*, openai_api_key: str, context: BoltContext, text: str) -> str:
|
||||
lang = from_locale_to_lang(context.get("locale"))
|
||||
if lang is None or lang == "English":
|
||||
return text
|
||||
|
||||
cached_result = _translation_result_cache.get(f"{lang}:{text}")
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
response = openai.ChatCompletion.create(
|
||||
api_key=openai_api_key,
|
||||
model=GPT_3_5_TURBO_0301_MODEL,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You're the AI model that primarily focuses on the quality of language translation. "
|
||||
"You must not change the meaning of sentences when translating them into a different language. "
|
||||
"You must provide direct translation result as much as possible. "
|
||||
"When the given text is a single verb/noun, its translated text must be a norm/verb form too. "
|
||||
"Slack's emoji (e.g., :hourglass_flowing_sand:) and mention parts must be kept as-is. "
|
||||
"Your response must not include any additional notes in English. "
|
||||
"Your response must omit English version / pronunciation guide for the result. ",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Can you translate {text} into {lang} in a professional tone? "
|
||||
"Please respond with the only the translated text in a format suitable for Slack user interface. "
|
||||
"No need to append any English notes and guides.",
|
||||
},
|
||||
],
|
||||
top_p=1,
|
||||
n=1,
|
||||
max_tokens=1024,
|
||||
temperature=1,
|
||||
presence_penalty=0,
|
||||
frequency_penalty=0,
|
||||
logit_bias={},
|
||||
user="system",
|
||||
)
|
||||
translated_text = response["choices"][0]["message"].get("content")
|
||||
_translation_result_cache[f"{lang}:{text}"] = translated_text
|
||||
return translated_text
|
||||
53
examples/slack/app/markdown.py
Normal file
53
examples/slack/app/markdown.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import re
|
||||
|
||||
|
||||
# Conversion from Slack mrkdwn to OpenAI markdown
|
||||
# See also: https://api.slack.com/reference/surfaces/formatting#basics
|
||||
def slack_to_markdown(content: str) -> str:
|
||||
# Split the input string into parts based on code blocks and inline code
|
||||
parts = re.split(r"(```.+?```|`[^`\n]+?`)", content)
|
||||
|
||||
# Apply the bold, italic, and strikethrough formatting to text not within code
|
||||
result = ""
|
||||
for part in parts:
|
||||
if part.startswith("```") or part.startswith("`"):
|
||||
result += part
|
||||
else:
|
||||
for o, n in [
|
||||
(r"\*(?!\s)([^\*\n]+?)(?<!\s)\*", r"**\1**"), # *bold* to **bold**
|
||||
(r"_(?!\s)([^_\n]+?)(?<!\s)_", r"*\1*"), # _italic_ to *italic*
|
||||
(r"~(?!\s)([^~\n]+?)(?<!\s)~", r"~~\1~~"), # ~strike~ to ~~strike~~
|
||||
]:
|
||||
part = re.sub(o, n, part)
|
||||
result += part
|
||||
return result
|
||||
|
||||
|
||||
# Conversion from OpenAI markdown to Slack mrkdwn
|
||||
# See also: https://api.slack.com/reference/surfaces/formatting#basics
|
||||
def markdown_to_slack(content: str) -> str:
|
||||
# Split the input string into parts based on code blocks and inline code
|
||||
parts = re.split(r"(```.+?```|`[^`\n]+?`)", content)
|
||||
|
||||
# Apply the bold, italic, and strikethrough formatting to text not within code
|
||||
result = ""
|
||||
for part in parts:
|
||||
if part.startswith("```") or part.startswith("`"):
|
||||
result += part
|
||||
else:
|
||||
for o, n in [
|
||||
(
|
||||
r"\*\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*\*",
|
||||
r"_*\1*_",
|
||||
), # ***bold italic*** to *_bold italic_*
|
||||
(
|
||||
r"(?<![\*_])\*(?!\s)([^\*\n]+?)(?<!\s)\*(?![\*_])",
|
||||
r"_\1_",
|
||||
), # *italic* to _italic_
|
||||
(r"\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*", r"*\1*"), # **bold** to *bold*
|
||||
(r"__(?!\s)([^_\n]+?)(?<!\s)__", r"*\1*"), # __bold__ to *bold*
|
||||
(r"~~(?!\s)([^~\n]+?)(?<!\s)~~", r"~\1~"), # ~~strike~~ to ~strike~
|
||||
]:
|
||||
part = re.sub(o, n, part)
|
||||
result += part
|
||||
return result
|
||||
234
examples/slack/app/openai_ops.py
Normal file
234
examples/slack/app/openai_ops.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import threading
|
||||
import time
|
||||
import re
|
||||
from typing import List, Dict, Any, Generator
|
||||
|
||||
import openai
|
||||
from openai.error import Timeout
|
||||
from openai.openai_object import OpenAIObject
|
||||
import tiktoken
|
||||
|
||||
from slack_bolt import BoltContext
|
||||
from slack_sdk.web import WebClient
|
||||
|
||||
from app.markdown import slack_to_markdown, markdown_to_slack
|
||||
from app.slack_ops import update_wip_message
|
||||
|
||||
from app.agent import (
|
||||
localagi
|
||||
)
|
||||
|
||||
# ----------------------------
|
||||
# Internal functions
|
||||
# ----------------------------
|
||||
|
||||
MAX_TOKENS = 1024
|
||||
GPT_3_5_TURBO_0301_MODEL = "gpt-3.5-turbo-0301"
|
||||
|
||||
|
||||
# Format message from Slack to send to OpenAI
|
||||
def format_openai_message_content(content: str, translate_markdown: bool) -> str:
|
||||
if content is None:
|
||||
return None
|
||||
|
||||
# Unescape &, < and >, since Slack replaces these with their HTML equivalents
|
||||
# See also: https://api.slack.com/reference/surfaces/formatting#escaping
|
||||
content = content.replace("<", "<").replace(">", ">").replace("&", "&")
|
||||
|
||||
# Convert from Slack mrkdwn to markdown format
|
||||
if translate_markdown:
|
||||
content = slack_to_markdown(content)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def ask_llm(
|
||||
*,
|
||||
messages: List[Dict[str, str]],
|
||||
) -> str:
|
||||
# Remove old messages to make sure we have room for max_tokens
|
||||
# See also: https://platform.openai.com/docs/guides/chat/introduction
|
||||
# > total tokens must be below the model’s maximum limit (4096 tokens for gpt-3.5-turbo-0301)
|
||||
# TODO: currently we don't pass gpt-4 to this calculation method
|
||||
while calculate_num_tokens(messages) >= 4096 - MAX_TOKENS:
|
||||
removed = False
|
||||
for i, message in enumerate(messages):
|
||||
if message["role"] in ("user", "assistant"):
|
||||
del messages[i]
|
||||
removed = True
|
||||
break
|
||||
if not removed:
|
||||
# Fall through and let the OpenAI error handler deal with it
|
||||
break
|
||||
|
||||
prompt=""
|
||||
|
||||
for i, message in enumerate(messages):
|
||||
prompt += message["content"] + "\n"
|
||||
|
||||
return localagi(prompt)
|
||||
|
||||
def consume_openai_stream_to_write_reply(
|
||||
*,
|
||||
client: WebClient,
|
||||
wip_reply: dict,
|
||||
context: BoltContext,
|
||||
user_id: str,
|
||||
messages: List[Dict[str, str]],
|
||||
steam: Generator[OpenAIObject, Any, None],
|
||||
timeout_seconds: int,
|
||||
translate_markdown: bool,
|
||||
):
|
||||
start_time = time.time()
|
||||
assistant_reply: Dict[str, str] = {"role": "assistant", "content": ""}
|
||||
messages.append(assistant_reply)
|
||||
word_count = 0
|
||||
threads = []
|
||||
try:
|
||||
loading_character = " ... :writing_hand:"
|
||||
for chunk in steam:
|
||||
spent_seconds = time.time() - start_time
|
||||
if timeout_seconds < spent_seconds:
|
||||
raise Timeout()
|
||||
item = chunk.choices[0]
|
||||
if item.get("finish_reason") is not None:
|
||||
break
|
||||
delta = item.get("delta")
|
||||
if delta.get("content") is not None:
|
||||
word_count += 1
|
||||
assistant_reply["content"] += delta.get("content")
|
||||
if word_count >= 20:
|
||||
|
||||
def update_message():
|
||||
assistant_reply_text = format_assistant_reply(
|
||||
assistant_reply["content"], translate_markdown
|
||||
)
|
||||
wip_reply["message"]["text"] = assistant_reply_text
|
||||
update_wip_message(
|
||||
client=client,
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=assistant_reply_text + loading_character,
|
||||
messages=messages,
|
||||
user=user_id,
|
||||
)
|
||||
|
||||
thread = threading.Thread(target=update_message)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
word_count = 0
|
||||
|
||||
for t in threads:
|
||||
try:
|
||||
if t.is_alive():
|
||||
t.join()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assistant_reply_text = format_assistant_reply(
|
||||
assistant_reply["content"], translate_markdown
|
||||
)
|
||||
wip_reply["message"]["text"] = assistant_reply_text
|
||||
update_wip_message(
|
||||
client=client,
|
||||
channel=context.channel_id,
|
||||
ts=wip_reply["message"]["ts"],
|
||||
text=assistant_reply_text,
|
||||
messages=messages,
|
||||
user=user_id,
|
||||
)
|
||||
finally:
|
||||
for t in threads:
|
||||
try:
|
||||
if t.is_alive():
|
||||
t.join()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
steam.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def calculate_num_tokens(
|
||||
messages: List[Dict[str, str]],
|
||||
# TODO: adjustment for gpt-4
|
||||
model: str = GPT_3_5_TURBO_0301_MODEL,
|
||||
) -> int:
|
||||
"""Returns the number of tokens used by a list of messages."""
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model)
|
||||
except KeyError:
|
||||
encoding = tiktoken.get_encoding("cl100k_base")
|
||||
if model == GPT_3_5_TURBO_0301_MODEL:
|
||||
# note: future models may deviate from this
|
||||
num_tokens = 0
|
||||
for message in messages:
|
||||
# every message follows <im_start>{role/name}\n{content}<im_end>\n
|
||||
num_tokens += 4
|
||||
for key, value in message.items():
|
||||
num_tokens += len(encoding.encode(value))
|
||||
if key == "name": # if there's a name, the role is omitted
|
||||
num_tokens += -1 # role is always required and always 1 token
|
||||
num_tokens += 2 # every reply is primed with <im_start>assistant
|
||||
return num_tokens
|
||||
else:
|
||||
error = (
|
||||
f"Calculating the number of tokens for for model {model} is not yet supported. "
|
||||
"See https://github.com/openai/openai-python/blob/main/chatml.md "
|
||||
"for information on how messages are converted to tokens."
|
||||
)
|
||||
raise NotImplementedError(error)
|
||||
|
||||
|
||||
# Format message from OpenAI to display in Slack
|
||||
def format_assistant_reply(content: str, translate_markdown: bool) -> str:
|
||||
for o, n in [
|
||||
# Remove leading newlines
|
||||
("^\n+", ""),
|
||||
# Remove prepended Slack user ID
|
||||
("^<@U.*?>\\s?:\\s?", ""),
|
||||
# Remove OpenAI syntax tags since Slack doesn't render them in a message
|
||||
("```\\s*[Rr]ust\n", "```\n"),
|
||||
("```\\s*[Rr]uby\n", "```\n"),
|
||||
("```\\s*[Ss]cala\n", "```\n"),
|
||||
("```\\s*[Kk]otlin\n", "```\n"),
|
||||
("```\\s*[Jj]ava\n", "```\n"),
|
||||
("```\\s*[Gg]o\n", "```\n"),
|
||||
("```\\s*[Ss]wift\n", "```\n"),
|
||||
("```\\s*[Oo]objective[Cc]\n", "```\n"),
|
||||
("```\\s*[Cc]\n", "```\n"),
|
||||
("```\\s*[Cc][+][+]\n", "```\n"),
|
||||
("```\\s*[Cc][Pp][Pp]\n", "```\n"),
|
||||
("```\\s*[Cc]sharp\n", "```\n"),
|
||||
("```\\s*[Mm]atlab\n", "```\n"),
|
||||
("```\\s*[Jj][Ss][Oo][Nn]\n", "```\n"),
|
||||
("```\\s*[Ll]a[Tt]e[Xx]\n", "```\n"),
|
||||
("```\\s*bash\n", "```\n"),
|
||||
("```\\s*zsh\n", "```\n"),
|
||||
("```\\s*sh\n", "```\n"),
|
||||
("```\\s*[Ss][Qq][Ll]\n", "```\n"),
|
||||
("```\\s*[Pp][Hh][Pp]\n", "```\n"),
|
||||
("```\\s*[Pp][Ee][Rr][Ll]\n", "```\n"),
|
||||
("```\\s*[Jj]ava[Ss]cript", "```\n"),
|
||||
("```\\s*[Ty]ype[Ss]cript", "```\n"),
|
||||
("```\\s*[Pp]ython\n", "```\n"),
|
||||
]:
|
||||
content = re.sub(o, n, content)
|
||||
|
||||
# Convert from OpenAI markdown to Slack mrkdwn format
|
||||
if translate_markdown:
|
||||
content = markdown_to_slack(content)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def build_system_text(
|
||||
system_text_template: str, translate_markdown: bool, context: BoltContext
|
||||
):
|
||||
system_text = system_text_template.format(bot_user_id=context.bot_user_id)
|
||||
# Translate format hint in system prompt
|
||||
if translate_markdown is True:
|
||||
system_text = slack_to_markdown(system_text)
|
||||
return system_text
|
||||
110
examples/slack/app/slack_ops.py
Normal file
110
examples/slack/app/slack_ops.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from typing import Optional
|
||||
from typing import List, Dict
|
||||
|
||||
from slack_sdk.web import WebClient, SlackResponse
|
||||
from slack_bolt import BoltContext
|
||||
|
||||
# ----------------------------
|
||||
# General operations in a channel
|
||||
# ----------------------------
|
||||
|
||||
|
||||
def find_parent_message(
|
||||
client: WebClient, channel_id: Optional[str], thread_ts: Optional[str]
|
||||
) -> Optional[dict]:
|
||||
if channel_id is None or thread_ts is None:
|
||||
return None
|
||||
|
||||
messages = client.conversations_history(
|
||||
channel=channel_id,
|
||||
latest=thread_ts,
|
||||
limit=1,
|
||||
inclusive=1,
|
||||
).get("messages", [])
|
||||
|
||||
return messages[0] if len(messages) > 0 else None
|
||||
|
||||
|
||||
def is_no_mention_thread(context: BoltContext, parent_message: dict) -> bool:
|
||||
parent_message_text = parent_message.get("text", "")
|
||||
return f"<@{context.bot_user_id}>" in parent_message_text
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# WIP reply message stuff
|
||||
# ----------------------------
|
||||
|
||||
|
||||
def post_wip_message(
|
||||
*,
|
||||
client: WebClient,
|
||||
channel: str,
|
||||
thread_ts: str,
|
||||
loading_text: str,
|
||||
messages: List[Dict[str, str]],
|
||||
user: str,
|
||||
) -> SlackResponse:
|
||||
system_messages = [msg for msg in messages if msg["role"] == "system"]
|
||||
return client.chat_postMessage(
|
||||
channel=channel,
|
||||
thread_ts=thread_ts,
|
||||
text=loading_text,
|
||||
metadata={
|
||||
"event_type": "chat-gpt-convo",
|
||||
"event_payload": {"messages": system_messages, "user": user},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def update_wip_message(
|
||||
client: WebClient,
|
||||
channel: str,
|
||||
ts: str,
|
||||
text: str,
|
||||
messages: List[Dict[str, str]],
|
||||
user: str,
|
||||
) -> SlackResponse:
|
||||
system_messages = [msg for msg in messages if msg["role"] == "system"]
|
||||
return client.chat_update(
|
||||
channel=channel,
|
||||
ts=ts,
|
||||
text=text,
|
||||
metadata={
|
||||
"event_type": "chat-gpt-convo",
|
||||
"event_payload": {"messages": system_messages, "user": user},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Home tab
|
||||
# ----------------------------
|
||||
|
||||
DEFAULT_HOME_TAB_MESSAGE = (
|
||||
"To enable this app in this Slack workspace, you need to save your OpenAI API key. "
|
||||
"Visit <https://platform.openai.com/account/api-keys|your developer page> to grap your key!"
|
||||
)
|
||||
|
||||
DEFAULT_HOME_TAB_CONFIGURE_LABEL = "Configure"
|
||||
|
||||
|
||||
def build_home_tab(message: str, configure_label: str) -> dict:
|
||||
return {
|
||||
"type": "home",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": message,
|
||||
},
|
||||
"accessory": {
|
||||
"action_id": "configure",
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": configure_label},
|
||||
"style": "primary",
|
||||
"value": "api_key",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
12
examples/slack/entrypoint.sh
Executable file
12
examples/slack/entrypoint.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /app
|
||||
|
||||
pip uninstall hnswlib -y
|
||||
|
||||
git clone https://github.com/nmslib/hnswlib.git
|
||||
cd hnswlib
|
||||
pip install .
|
||||
cd ..
|
||||
|
||||
python main.py
|
||||
69
examples/slack/main.py
Normal file
69
examples/slack/main.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from slack_bolt import App, BoltContext
|
||||
from slack_sdk.web import WebClient
|
||||
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
|
||||
|
||||
from app.bolt_listeners import before_authorize, register_listeners
|
||||
from app.env import *
|
||||
from app.slack_ops import (
|
||||
build_home_tab,
|
||||
DEFAULT_HOME_TAB_MESSAGE,
|
||||
DEFAULT_HOME_TAB_CONFIGURE_LABEL,
|
||||
)
|
||||
from app.i18n import translate
|
||||
|
||||
if __name__ == "__main__":
|
||||
from slack_bolt.adapter.socket_mode import SocketModeHandler
|
||||
|
||||
logging.basicConfig(level=SLACK_APP_LOG_LEVEL)
|
||||
|
||||
app = App(
|
||||
token=os.environ["SLACK_BOT_TOKEN"],
|
||||
before_authorize=before_authorize,
|
||||
process_before_response=True,
|
||||
)
|
||||
app.client.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2))
|
||||
|
||||
register_listeners(app)
|
||||
|
||||
@app.event("app_home_opened")
|
||||
def render_home_tab(client: WebClient, context: BoltContext):
|
||||
already_set_api_key = os.environ["OPENAI_API_KEY"]
|
||||
text = translate(
|
||||
openai_api_key=already_set_api_key,
|
||||
context=context,
|
||||
text=DEFAULT_HOME_TAB_MESSAGE,
|
||||
)
|
||||
configure_label = translate(
|
||||
openai_api_key=already_set_api_key,
|
||||
context=context,
|
||||
text=DEFAULT_HOME_TAB_CONFIGURE_LABEL,
|
||||
)
|
||||
client.views_publish(
|
||||
user_id=context.user_id,
|
||||
view=build_home_tab(text, configure_label),
|
||||
)
|
||||
|
||||
if USE_SLACK_LANGUAGE is True:
|
||||
|
||||
@app.middleware
|
||||
def set_locale(
|
||||
context: BoltContext,
|
||||
client: WebClient,
|
||||
next_,
|
||||
):
|
||||
user_id = context.actor_user_id or context.user_id
|
||||
user_info = client.users_info(user=user_id, include_locale=True)
|
||||
context["locale"] = user_info.get("user", {}).get("locale")
|
||||
next_()
|
||||
|
||||
@app.middleware
|
||||
def set_openai_api_key(context: BoltContext, next_):
|
||||
context["OPENAI_API_KEY"] = os.environ["OPENAI_API_KEY"]
|
||||
context["OPENAI_MODEL"] = OPENAI_MODEL
|
||||
next_()
|
||||
|
||||
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
|
||||
handler.start()
|
||||
306
examples/slack/main_prod.py
Normal file
306
examples/slack/main_prod.py
Normal file
@@ -0,0 +1,306 @@
|
||||
# Unzip the dependencies managed by serverless-python-requirements
|
||||
try:
|
||||
import unzip_requirements # type:ignore
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
#
|
||||
# Imports
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import openai
|
||||
|
||||
from slack_sdk.web import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
|
||||
from slack_bolt import App, Ack, BoltContext
|
||||
|
||||
from app.bolt_listeners import register_listeners, before_authorize
|
||||
from app.env import USE_SLACK_LANGUAGE, SLACK_APP_LOG_LEVEL, DEFAULT_OPENAI_MODEL
|
||||
from app.slack_ops import (
|
||||
build_home_tab,
|
||||
DEFAULT_HOME_TAB_MESSAGE,
|
||||
DEFAULT_HOME_TAB_CONFIGURE_LABEL,
|
||||
)
|
||||
from app.i18n import translate
|
||||
|
||||
#
|
||||
# Product deployment (AWS Lambda)
|
||||
#
|
||||
# export SLACK_CLIENT_ID=
|
||||
# export SLACK_CLIENT_SECRET=
|
||||
# export SLACK_SIGNING_SECRET=
|
||||
# export SLACK_SCOPES=app_mentions:read,channels:history,groups:history,im:history,mpim:history,chat:write.public,chat:write,users:read
|
||||
# export SLACK_INSTALLATION_S3_BUCKET_NAME=
|
||||
# export SLACK_STATE_S3_BUCKET_NAME=
|
||||
# export OPENAI_S3_BUCKET_NAME=
|
||||
# npm install -g serverless
|
||||
# serverless plugin install -n serverless-python-requirements
|
||||
# serverless deploy
|
||||
#
|
||||
|
||||
import boto3
|
||||
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
|
||||
from slack_bolt.adapter.aws_lambda.lambda_s3_oauth_flow import LambdaS3OAuthFlow
|
||||
|
||||
SlackRequestHandler.clear_all_log_handlers()
|
||||
logging.basicConfig(format="%(asctime)s %(message)s", level=SLACK_APP_LOG_LEVEL)
|
||||
|
||||
s3_client = boto3.client("s3")
|
||||
openai_bucket_name = os.environ["OPENAI_S3_BUCKET_NAME"]
|
||||
|
||||
client_template = WebClient()
|
||||
client_template.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2))
|
||||
|
||||
|
||||
def register_revocation_handlers(app: App):
|
||||
# Handle uninstall events and token revocations
|
||||
@app.event("tokens_revoked")
|
||||
def handle_tokens_revoked_events(
|
||||
event: dict,
|
||||
context: BoltContext,
|
||||
logger: logging.Logger,
|
||||
):
|
||||
user_ids = event.get("tokens", {}).get("oauth", [])
|
||||
if len(user_ids) > 0:
|
||||
for user_id in user_ids:
|
||||
app.installation_store.delete_installation(
|
||||
enterprise_id=context.enterprise_id,
|
||||
team_id=context.team_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
bots = event.get("tokens", {}).get("bot", [])
|
||||
if len(bots) > 0:
|
||||
app.installation_store.delete_bot(
|
||||
enterprise_id=context.enterprise_id,
|
||||
team_id=context.team_id,
|
||||
)
|
||||
try:
|
||||
s3_client.delete_object(Bucket=openai_bucket_name, Key=context.team_id)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete an OpenAI auth key: (team_id: {context.team_id}, error: {e})"
|
||||
)
|
||||
|
||||
@app.event("app_uninstalled")
|
||||
def handle_app_uninstalled_events(
|
||||
context: BoltContext,
|
||||
logger: logging.Logger,
|
||||
):
|
||||
app.installation_store.delete_all(
|
||||
enterprise_id=context.enterprise_id,
|
||||
team_id=context.team_id,
|
||||
)
|
||||
try:
|
||||
s3_client.delete_object(Bucket=openai_bucket_name, Key=context.team_id)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete an OpenAI auth key: (team_id: {context.team_id}, error: {e})"
|
||||
)
|
||||
|
||||
|
||||
def handler(event, context_):
|
||||
app = App(
|
||||
process_before_response=True,
|
||||
before_authorize=before_authorize,
|
||||
oauth_flow=LambdaS3OAuthFlow(),
|
||||
client=client_template,
|
||||
)
|
||||
app.oauth_flow.settings.install_page_rendering_enabled = False
|
||||
register_listeners(app)
|
||||
register_revocation_handlers(app)
|
||||
|
||||
if USE_SLACK_LANGUAGE is True:
|
||||
|
||||
@app.middleware
|
||||
def set_locale(
|
||||
context: BoltContext,
|
||||
client: WebClient,
|
||||
logger: logging.Logger,
|
||||
next_,
|
||||
):
|
||||
bot_scopes = context.authorize_result.bot_scopes
|
||||
if bot_scopes is not None and "users:read" in bot_scopes:
|
||||
user_id = context.actor_user_id or context.user_id
|
||||
try:
|
||||
user_info = client.users_info(user=user_id, include_locale=True)
|
||||
context["locale"] = user_info.get("user", {}).get("locale")
|
||||
except SlackApiError as e:
|
||||
logger.debug(f"Failed to fetch user info due to {e}")
|
||||
pass
|
||||
next_()
|
||||
|
||||
@app.middleware
|
||||
def set_s3_openai_api_key(context: BoltContext, next_):
|
||||
try:
|
||||
s3_response = s3_client.get_object(
|
||||
Bucket=openai_bucket_name, Key=context.team_id
|
||||
)
|
||||
config_str: str = s3_response["Body"].read().decode("utf-8")
|
||||
if config_str.startswith("{"):
|
||||
config = json.loads(config_str)
|
||||
context["OPENAI_API_KEY"] = config.get("api_key")
|
||||
context["OPENAI_MODEL"] = config.get("model")
|
||||
else:
|
||||
# The legacy data format
|
||||
context["OPENAI_API_KEY"] = config_str
|
||||
context["OPENAI_MODEL"] = DEFAULT_OPENAI_MODEL
|
||||
except: # noqa: E722
|
||||
context["OPENAI_API_KEY"] = None
|
||||
next_()
|
||||
|
||||
@app.event("app_home_opened")
|
||||
def render_home_tab(client: WebClient, context: BoltContext):
|
||||
message = DEFAULT_HOME_TAB_MESSAGE
|
||||
configure_label = DEFAULT_HOME_TAB_CONFIGURE_LABEL
|
||||
try:
|
||||
s3_client.get_object(Bucket=openai_bucket_name, Key=context.team_id)
|
||||
message = "This app is ready to use in this workspace :raised_hands:"
|
||||
except: # noqa: E722
|
||||
pass
|
||||
|
||||
openai_api_key = context.get("OPENAI_API_KEY")
|
||||
if openai_api_key is not None:
|
||||
message = translate(
|
||||
openai_api_key=openai_api_key, context=context, text=message
|
||||
)
|
||||
configure_label = translate(
|
||||
openai_api_key=openai_api_key,
|
||||
context=context,
|
||||
text=DEFAULT_HOME_TAB_CONFIGURE_LABEL,
|
||||
)
|
||||
|
||||
client.views_publish(
|
||||
user_id=context.user_id,
|
||||
view=build_home_tab(message, configure_label),
|
||||
)
|
||||
|
||||
@app.action("configure")
|
||||
def handle_some_action(ack, body: dict, client: WebClient, context: BoltContext):
|
||||
ack()
|
||||
already_set_api_key = context.get("OPENAI_API_KEY")
|
||||
api_key_text = "Save your OpenAI API key:"
|
||||
submit = "Submit"
|
||||
cancel = "Cancel"
|
||||
if already_set_api_key is not None:
|
||||
api_key_text = translate(
|
||||
openai_api_key=already_set_api_key, context=context, text=api_key_text
|
||||
)
|
||||
submit = translate(
|
||||
openai_api_key=already_set_api_key, context=context, text=submit
|
||||
)
|
||||
cancel = translate(
|
||||
openai_api_key=already_set_api_key, context=context, text=cancel
|
||||
)
|
||||
|
||||
client.views_open(
|
||||
trigger_id=body["trigger_id"],
|
||||
view={
|
||||
"type": "modal",
|
||||
"callback_id": "configure",
|
||||
"title": {"type": "plain_text", "text": "OpenAI API Key"},
|
||||
"submit": {"type": "plain_text", "text": submit},
|
||||
"close": {"type": "plain_text", "text": cancel},
|
||||
"blocks": [
|
||||
{
|
||||
"type": "input",
|
||||
"block_id": "api_key",
|
||||
"label": {"type": "plain_text", "text": api_key_text},
|
||||
"element": {"type": "plain_text_input", "action_id": "input"},
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"block_id": "model",
|
||||
"label": {"type": "plain_text", "text": "OpenAI Model"},
|
||||
"element": {
|
||||
"type": "static_select",
|
||||
"action_id": "input",
|
||||
"options": [
|
||||
{
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "GPT-3.5 Turbo",
|
||||
},
|
||||
"value": "gpt-3.5-turbo",
|
||||
},
|
||||
{
|
||||
"text": {"type": "plain_text", "text": "GPT-4"},
|
||||
"value": "gpt-4",
|
||||
},
|
||||
],
|
||||
"initial_option": {
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "GPT-3.5 Turbo",
|
||||
},
|
||||
"value": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
def validate_api_key_registration(ack: Ack, view: dict, context: BoltContext):
|
||||
already_set_api_key = context.get("OPENAI_API_KEY")
|
||||
|
||||
inputs = view["state"]["values"]
|
||||
api_key = inputs["api_key"]["input"]["value"]
|
||||
model = inputs["model"]["input"]["selected_option"]["value"]
|
||||
try:
|
||||
# Verify if the API key is valid
|
||||
openai.Model.retrieve(api_key=api_key, id="gpt-3.5-turbo")
|
||||
try:
|
||||
# Verify if the given model works with the API key
|
||||
openai.Model.retrieve(api_key=api_key, id=model)
|
||||
except Exception:
|
||||
text = "This model is not yet available for this API key"
|
||||
if already_set_api_key is not None:
|
||||
text = translate(
|
||||
openai_api_key=already_set_api_key, context=context, text=text
|
||||
)
|
||||
ack(
|
||||
response_action="errors",
|
||||
errors={"model": text},
|
||||
)
|
||||
return
|
||||
ack()
|
||||
except Exception:
|
||||
text = "This API key seems to be invalid"
|
||||
if already_set_api_key is not None:
|
||||
text = translate(
|
||||
openai_api_key=already_set_api_key, context=context, text=text
|
||||
)
|
||||
ack(
|
||||
response_action="errors",
|
||||
errors={"api_key": text},
|
||||
)
|
||||
|
||||
def save_api_key_registration(
|
||||
view: dict,
|
||||
logger: logging.Logger,
|
||||
context: BoltContext,
|
||||
):
|
||||
inputs = view["state"]["values"]
|
||||
api_key = inputs["api_key"]["input"]["value"]
|
||||
model = inputs["model"]["input"]["selected_option"]["value"]
|
||||
try:
|
||||
openai.Model.retrieve(api_key=api_key, id=model)
|
||||
s3_client.put_object(
|
||||
Bucket=openai_bucket_name,
|
||||
Key=context.team_id,
|
||||
Body=json.dumps({"api_key": api_key, "model": model}),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
app.view("configure")(
|
||||
ack=validate_api_key_registration,
|
||||
lazy=[save_api_key_registration],
|
||||
)
|
||||
|
||||
slack_handler = SlackRequestHandler(app=app)
|
||||
return slack_handler.handle(event, context_)
|
||||
32
examples/slack/manifest-dev.yml
Normal file
32
examples/slack/manifest-dev.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
display_information:
|
||||
name: ChatGPT (dev)
|
||||
features:
|
||||
app_home:
|
||||
home_tab_enabled: false
|
||||
messages_tab_enabled: true
|
||||
messages_tab_read_only_enabled: false
|
||||
bot_user:
|
||||
display_name: ChatGPT Bot (dev)
|
||||
always_online: true
|
||||
oauth_config:
|
||||
scopes:
|
||||
bot:
|
||||
- app_mentions:read
|
||||
- channels:history
|
||||
- groups:history
|
||||
- im:history
|
||||
- mpim:history
|
||||
- chat:write.public
|
||||
- chat:write
|
||||
- users:read
|
||||
settings:
|
||||
event_subscriptions:
|
||||
bot_events:
|
||||
- app_mention
|
||||
- message.channels
|
||||
- message.groups
|
||||
- message.im
|
||||
- message.mpim
|
||||
interactivity:
|
||||
is_enabled: true
|
||||
socket_mode_enabled: true
|
||||
43
examples/slack/manifest-prod.yml
Normal file
43
examples/slack/manifest-prod.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
display_information:
|
||||
name: ChatGPT
|
||||
description: Interact with ChatGPT in Slack!
|
||||
background_color: "#195208"
|
||||
features:
|
||||
app_home:
|
||||
home_tab_enabled: true
|
||||
messages_tab_enabled: true
|
||||
messages_tab_read_only_enabled: false
|
||||
bot_user:
|
||||
display_name: ChatGPT Bot
|
||||
always_online: true
|
||||
oauth_config:
|
||||
redirect_urls:
|
||||
- https://TODO.amazonaws.com/slack/oauth_redirect
|
||||
scopes:
|
||||
bot:
|
||||
- app_mentions:read
|
||||
- channels:history
|
||||
- groups:history
|
||||
- im:history
|
||||
- mpim:history
|
||||
- chat:write.public
|
||||
- chat:write
|
||||
- users:read
|
||||
settings:
|
||||
event_subscriptions:
|
||||
request_url: https://TODO.amazonaws.com/slack/events
|
||||
bot_events:
|
||||
- app_home_opened
|
||||
- app_mention
|
||||
- app_uninstalled
|
||||
- message.channels
|
||||
- message.groups
|
||||
- message.im
|
||||
- message.mpim
|
||||
- tokens_revoked
|
||||
interactivity:
|
||||
is_enabled: true
|
||||
request_url: https://TODO.amazonaws.com/slack/events
|
||||
org_deploy_enabled: false
|
||||
socket_mode_enabled: false
|
||||
token_rotation_enabled: false
|
||||
15
examples/slack/requirements.txt
Normal file
15
examples/slack/requirements.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
slack-bolt>=1.18.0,<2
|
||||
lxml==4.9.2
|
||||
bs4==0.0.1
|
||||
openai>=0.27.4,<0.28
|
||||
tiktoken>=0.3.3,<0.4
|
||||
chromadb==0.3.23
|
||||
langchain==0.0.242
|
||||
GitPython==3.1.31
|
||||
InstructorEmbedding
|
||||
loguru
|
||||
git+https://github.com/mudler/LocalAGI
|
||||
pysqlite3-binary
|
||||
requests
|
||||
ascii-magic
|
||||
duckduckgo_search
|
||||
2
examples/slack/run.sh
Normal file
2
examples/slack/run.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
docker build -t slack-bot .
|
||||
docker run -v $PWD/data:/data --rm -ti --env-file .dockerenv slack-bot
|
||||
Reference in New Issue
Block a user