百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

构建一个使用 LlamaIndex、Qdrant 和 Render 进行学习的 Slack 机器人

toyiye 2024-06-21 12:19 10 浏览 0 评论

在这篇文章中,我们将引导您完成构建和部署一个 Slackbot 的过程,该 Slackbot 可以监听您的对话,从中学习,并利用这些知识回答有关 Slack 工作区正在进行的事情的问题。我们还将在 Render 上将其部署到生产环境!

开始之前,您需要以下几样东西:

  • 对 LlamaIndex 的基本理解。如果您还没有这个理解,我们文档中的入门教程将为您提供足够理解本教程所需的知识,而且只需几分钟。
  • 对 Python 有一定的了解,并已安装 Python 3.11 或更高版本。
  • 一个可以安装应用程序的 Slack 工作区(因此您需要是管理员)。
  • 在本地机器上克隆我们 Slackbot 存储库的副本。我们将在整个文章中引用该存储库中的文件。

步骤 1:创建一个 Slack 应用,并将其安装到您的工作区

这是最复杂的一步,因为 Slack 对权限非常挑剔。

您的 Slackbot 的第一个版本将只有大约 20 行代码。它的全部功能是提供一个“challenge”端点,Slack 需要验证您的应用程序是否可用。您可以在该存储库中的文件中查看此代码。让我们一起来看一下 1_flask.py。

首先,我们引入您的依赖项。如果您尚未安装这些依赖项,您需要使用 pip 或 poetry 进行安装。

from flask import Flask, request, jsonify

现在,我们将创建您的 Flask 应用程序并设置它,以便它可以在开发中运行。

flask_app = Flask(__name__)

if __name__ == "__main__":
    flask_app.run(port=3000)

在这些行之间,我们将添加我们的基本路由:如果收到包含键的 JSON 对象的 POST 请求,我们将返回该键的值。否则,我们将什么都不做。

@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

这段代码的作用是:如果收到包含 "challenge" 键的 JSON 对象的 POST 请求,它将返回该键的值。否则,它将打印出未知请求的信息。

使您的应用对 Slack 可用

要配置 Slack 应用,它需要在 Slack 能够看到的地方运行。让我们运行我们的 Slack 应用:

python 1_flask.py

然后,我们将设置它,以便全世界都能看到,使用 ngrok。您需要下载并安装 ngrok 以完成此步骤。安装完成后,运行以下命令,以便 ngrok 可以找到我们在端口 3000 上运行的应用程序:

ngrok http 3000

ngrok 将为您提供一个 HTTPS URL,例如 https://1bf6-64-38-189-168.ngrok-free.app。记下它,因为我们需要将其提供给 Slack。还请记住,如果您停止 ngrok 并再次启动它,此 URL 将更改,您需要告诉 Slack。在开发过程中,您只需要这个 URL。

在 Slack 中注册您的应用

转到 Slack API 网站,然后点击“创建新应用程序”。您将看到一个类似于此的屏幕,您将选择“从头开始”:

选择一个友好的名称和要安装到的工作区。您将看到类似于此的屏幕:

接下来,您将设置应用程序需要的权限。点击右下角的“Permissions”链接:

这将带您进入“scopes”屏幕,在这里,您需要添加此图片中所见的所有范围,即:

  • channels:read — 允许您的应用查看可用的频道
  • channels:join — 允许您的应用加入频道
  • channels:history — 允许您的应用查看频道中的先前消息
  • chat:write — 允许您的应用发送消息
  • users:read — 允许您的应用查看用户的姓名

保存这些范围后,滚动到“Install to workspace”以安装您的应用程序。

现在,您需要告诉Slack您的应用程序在哪里,以便可以从中接收消息。单击左侧导航中的“Event Subscriptions”链接,并填写它,使其看起来像这样,具体而言:

  • 将请求URL设置为ngrok先前提供的URL
  • 订阅message.channels事件

如果您的应用程序正在运行且ngrok正确进行隧道连接,则您的请求URL应该是“已验证”的。

呼!那真是够多的了。您的Slack应用程序现在已注册,Slack将向其发送消息。但要获取这些消息,您必须告诉它加入一个频道。

第2步:加入频道,并回复消息 为此,我们需要扩展我们的应用程序。您可以在2_join_and_reply.py中看到此步骤的最终结果。让我们逐步了解我们添加了什么:

import dotenv, os
dotenv.load_dotenv()

我们需要一些环境变量,因此您需要添加这些行并安装python-dotenv。您还需要在项目的根目录中创建一个名为.env的文件,其中包含三个值:

  • OPENAI_API_KEY:您的OpenAI API密钥。您现在不需要它,但最好在这里获取它。
  • SLACK_BOT_TOKEN:您可以在Slack应用程序的“OAuth and Permissions”部分找到此令牌。
  • SLACK_SIGNING_SECRET:您可以在Slack应用程序的“Basic Information”部分找到此签名密钥。

我们将使用Slack方便的Python SDK来构建我们的应用程序,因此执行pip install 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参数是一个函数,将向发送消息的频道发送一条消息。因此,每次接收到消息时,此代码将向频道发送一条消息,内容为“Yes?”。

让我们试试吧!停止运行1_flask.py,然后运行python 2_join_and_reply.py。您不需要重新启动ngrok,它将继续像以前一样将消息发送到端口3000。以下是我尝试的截图:

成功了!我们有一个非常讨厌的机器人,会回复任何人说的每一句话。我们可以做得更好!

第3步:只回复提到机器人的消息

表面上这是一个相当简单的更改,但Slack的传入消息格式有点复杂,因此我们必须添加相当多的代码。您可以在3_reply_to_mentions.py中看到最终的结果。

首先,为了知道我们的机器人被提及了,我们需要知道机器人的用户ID。在幕后,Slack不使用用户名甚至不是@-handles,而是在所有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步,我们还没有使用LlamaIndex做任何事情!但现在是时候了。在4_incremental_rag.py中,您将看到一个演示简单命令行Python脚本的地方,该脚本使用LlamaIndex存储事实并回答问题。我不会逐行为您讲解(脚本中有有用的注释),但让我们看看重要的部分。记得要安装llama-index!

首先,我们创建一个新的VectorStoreIndex,这是一个内存中的向量存储,我们将在其中存储我们的事实。一开始它是空的。

index = VectorStoreIndex([])

接下来,我们创建3个Document对象,并将它们插入我们的索引。真实的文档可以是庞大的文本块,整个PDF,甚至是图像,但这些只是一些简单的、适合Slack消息大小的事实。

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)

最后,我们从我们的索引中创建一个查询引擎并询问它一个问题:

# 运行一个查询
query_engine = index.as_query_engine()
response = query_engine.query("Who is Molly?")
print(response)

结果是“Molly是一只猫”,还有很多嘈杂的调试信息,因为我们在4_incremental_rag.py中打开了嘈杂的调试。您可以看到我们发送给LLM的提示,它从索引中检索到的上下文,以及它生成并发送回给我们的响应。

第5步:在Slack中使用LlamaIndex存储事实和回答问题

在5_rag_in_slack.py中,我们将之前的两个东西结合在一起:脚本3,我们回复查询的地方,以及脚本4,我们存储事实并回答问题的地方。再次,我们不会逐行讲解,但以下是重要的更改:

首先,如果您还没有安装llama-index,请使用pip install llama-index,并在进行初始化时引入您的依赖项:

from llama_index import VectorStoreIndex, Document

index = VectorStoreIndex([])

在以前我们只是回复“是的?”(第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')))

结果是一个能够回答关于它所听到的事情的问题的Slackbot:

了不起!您可以轻松想象一个机器人,它听取所有人的对话,并能够回答关于人们几周或几个月前说过的事情的问题,为所有人节省时间和精力,而不必搜索旧消息。

第6步:持久化我们的记忆

然而,我们的机器人有一个关键的缺陷:索引仅存储在内存中。如果我们重新启动机器人,它会忘记所有内容:

在6_qdrant.py中,我们引入了Qdrant,一个开源的本地向量数据库,它将这些事实存储在磁盘上。这样,如果我们重新启动机器人,它就记得以前说过的话。使用pip install qdrant-client并引入一些新的依赖项:

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)

这就是这一步!您的机器人现在可以在重新启动时存活,并记得我在截图中拼错了“Doug”为“Dough”,而我懒得更正的事实:

第7步:使最近的消息更重要

我们现在有一个相当强大的机器人!但它有一个微妙的问题:人们可能会说出相互矛盾的事情,而它没有办法决定谁是“正确的”,比如当我改变对狗名字的看法时:

在真实的Slack对话中,随着情况的发展,人们可能会从说项目“正在规划”到“进行中”再到“已启动”。因此,我们需要一种方法告诉机器人,最近的消息比旧的消息更重要。

为了实现这一点,我们必须进行相当多的重构,最终结果可以在7_recency.py中看到。首先,我们需要一堆新的依赖项:

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')

# 获取消息文本
text = message.get('text')
# 创建一个带有元数据的节点
node = TextNode(
    text=text,
    id_=str(uuid.uuid4()),
    metadata={
        "when": formatted_time
    }
)
index.insert_nodes([node])

我还将回复逻辑从消息处理中分离出来到它自己的函数,answer_question,只是为了使事情看起来更容易阅读一些。我们要更改的第一件事是我们给LLM的提示:我们必须告诉它最近的消息很重要。为此,我们创建了一个提示模板:

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])

在这个过程中,我们还做了最后一件事,即传递similarity_top_k=20,这意味着向量存储将为我们提供20条Slack消息作为上下文(默认值仅为2,因为通常节点中的文本块要大得多)。

大功告成!现在机器人知道将更近的声明视为事实。

第8步:画出猫头鹰的其余部分 这个机器人现在运作得相当不错,但我在构建它时玩得太开心了,我加了两个更多的功能:

  • 我附加了关于说话者是谁的元数据,这样机器人就可以回答问题,比如“Logan对项目说了什么?”
  • 与机器人互动的同事尝试在线程中提出后续问题,就像我们彼此一样。因此,我添加了一种让机器人理解它在一个线程中的方法,并将在线程中的回复视为后续问题,即使用户没有直接提到机器人:

使这两者发生的代码在8_rest_of_the_owl.py中,但我不会逐行讲解它。我们必须将这个东西部署!

第9步:部署到Render

到目前为止,我们一直在使用通过ngrok隧道运行的本地脚本,但即使最专注的程序员有时也会关闭他们的笔记本电脑。让我们将这个东西放到一个真正的服务器上。

登录到Render

我们将部署到Render,这是一个对小型项目免费的Python友好的托管服务。注册一个帐户(我建议使用GitHub登录)。

创建一个新的GitHub存储库

Render从GitHub存储库部署东西,因此您需要创建一个新的存储库,并将我们现有存储库中的2个文件复制到其中:

  1. pyproject.toml
  2. 8_rest_of_the_owl.py,我们将其简单地重命名为“app.py”。 提交这些文件并将它们推送到GitHub。

创建一个新的Render Web服务

在Render中,创建一个新的Web服务。将其连接到您刚刚创建的GitHub存储库:

Render可能会自动检测到这是一个Python应用程序,但您应该确保以下设置是正确的:

  • 名称:您选择的任何名称
  • 区域:任何区域都可以
  • 分支:main
  • 根目录:(空白,表示根目录)
  • 运行时:Python 3
  • 构建命令:poetry install
  • 启动命令:gunicorn app:flask_app(这肯定需要设置)

您还需要向下滚动并设置一些环境变量:

  • PYTHON_VERSION:3.11.6(或您使用的任何版本)
  • OPENAI_API_KEY:您的OpenAI API密钥
  • SLACK_BOT_TOKEN:您的Slack机器人令牌
  • SLACK_SIGNING_SECRET:之前从Slack获取的签名密钥

然后点击部署,就可以了!


您现在拥有一个生产中的Slack机器人,监听消息,记住,学习和回复。恭喜!

下一步做什么呢?

有许多功能可以添加到这个机器人中,大致按难度递增的顺序:

  1. 加入每个频道,而不仅仅是一个频道,显然!
  2. 添加一种告诉机器人忘记事物(删除节点)的方法
  3. 给机器人添加使用多个索引的能力,比如文档索引,或连接到您的电子邮件,或您的日历
  4. 为机器人添加“标签”,以便它可以将元数据附加到节点,并仅使用(或忽略)以某种方式标记的内容来回答问题
  5. 添加多模式功能,使机器人能够阅读图像,甚至用生成的图像回复
  6. 还有很多!

这个机器人非常有趣,玩起来也很有趣。希望您享受学习有关Slack机器人和LlamaIndex的知识,就像我写这篇教程一样有趣!


点赞关注 二师兄 talk 获取更多资讯,并在 头条 上阅读我的短篇技术文章

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码