在这篇文章中,我们将引导你完成构建和部署 Slackbot 的过程,该机器人可以监听你的对话,从中学习,并使用这些知识来回答有关你的 Slack 工作区中发生的情况的问题。
第1步:创建 Slack 应用程序并将其安装到你的工作区
首先我们引入你的依赖项。如果你还没有安装它们,则需要使用 pip 或诗歌来安装它们。
from flask import Flask, request, jsonify
现在,我们将创建你的Flask应用程序并设置它,以便它可以在开发中运行。
flask_app = Flask(__name__)
if __name__ == "__main__":
flask_app.run(port=3000)
在这些行之间,我们将添加我们的基本路由:如果接收到的POST请求包含具有挑战键的JSON对象,我们将返回该键的值。否则我们将什么也不做。
@flask_app.route("/", methods=["POST"])
def slack_challenge():
if request.json and "challenge" in request.json:
print("Received challenge")
return jsonify({"challenge": request.json["challenge"]})
else:
print("Got unknown request incoming")
print(request.json)
return
使你的应用程序可供Slack使用
要配置Slack应用程序,它需要在Slack可以看到它的地方运行。所以让我们运行我们的Slack应用程序:
python 1_flask.pypy
这一步你需要下载并安装ngrok。安装后,运行以下命令,以便它可以找到在端口3000上运行的应用程序:
ngrok http 3000
ngrok会给你一个HTTPS URL,比如https://1bf6-64-38-189-168.ngrok-free.app.记下它,因为我们需要把它交给Slack。此外,请记住,如果你停止ngrok并重新启动它,此URL将会更改,你需要告诉Slack这一点。你只需要在开发过程中使用它。
向Slack注册你的应用
转到Slack API站点并单击“创建新应用程序”。你会看到这样的屏幕,你会想要选择“从头开始”:
选择一个友好的名称和要将其安装到的工作区。你会看到这样的屏幕:
接下来,你需要设置应用程序所需的权限。点击右下角的“权限”链接:
这将带你进入“范围”屏幕,你需要在其中添加你在此图片中看到的所有范围,即:
一旦你保存了这些范围,向上滚动到“安装到工作区”来安装你的应用程序。
你现在需要告诉Slack你的应用程序在哪里,这样你就可以从它那里接收消息。单击左侧导航中的“ Event Subscriptions ”(事件订阅)链接并填写,使其看起来像这样,具体如下:
如果你的应用程序正在运行,并且ngrok正在正确地进行隧道传输,则应验证你的请求URL。
第2步:加入频道并回复消息
为此,我们需要扩展我们的应用程序。你可以在 中看到这一步的最终结果2_join_and_reply.py。让我们看一下我们添加的内容:
import dotenv, os
dotenv.load_dotenv()
我们需要一些环境变量,因此你需要添加这些行并安装python-dotenv。你还需要在项目的根目录中创建一个具有三个值的.env文件:
我们将使用Slack方便的Python SDK来构建我们的应用程序,所以PIP安装Slack-Bolt,然后更新我们所有的导入:
from slack_bolt import App
from flask import Flask, request, jsonify
from slack_bolt.adapter.flask import SlackRequestHandler
现在使用我们刚才设置的秘密初始化一个Slack Bolt应用程序:
app = App(
token=os.environ.get("SLACK_BOT_TOKEN"),
signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)
handler = SlackRequestHandler(app)
要收听消息,机器人必须在一个频道中。你可以让它加入任何和所有的公共频道,但出于测试的目的,我已经创建了一个名为#bot-testing的频道,这就是它在这里加入的频道:
channel_list = app.client.conversations_list().data
channel = next((channel for channel in channel_list.get('channels') if channel.get("name") == "bot-testing"), None)
channel_id = channel.get('id')
app.client.conversations_join(channel=channel_id)
App.Client是Bolt框架的Slack WebClient,因此你可以直接在框架中执行WebClient可以执行的任何操作。这里最后添加的是一个非常简单的消息侦听器:
@app.message()
def reply(message, say):
print(message)
say("Yes?")
在Bolt框架中,@app.message修饰器告诉框架在接收到消息事件时触发此方法。say参数是将消息发送回消息来源通道的函数。因此,每次收到消息时,此代码都会向通道发送一条消息,说“是吗?”。
让我们试试吧!停止运行1_flask.py并运行Python 2_join_和_reply.py。你不需要重新启动ngrok,它将像以前一样继续向端口3000发送消息。下面是我的尝试:
成功!我们有一个非常烦人的机器人,它会回复任何人说的每一件事。我们可以做得更好!
第3步:仅回复提到机器人的消息
从表面上看,这是一个非常简单的更改,但 Slack 的传入消息格式有点复杂,因此我们必须添加相当多的代码。你可以在 中看到最终结果3_reply_to_mentions.py。
首先,要知道我们的机器人何时被提及,我们需要机器人的用户ID。在后台,Slack不使用用户名,甚至不使用@-handle,而是在所有Slack安装中使用全局唯一的ID。我们必须得到:
auth_response = app.client.auth_test()
bot_user_id = auth_response["user_id"]
现在,我们添加了一个令人讨厌的复杂代码块,它解析Slack的消息对象,以查看传入消息中提到的用户。如果是机器人,机器人就会回复,否则它就会忽略这条消息。随着我们的深入,我们将把发送给机器人的消息视为“查询”,并将任何其他消息视为要存储的“事实”,但我们现在还不会存储它。
@app.message()
def reply(message, say):
if message.get('blocks'):
for block in message.get('blocks'):
if block.get('type') == 'rich_text':
for rich_text_section in block.get('elements'):
for element in rich_text_section.get('elements'):
if element.get('type') == 'user' and element.get('user_id') == bot_user_id:
for element in rich_text_section.get('elements'):
if element.get('type') == 'text':
query = element.get('text')
print(f"Somebody asked the bot: {query}")
say("Yes?")
return
# otherwise do something else with it
print("Saw a fact: ", message.get('text'))
哦。花了一段时间才恢复正常。但现在我们的机器人只在提到它时才回复:
第4步:使用llamaindex存储事实并回答问题
你将4_incremental_rag.py看到一个简单的命令行 Python 脚本的演示,该脚本使用 LlamaIndex 来存储事实并回答问题。我不会引导你完成每一行(脚本对此有有用的注释),但让我们看看重要的几行。记得pip install llama-index!
首先我们创建一个新的VectorStoreIndex,一个内存中的向量存储,我们将在其中存储我们的事实。它开始是空的。
index = VectorStoreIndex([])
接下来,我们创建3个文档对象,并将它们分别插入到索引中。真正的文档可以是巨大的文本块,整个PDF,甚至是图像,但这些只是一些简单的、松散的消息大小的事实。
doc1 = Document(text="Molly is a cat")
doc2 = Document(text="Doug is a dog")
doc3 = Document(text="Carl is a rat")
index.insert(doc1)
index.insert(doc2)
index.insert(doc3)
最后,我们从索引中创建一个查询引擎,并问它一个问题:
# run a query
query_engine = index.as_query_engine()
response = query_engine.query("Who is Molly?")
print(response)
结果是“ Molly is a cat ”加上一大堆调试信息,因为我们在4_增量_RAG.PY中打开了嘈杂的调试。你可以看到我们发送给LLM的提示、它从索引中检索到的上下文,以及它生成并发送回给我们的响应。
第5步:使用llamaindex在Slack中存储事实和回答问题
我们5_rag_in_slack.py将之前的两件事结合起来:脚本 3,我们在其中回复查询;脚本 4,我们在其中存储事实并回答问题。再次强调,我们不会遍历每一行,但以下是重要的更改:
如果你还没有安装Llama-Index,请首先安装PIP,并引入你的DEP。使用索引时初始化索引:
from llama_index import VectorStoreIndex, Document
index = VectorStoreIndex([])
以前我们只是回复“ Yes?”(第73行),现在让我们向查询引擎发送一个查询,并回复响应:
query = element.get('text')
query_engine = index.as_query_engine()
response = query_engine.query(query)
say(str(response))
之前我们只是注意到我们看到了一个事实(第82行),让我们把它存储在索引中:
index.insert(Document(text=message.get('text')))
结果是一个懒人机器人可以回答关于它被告知的问题:
你可以很容易地想象一个机器人,它可以倾听每个人的谈话,并能够回答关于人们几周或几个月前所说的事情的问题,从而节省每个人搜索旧消息的时间和精力。
第6步:保留我们的记忆
我们的机器人有一个严重的缺陷:索引只存储在内存中。如果我们重新启动机器人,它会忘记一切:
在6_qdrant.py中,我们引入了qdrant,这是一个开源的本地向量数据库,它将这些事实存储在磁盘上。这样,如果我们重新启动机器人,它就会记住之前说过的话。pip install qdrant-client并引入一些新的DEP:
import qdrant_client
from llama_index.vector_stores.qdrant import QdrantVectorStore
现在,我们将初始化qdrant客户端,将其附加到存储上下文,并在初始化时将该存储上下文提供给我们的索引:
client = qdrant_client.QdrantClient(
path="./qdrant_data"
)
vector_store = QdrantVectorStore(client=client, collection_name="slack_messages")
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex([],storage_context=storage_context)
这一步就这样了!你的机器人现在可以重新启动:
第7步:让最近的消息更重要
我们现在有一个相当有能力的机器人!但它有一个微妙的问题:人们可能会说出相互矛盾的事情,而且它没有办法决定谁是“正确的”,比如当我改变了对狗的名字的看法时:
在真正的Slack对话中,随着情况的发展,人们可能会从说项目“正在计划中”到“正在进行中”再到“已启动”。因此,我们需要一种方法来告诉机器人,最近的消息比以前的消息更重要。
要做到这一点,我们必须进行相当多的重构,最终结果可以在7个_Recency.py中看到。首先,我们需要一组新的DEP:
import datetime, uuid
from llama_index.schema import TextNode
from llama_index.prompts import PromptTemplate
from llama_index.postprocessor import FixedRecencyPostprocessor
from llama_index import set_global_handler
为了使最近的消息更重要,我们必须知道消息是何时发送的。为此,我们将停止向索引中插入文档,而是插入节点,我们将把时间戳作为元数据附加到这些节点上(实际上,我们的文档总是被转换为节点,因此这不会有太大变化):
dt_object = datetime.datetime.fromtimestamp(float(message.get('ts')))
formatted_time = dt_object.strftime('%Y-%m-%d %H:%M:%S')
# get the message text
text = message.get('text')
# create a node with metadata
node = TextNode(
text=text,
id_=str(uuid.uuid4()),
metadata={
"when": formatted_time
}
)
index.insert_nodes([node])
我还将消息处理中的回复逻辑分解到它自己的函数中,answer_question只是为了让事情更容易理解。我们要改变的第一件事是我们给法学硕士的提示:我们必须告诉它最近的消息是重要的。为此,我们创建一个提示模板:
template = (
"Your context is a series of chat messages. Each one is tagged with 'who:' \n"
"indicating who was speaking and 'when:' indicating when they said it, \n"
"followed by a line break and then what they said. There can be up to 20 chat messages.\n"
"The messages are sorted by recency, so the most recent one is first in the list.\n"
"The most recent messages should take precedence over older ones.\n"
"---------------------\n"
"{context_str}"
"\n---------------------\n"
"You are a helpful AI assistant who has been listening to everything everyone has been saying. \n"
"Given the most relevant chat messages above, please answer this question: {query_str}\n"
)
qa_template = PromptTemplate(template)
与LLM一起工作的有趣之处在于,你经常只是用英语描述你正在做的事情,而这就是你发送给LLM的内容。提示模板将自动从查询引擎获取context_str和。query_str但我们必须在查询引擎上设置此模板,如下所示:
query_engine.update_prompts(
{"response_synthesizer:text_qa_template": qa_template}
)
现在还有两件事我们要改变。我们将获取从向量存储中获得的结果,并按新旧程度对它们进行排序,LlamaIndex有一个内置类。它被称为FixedRecencyPostProcessor。我们告诉它保存时间戳的键(我们之前在上面的节点上定义的)以及它应该返回多少个结果:
postprocessor = FixedRecencyPostprocessor(
top_k=20,
date_key="when", # the key in the metadata to find the date
service_context=ServiceContext.from_defaults()
)
然后,我们需要创建附加了后处理器的查询引擎:
query_engine = index.as_query_engine(similarity_top_k=20, node_postprocessors=[postprocessor])
当我们这样做时,我们做了最后一件事,即 pass similarity_top_k=20,这意味着向量存储将为我们提供 20 个 Slack 消息作为上下文(默认值为 2,因为通常 Node 中的文本块要大得多)。
现在机器人知道把更多最近的陈述当作事实。
第8步:画出猫头鹰的其余部分
这个机器人现在运行得很好,添加了两个功能:
第9步:部署到渲染
到目前为止,我们一直在使用通过ngrok隧道运行的本地脚本,但即使是最专业的程序员有时也会关闭他们的笔记本电脑。让我们把这个东西放在一个真正的服务器上。
登录以进行渲染
我们将部署到Render,这是一个Python友好的托管服务,对于小型项目是免费的。注册一个帐户(我建议使用GitHub登录)。
创建新的GitHub存储库
Render部署来自GitHub存储库的内容,因此你需要创建一个新的存储库,并将现有存储库中的2个文件复制到其中:
提交它们并将它们推送到GitHub。
创建新的呈现Web服务
在Render中,创建一个新的Web服务。将其连接到你刚刚创建的GitHub上的repo:
Render可能会自动检测到这是一个Python应用程序,但你应该确保以下设置是正确的:
你还需要向下滚动并设置一些环境变量:
然后单击“部署”即可开始!
你现在有一个听消息、记忆、学习和回复的生产Slack机器人。