作者:Lukasz Kowejsza
人工智能和商业管理领域的游戏规则改变者是人工智能代理与广泛使用的通信工具的集成。想象一下拥有一个熟悉的聊天界面,其中包含实时数据请求、更新和任务自动化,所有这些都可以通过 WhatsApp 与您的企业管理层或个人助理 AI 直接交互来实现。
在创建人工智能驱动的业务经理系列的第三部分中,我将引导您完成将人工智能代理连接到 WhatsApp 的步骤,以增强其功能和覆盖范围。要实现的目标是人工智能助手能够与所有相关的数据库表进行交互,甚至可以自行创建表和所有必要的工具。作为主要展示,我重点关注业务用例,例如跟踪费用、发票等。但是,您可以轻松地采用相同的逻辑来创建,例如跟踪您的任务、项目和想法的个人助理。
这是我的系列的第三部分。在我们开始之前,对于大家等待的时间,我为这么长时间的延误表示歉意。在过去的几个月里,我一直忙于开始一份新的人工智能软件工程工作并适应新的工作与生活平衡。到目前为止,我已经准备了本文的一些后续部分,我们将探讨代理工作流程中的重大变化,以及具有几个附加功能的更复杂的工作流程。前两篇文章中使用的一些解决方法对于当时可靠的工具调用是必要的,但由于 GPT-4o 和 GPT-4o-mini 等性能更好的模型而不再需要。如果您是工具调用和代理工作流程开发的新手,我仍然建议您从前两部分开始。我发现,在依赖 LangChain 等框架或更具体地说,LangGraph 来实现深度可定制的代理工作流程(我将在不久的将来介绍)之前,了解如何从头开始构建某些东西很有用。
现在,我们必须退后一步,首先关注基础设施。我认为在大多数项目中,特别是在人工智能软件项目中,在迷失功能蔓延之前首先创建一个可用的端到端产品是一个很好的做法。我经常发现自己在最初的设计选择上考虑过多,并在脑海中开发出过于复杂的产品。为了克服这个问题,在开发后的几天内专注于构建一个可用的端到端产品确实有助于建立一个清晰的基础。之后,您将知道要优先考虑哪些功能,并且能够收集初步反馈。这启动了增量开发过程,这始终是我致力于项目时的目标。
在本系列的前几部分中,我们为人工智能驱动的业务经理奠定了基础:
像往常一样,让我们首先定义本文的范围:
由于我们正在转向可部署的服务器,因此我们还需要调整我们的项目架构。我们本质上是在实现一个 FastAPI 服务器,因此,我首选的存储库结构是领域驱动设计 (DDD),或者更倾向于 DDD。(你可以检查Repo结构这里)首先,您需要熟悉Meta提供的Cloud API。您可以使用 Twilio 等 SaaS 产品获得相同的结果,这些产品提供了更加用户友好的集成。
然而,由于最近的数据泄露以及出于成本效率的原因,我更喜欢使用 Meta 提供的 root API。
在您的 Meta 开发者帐户中创建应用程序后,系统会要求您向其中添加产品。在这里您必须选择 WhatsApp 并按照设置过程进行操作。如果您还没有这样做,请在此处创建一个元企业帐户。完成后,您将拥有一个测试 WhatsApp Business 帐户和一个测试电话号码。
WhatsApp > API 设置
,您现在可以通过填写以下内容来发送测试消息从
字段包含您的测试电话号码和到
字段包含您的收件人号码(您自己的电话号码)。WHATSAPP_API_TOKEN
,稍后我们将在第 6 步中需要它。我们已经按照要求成功设置了 Cloud API。在下一步中,我们将创建一个 Webhook,以实现与 AI Assistant 应用程序的通信。
为了实现这一点,我们需要在后端应用程序中创建并提供一个端点。这意味着我们的 Python 后端必须可以通过 URL 访问。此 URL 将充当 AI Assistant 可以调用来发送和接收数据的 Webhook 端点。
为了被 Webhook 接受,我们的根端点必须验证添加 URL 时将由 Webhook 发送的特定 GET 请求。Webhook 将发送三个查询参数:
集线器模式
,枢纽挑战
,hub.验证.token。
验证令牌是在 Cloud API 中创建 Webhook 时定义的。
您的后端应该验证该令牌是否与您定义的令牌匹配并返回枢纽挑战
对象作为响应。确保使用安装 FastAPI 和 Uvicornpip 安装 fastapi uvicorn
第一的。
创建一个名为主要.py
包含以下内容:
从 fastapi 导入 FastAPI、查询、HTTPExceptionVERIFICATION_TOKEN =“abcdefg12345”
应用程序 = FastAPI()
@app.get("/")
def verify_whatsapp(
hub_mode: str = Query("订阅",description="webhook的模式",alias="hub.mode"),
hub_challenge: int = Query(..., description="验证 webhook 的挑战", alias="hub.challenge"),
hub_verify_token: str = Query(..., description="验证令牌", alias="hub.verify_token"),
):
如果 hub_mode ==“订阅”且 hub_verify_token == VERIFICATION_TOKEN:
返回 hub_challenge
引发HTTPException(status_code = 403,detail =“无效的验证令牌”)
@app.get(“/健康”)
def health():
返回{“状态”:“健康”}
@app.get("/准备情况")
def 准备就绪():
返回 {"status": "ready"}
在第三行中,您可以定义一个VERIFICATION_TOKEN
Webhook 稍后会使用它来验证后端是否在您的控制之下。在这种情况下,我们将其定义为“abcdefg12345”
,但您可以定义自己的自定义令牌。
我将继续纠正其余部分,并很快包括下一部分!
使用 Uvicorn 运行应用程序:
uvicorn 主:应用程序--重新加载
您的后端现在在本地运行http://本地主机:8000
和/或http://127.0.0.1:8000。
我们现在为以下端点提供服务:
验证 WhatsApp 网络钩子:
健康终点:
准备就绪终点:
您可以使用运行状况端点来检查应用程序是否正在运行。
打开http://127.0.0.1:8000/health
在您的浏览器中,您应该看到:{“状态”:“健康”}
由于我们的服务器在本地运行,因此 WhatsApp Webhook 无法调用端点进行验证。我们需要的是一个可供 webhook 使用的公共 URL。有两种选择:将应用程序部署到云服务器或创建代理服务器隧道。由于我们仍处于开发过程中,因此我们将使用第二个选项。
$您的AUTHENTICATION_TOKEN
使用您的 ngrok 身份验证令牌,该令牌可以在 ngrok 仪表板的“您的 Authtoken”下找到。> ngrok 配置 add-authtoken $YOUR-AUTHENTICATION_TOKEN
> ngrok http http://localhost:8000转发 https://<random-string>.ngrok.io -> http://localhost:8000
现在可以通过 ngrok 提供的公共 URL 访问您的本地服务器。你应该看到这样的东西:
转发 https://<random-string>.ngrok.io -> http://localhost:8000
使用 ngrok 提供的 HTTPS URL 进行 webhook 配置。
现在让我们返回 Meta 的 Cloud API 来实现所需的 Webhook。
VERIFICATION_TOKEN
定义于主要.py
进入验证令牌场地。消息
切换下订阅字段。就是这样!您现在应该能够在 Python 后端服务器中接收 WhatsApp 消息。
Webhook 是 HTTP 回调,使程序能够在发生某些事件(例如新消息或状态更改)时接收实时更新。Webhooks 通过将包含事件数据的 HTTP 请求传递到预先配置的 URL(在我们的示例中为 ngrok 代理服务器 URL),使系统集成和自动化成为可能。
要了解 Meta cosmos 中 webhooks 背后的逻辑和定价,了解有关对话的一些基本原则会很有帮助。
WhatsApp API 上的“对话”在以下情况下开始:
1. 用户发送消息:这会打开一个 24 小时窗口,在此期间您可以回复消息,包括文本、图像或其他媒体无需额外费用。
2. 企业主动联系:如果最近没有收到用户消息(没有开放24小时窗口),您的AI助手必须使用预先批准的模板消息开始对话。您可以添加自定义模板,但它们需要得到 Meta 的批准。
只要用户继续回复,24 小时窗口就会随着每条新消息而重置。这使得无需额外成本即可进行持续交互。一次对话的费用约为 0.00-0.08 美元。具体定价取决于您的对话类型营销、实用程序、服务和您的位置。仅供参考:服务对话现在似乎是免费的。您可以在这里找到具体的定价:WhatsApp 定价
现在我们可以在后台接收消息了。由于我们已经订阅了消息对象,因此每次将消息发送到您的测试号码时,Webhook 都会向您在上一步中定义的回调 URL 创建一个 POST 请求。接下来我们需要做的是在 FastAPI 应用程序中构建 POST 请求的端点。
我们首先定义一下需求:
我们将从网络钩子接收有效负载。您可以在 Meta 的文档中找到示例有效负载:有效负载示例
我更喜欢使用 Pydantic 编写代码,为我的 Python 代码添加类型安全性。此外,类型注释和 Pydantic 是 FastAPI 应用程序的最佳匹配。因此,我们首先定义端点中使用的模型:
# 应用程序/schema.py
从输入导入列表,可选
从 pydantic 导入 BaseModel、Field类简介(BaseModel):
名称:str
联系类(基础模型):
简介: 简介
wa_id:str
类文本(基础模型):
身体:str
图像类(基础模型):
mime_type:str
sha256:str
编号:str
音频类(基础模型):
mime_type:str
sha256:str
编号:str
声音:布尔
消息类(基础模型):
from_: str = Field(..., 别名=“from”)
编号:str
时间戳:str
文字:文字|无=无
图像:图像|无=无
音频:音频|无=无
类型:str
类元数据(BaseModel):
显示电话号码:str
电话号码_id:str
类值(基础模型):
消息传递产品:str
元数据:元数据
联系人:列表[联系人]|无=无
消息:列表[消息]|无=无
类更改(基础模型):
价值:价值
字段:str
状态:列表[dict] |无=无
类入口(BaseModel):
编号:str
更改:列表[更改]
类有效负载(基础模型):
对象:str
条目:列表[条目]
用户类(基础模型):
id:整数
名字:str
姓氏:str
电话: STR
角色:str
类 UserMessage(BaseModel):
用户: 用户
消息:str |无=无
图像:图像|无=无
音频:音频|无=无
接下来,我们将创建一些辅助函数来在 FastAPI 中使用依赖注入:
# 应用程序/main.py从 app.domain 导入 message_service
def parse_message(payload: Payload) -> 消息 |没有任何:
如果不是payload.entry[0].changes[0].value.messages:
返回无
返回payload.entry[0].changes[0].value.messages[0]
def get_current_user(message: Annotated[Message, Depends(parse_message)]) -> 用户 |没有任何:
如果没有留言:
返回无
返回 message_service.authenticate_user_by_phone_number(message.from_)
def parse_audio_file(message: Annotated[Message, Depends(parse_message)]) -> 音频 |没有任何:
如果消息和消息类型==“音频”:
返回消息.音频
返回无
def parse_image_file(message: Annotated[Message, Depends(parse_message)]) -> 图像 |没有任何:
如果消息和消息类型==“图像”:
返回消息.image
返回无
def message_extractor(
消息:带注释的[消息,取决于(parse_message)],
音频:带注释的[音频,取决于(parse_audio_file)],
):
如果有音频:
返回message_service.transcribe_audio(音频)
如果消息和消息.text:
返回消息.text.body
返回无
解析消息
函数从传入的有效负载中提取第一条消息(如果存在)。该函数返回没有任何
如果没有找到消息,则只处理有效的消息。获取当前用户
函数使用解析消息
依赖注入来提取消息,然后根据与消息关联的电话号码对用户进行身份验证。在这里,我们确保只有经过身份验证的用户才可以发送消息。消息提取器
函数尝试从消息中提取文本或将音频转录为文本。这确保了无论消息类型如何,都可以处理内容。这里我们有一个来自域层的导入。整个脚本消息服务
是我们放置此实现的所有特定于域的代码的地方,例如通过电话号码验证用户
和转录音频
。
# 应用程序/main.py
导入线程
从typing_extensions导入注释
从 fastapi 导入 APIRouter、查询、HTTPException、取决于
从 app.domain 导入 message_service
从 app.schema 导入有效负载、消息、音频、图像、用户# ...其余代码...
@app.post("/", status_code=200)
def receive_whatsapp(
用户:带注释的[用户,取决于(get_current_user)],
user_message: 带注释的[str, Depends(message_extractor)],
图像:带注释的[图像,取决于(parse_image_file)],
):
如果不是 user 且不是 user_message 且不是 image:
返回{“状态”:“确定”}
如果不是用户:
引发 HTTPException(status_code=401,详细信息=“未经授权”)
如果图像:
return print("图像已收到")
如果用户消息:
线程 = 线程. 线程(
目标=message_service.respond_and_send_message,
args =(用户消息,用户))
线程.守护进程 = True
线程.start()
返回{“状态”:“确定”}
POST 端点实施:
HTTP异常
带有 401 状态代码。message_service.respond_and_send_message
根据 LLM-Agent 工作流程调用函数来处理消息。Webhook 使用线程池的说明:WhatsApp 将重新发送 Webhook,直到收到 200 响应,因此使用线程池来确保消息处理不会阻止 Webhook 响应。
在我们之前定义端点的表示层中,我们使用一些消息服务
接下来需要定义的函数。具体来说,我们需要一个实现来处理和转录音频有效负载、对用户进行身份验证,最后调用我们的代理并发回响应。我们将把所有这些功能放在里面域/message_service.py。
在生产环境中,随着应用程序的增长,我建议将它们进一步拆分为,例如,转录服务.py
,消息服务.py
, 和身份验证服务.py。
在本节的多个函数中,我们将向 Meta API 发出请求
“https://graph.facebook.com/...”。
在所有这些请求中,我们需要包含授权标头WHATSAPP_API_KEY
,我们创建于步骤1.3,作为不记名令牌。我通常将 API 密钥和令牌存储在.env
文件并使用 Python 访问它们多滕夫
图书馆。我们还将 OpenAI 客户端与您的OPENAI_API_KEY
,也可以存储在.env
文件。
但为了简单起见,我们将它们放置在顶部并初始化消息服务.py
脚本如下:
导入操作系统
导入 json
导入请求
输入 import BinaryIOWHATSAPP_API_KEY = "您的访问令牌"
llm = OpenAI(api_key="YOUR_OPENAI_API_KEY")
将“YOUR_ACCESS_TOKEN”替换为您在步骤 1.3 中创建的实际访问令牌。
处理来自 WhatsApp Webhook 的语音记录并不像看起来那么简单。首先,重要的是要知道传入的 webhook 仅告诉我们数据类型和对象 ID。所以它不包含二进制音频文件。我们首先必须使用 Meta 的 Graph API 下载音频文件。要下载收到的音频,我们需要发出两个连续的请求。第一个是 GET 请求,其中包含对象 ID
来获取下载地址。此下载 URL 是我们第二个 GET 请求的目标。
def download_file_from_facebook(file_id: str, file_type: str, mime_type: str) -> str |没有任何:
# 第一个 GET 请求来检索下载 URL
url = f"https://graph.facebook.com/v19.0/{file_id}"
headers = {"授权": f"承载 {WHATSAPP_API_KEY}"}
响应 = requests.get(url, headers=headers)
如果响应.status_code == 200:
download_url = response.json().get('url')
# 第二个 GET 请求下载文件
响应 = requests.get(download_url, headers=headers)
如果响应.status_code == 200:
# 从 mime_type 中提取文件扩展名
file_extension = mime_type.split('/')[-1].split(';')[0]
# 创建带扩展名的file_path
文件路径 = f"{file_id}.{file_extension}"
以 open(file_path, 'wb') 作为文件:
文件.write(响应.内容)
如果 file_type == “图像” 或 file_type == “音频”:
返回文件路径
raise ValueError(f"下载文件失败。状态代码:{response.status_code}")
raise ValueError(f"无法检索下载 URL。状态代码:{response.status_code}")
在这里,我们基本上获取下载 URL 并使用对象 ID 和文件扩展名作为文件下载文件到文件系统文件路径
。如果出现问题,我们会提出值错误
表明错误发生的位置。
接下来,我们简单地定义一个函数,该函数获取音频二进制文件并使用 Whisper 对其进行转录:
def transcribe_audio_file(audio_file: BinaryIO) -> str:
如果不是音频文件:
返回“未提供音频文件”
尝试:
转录 = llm.audio.transcriptions.create(
文件=音频文件,
模型=“耳语-1”,
响应格式=“文本”
)
返回转录
除了异常 e:
从 e 引发 ValueError("转录音频时出错")
最后,让我们将下载和转录功能结合在一起:
def transcribe_audio(音频: 音频) -> str:
file_path = download_file_from_facebook(audio.id, "音频", audio.mime_type)
使用 open(file_path, 'rb') 作为audio_binary:
转录 = transcribe_audio_file(audio_binary)
尝试:
os.remove(文件路径)
除了异常 e:
print(f"删除文件失败:{e}")
返回转录
在使用 Meta 提供的测试号码时,我们必须预先定义聊天机器人可以向哪些号码发送消息。我不太确定,也没有测试是否有任何号码可以向我们的聊天机器人发送消息。但无论如何,一旦我们切换到自定义号码,我们就不希望任何人能够执行我们的代理聊天机器人。所以我们需要一种方法来验证用户的身份。我们有多种选择来做到这一点。首先,我们要考虑在哪里存储用户信息。例如,我们可以使用 PostgreSQL 等数据库或 Firestore 等非关系数据库。我们可以在 JSON 文件或一个文件系统中预定义我们的用户.env
文件。在本教程中,我将采用最简单的方法,并将用户硬编码在我们的身份验证函数的列表中。
列表条目的结构为用户
模型定义为步骤5.1。因此,用户由 ID、名字、姓氏和电话号码组成。我们尚未在代理工作流程中实施角色系统。但在大多数具有不同用户的用例中,例如在小型企业助理的示例中,不同的用户将具有不同的权限和访问范围。现在,我们只是通过“默认”
作为占位符角色。
defauthenticate_user_by_phone_number(电话号码:str)->用户|没有任何:
允许的用户 = [
{“id”:1,“电话”:“+1234567890”,“名字”:“约翰”,“姓氏”:“多伊”,“角色”:“默认”},
{“id”:2,“电话”:“+0987654321”,“名字”:“简”,“姓氏”:“史密斯”,“角色”:“默认”}
]
对于 allowed_users 中的用户:
如果用户[“电话”] == 电话号码:
返回用户(**用户)
返回无
因此,只需验证该电话号码是否在我们的列表中即可允许的用户
并返回用户(如果是)。否则,我们返回没有任何
。如果您查看我们的端点步骤5.3,如果用户是没有任何
以防止进一步处理未经授权的用户消息。
现在,在我们实际调用代理之前,我们的最后一个辅助函数是发送whatsapp消息
。由于一些元特定的 WhatsApp API 逻辑,我在此函数中包含了两种模式。
基本上,您不允许将自定义消息作为对话开始者发送给用户。这意味着如果用户开始对话并首先向聊天机器人写入消息,您可以通过单独的短信进行响应。否则,如果您希望聊天机器人发起对话,您只能使用批准的模板,例如“Hello World”模板。
另外值得一提的是,当我们谈论元逻辑时,对话启动后会打开一个 24 小时的对话窗口,您可以在其中向该用户发送消息。这个对话窗口也是收费的,而不是单独的消息。根据对话类型(例如营销、支持等),它会变得更加复杂。
你也可以自己定义一个模板并让Meta批准。目前我还没有这样做,因此为了测试我们是否可以从后端向用户发送消息,我使用了“Hello World”模板。如果您添加了一些自定义批准的模板,您还可以使用此功能将它们发送给用户。
回到代码。要发送消息,我们发出 POST 请求并定义包含文本正文或模板的有效负载:
def send_whatsapp_message(to, message, template=True):
url = f“https://graph.facebook.com/v18.0/289534840903017/messages”
标题= {
“授权”:f“承载” + WHATSAPP_API_KEY,
“内容类型”:“应用程序/json”
}
如果不是模板:
数据 = {
“messaging_product”:“whatsapp”,
“preview_url”:错误,
"recipient_type": "个人",
“到”:到,
“类型”:“文本”,
“文本”: {
“正文”:消息
}}
别的:
数据 = {
“messaging_product”:“whatsapp”,
“到”:到,
“类型”:“模板”,
“模板”: {
“名称”:“你好世界”,
“语言”: {
“代码”:“en_US”
}
}}
响应 = requests.post(url, headers=headers, data=json.dumps(data))
返回response.json()最后,我们可以集成之前示例中的代理。
在此阶段,您还可以集成您的自定义代理,Langchain
代理执行者, 郎图
代理工作流程, ETC。
因此,我们将在每个传入消息上调用的主要函数是
响应并发送消息,这需要
用户消息字符串并将其作为输入对象传递给我们的代理工作流程。
# 应用程序/域/message_service.py
导入 json
导入请求
从 app.domain.agents.routing_agent 导入 RoutingAgent
从 app.schema 导入用户
def respond_and_send_message(user_message: str, user: 用户):代理 = 路由代理()
响应 = agent.run(user_message, user.id)
send_whatsapp_message(user.phone, 响应, template=False)
调用我们的代理后,我们会收到一条响应消息,我们希望使用 send_whatsapp_message 函数将其发送回用户。
现在您应该能够向测试号码发送消息并得到代理执行者的答复。
评论:在使用WhatsApp测试号时,您必须注册允许在Meta API应用程序中将消息发送到bot的电话号码。通过遵循本指南,您迈出了一个重要的一步,可以创建一个强大的LLM供电聊天机器人,该聊天机器人与WhatsApp无缝搭配。
这不是实时建立自动化的业务通信;这是为了在道路上为更高级的AI驱动工作流奠定基础。
在下一部分中,我保证将更早发布,我将将实施方式移至langgraph。我将在代理商中添加更多功能,例如在其上创建数据库表 +工具。这将使代理更加灵活。我也向反馈和想法开放,要添加什么功能!
将WhatsApp的覆盖范围和可用性与LLM相结合是企业和个人用例的巨大胜利。无论您是针对个人助理还是成熟的业务工具,本指南都为您提供了到达那里的道路。保持修补,改进和推动界限 - 这仅仅是您可以构建的开始。
快乐编码!ð