Using Laravel to interact with OpenAI's Assistants API (with Vision)
几个月前,Open AI发布了其助理API,该API在撰写本文时仍在beta测试中。简单来说,助理是一种新的、(希望)更好的方式来利用OpenAI模型。在此之前,使用Chat Completions API时,你必须保存用户和模型之间交换的每个消息的历史,然后每次将整个历史发送回API,因为Chat Completions API不是有状态的。
于是有了助理。助理是有状态的,会为你保存历史,因此你可以轻松地只发送最后一条用户消息,并接收回一个答案。除函数调用外,还包括像代码解释器和知识检索等酷炫工具。
<h2 class="subheading">入门指南</h2>现在,要使用Laravel与OpenAI API交互,我们将使用由Nuno Maduro和Sandro Gehri开发的已经受欢迎的包:<a title="OpenAI PHP客户端" href="https://github.com/openai-php/client" target="_blank" rel="noopener">https://github.com/openai-php/client</a>以及该包的Laravel特定包装器:<a title="OpenAI Laravel客户端" href="https://github.com/openai-php/laravel" target="_blank" rel="noopener">https://github.com/openai-php/laravel</a>。
首先,简单地引入Laravel包装器包
composer require openai-php/laravel
这将安装所需的依赖项,包括OpenAI PHP客户端包。之后,执行安装命令
php artisan openai:install
接下来,你应在.env
文件中添加你的OpenAI API密钥
OPENAI_API_KEY=sk-...
OPENAI_ORGANIZATION=org-...
在你的API密钥部分找到或创建你的API密钥
<a title="Api keys" href="https://geoligard.com/storage/posts/December2023/api-keys.png" target="_blank"><img title="api keys" src="https://geoligard.com/storage/posts/December2023/api-keys.png" alt="api keys" width="100%" height="auto"></a>
现在我们已经准备好使用这个包与API进行交互。但在那之前,让我们创建一个助手。请注意,助手也可以通过API创建,但为了简化速度,我们将使用OpenAI的游乐场来创建助手。请转到:<a title="OpenAI playground" href="https://platform.openai.com/playground" target="_blank" rel="noopener">https://platform.openai.com/playground</a> 并创建一个助手。您可以给他任何名字,输入任何您可能有的说明,选择一个模型,并可以启用或不启用额外的工具。当您保存更改时,请确保复制助手的ID,我们稍后需要它。
<a title="Create Assistant" href="https://geoligard.com/storage/posts/December2023/create-assistant.png" target="_blank"><img title="Create Assistant" src="https://geoligard.com/storage/posts/December2023/create-assistant.png" alt="Create Assistant" width="100%" height="auto"></a>
好的,现在,设想我们正在开发一个应用程序,其中我们在后端负责。用户将在前端输入一条聊天消息,通常是我们的助手需要回答的问题,前端将使用我们的应用程序的内部API,并将用户输入的消息发送给我们。在后台,我们将向助手发出请求并等待他的响应。在收到响应后,我们将将其返回到前端显示给用户。本文将专注于过程的客户端部分。我提到,理论上某些前端将调用我们的后端API,但我们可以像使用Postman一样模拟这个过程,我们将在以后实际做。
让我们只创建一条路由和一个 AssistantController.php
来执行我们的逻辑。
php artisan make:controller AssistantController
// api.php
Route::post('/assistant', [AssistantController::class, 'generateAssistantsResponse']);
<h2 class="subheading">创建和运行线程</h2>在我们创建主函数 generateAssistantsResponse
之前,让我们创建几个辅助函数。按照助手的流程,我们需要使用助手的ID创建一个 thread
(这是一个在用户和助手之间交换信息的类型容器),然后我们需要提交一个实际的用户的 message
,并触发一个 run
。当运行成功完成后,我们应得到响应。
// AssistantController.php
use OpenAI\Laravel\Facades\OpenAI;
class AssistantController extends Controller
{
private function submitMessage($assistantId, $threadId, $userMessage)
{
$message = OpenAI::threads()->messages()->create($threadId, [
'role' => 'user',
'content' => $userMessage,
]);
$run = OpenAI::threads()->runs()->create(
threadId: $threadId,
parameters: [
'assistant_id' => $assistantId,
],
);
return [
$message,
$run
];
}
private function createThreadAndRun($assistantId, $userMessage)
{
$thread = OpenAI::threads()->create([]);
[$message, $run] = $this->submitMessage($assistantId, $thread->id, $userMessage);
return [
$thread,
$message,
$run
];
}
private function waitOnRun($run, $threadId)
{
while ($run->status == "queued" || $run->status == "in_progress")
{
$run = OpenAI::threads()->runs()->retrieve(
threadId: $threadId,
runId: $run->id,
);
sleep(1);
}
return $run;
}
private function getMessages($threadId, $order = 'asc', $messageId = null)
{
$params = [
'order' => $order,
'limit' => 10
];
if($messageId) {
$params['after'] = $messageId;
}
return OpenAI::threads()->messages()->list($threadId, $params);
}
}
简而言之,我们将使用 createThreadAndRun
函数创建一个线程、向线程提交消息并创建一个运行。我们使用的包中有一个等价的 createAndRun
函数,但这个我们创建的函数可能有助于您更好地理解正在发生的事情。然后我们将使用 waitOnRun
函数等待运行完成,最后我们可以使用 getMessages
函数从助手那里获取响应。
现在,我们可以创建主函数 generateAssistantsResponse
,该函数定义在我们之前创建的 assistant
路由中。
public function generateAssistantsResponse(Request $request)
{
// hard coded assistant id
$assistantId = 'asst_someId';
$userMessage = $request->message;
// create new thread and run it
[$thread, $message, $run] = $this->createThreadAndRun($assistantId, $userMessage);
// wait for the run to finish
$run = $this->waitOnRun($run, $thread->id);
if($run->status == 'completed') {
// get the latest messages after user's message
$messages = $this->getMessages($run->threadId, 'asc', $message->id);
$messagesData = $messages->data;
if (!empty($messagesData)) {
$messagesCount = count($messagesData);
$assistantResponseMessage = '';
// check if assistant sent more than 1 message
if ($messagesCount > 1) {
foreach ($messagesData as $message) {
// concatenate multiple messages
$assistantResponseMessage .= $message->content[0]->text->value . "\n\n";
}
// remove the last new line
$assistantResponseMessage = rtrim($assistantResponseMessage);
} else {
// take the first message
$assistantResponseMessage = $messagesData[0]->content[0]->text->value;
}
return response()->json([
"assistant_response" => $assistantResponseMessage,
]);
} else {
\Log::error('Something went wrong; assistant didn\'t respond');
}
} else {
\Log::error('Something went wrong; assistant run wasn\'t completed successfully');
}
}
所以,我们从 request
中获取用户的 message
,并使用这些数据以及助手的ID来生成一个新的线程、提交消息并开始运行。我们从 createThreadAndRun
函数中获取所有必要的数据,这我们可以在过程中进一步使用。运行开始后,我们将定期检查运行是否完成。
几秒钟后,运行的状态将设置为 completed
。一旦发生,我们将获取用户发送的消息之后的最新消息。由于(根据OpenAI文档)助手有时可能会发送多于一条消息(尚未发生在我身上,但以防万一),我们将检查发送的消息数量并将它们连接成一个单条响应,以简化处理。否则,如果只有一条消息,我们就获取第一条消息。最后,助手的消息以JSON响应的形式返回。
注意:在我们开始使用视觉API之前,请记住,虽然您可以使用聊天和助手API与GPT-3模型一起使用,但是要使用视觉,您将需要访问GPT-4模型,并且您需要花费1美元才能解锁对GPT-4模型的访问权限:<a title="访问GPT-4" href="https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4" target="_blank" rel="noopener">https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4</a>。
好的,我们现在可以讨论助手函数
以及我们如何利用它们将不同的数据(第三方或我们自己的应用程序)传递给助手。使用视觉API,例如,您可以允许您的用户上传一张图片,然后GPT-4
模型将能够接收这张图片并回答有关它的不同问题。但现在的问题是视觉API尚不支持助手API。如果您尝试上传图片并发送给助手,它将无法描述图片的内容。但是有一个解决方案。
进入函数调用。使用助手函数,我们能够指示助手调用一个特定函数,该函数将转而调用另一个API - 聊天完成API:<a title="聊天API" href="https://platform.openai.com/docs/api-reference/chat/create" target="_blank" rel="noopener">https://platform.openai.com/docs/api-reference/chat/create</a>,它提供视觉API。稍后我们将从聊天API中获取这些数据,以便我们的助手可以相应地做出回应。
回到我们的AssistantController.php
,让我们从另一个辅助函数开始。我们将创建一个processRunFunctions
方法,该方法检查我们的助手run
是否需要执行任何操作,在本例中检查是否需要调用任何函数。如果助手决定调用describe_image
函数,我们将启动聊天API并要求它根据消息和用户已经提供的内容和图片提供图片描述。然后我们将视觉相关数据提交回助手并等待该操作的完成。请注意,我们正在增加max_tokens
的默认值,以便响应不会中断。
private function processRunFunctions($run)
{
// check if the run requires any action
while ($run->status == 'requires_action' && $run->requiredAction->type == 'submit_tool_outputs')
{
// Extract tool calls
// multiple calls possible
$toolCalls = $run->requiredAction->submitToolOutputs->toolCalls;
$toolOutputs = [];
foreach ($toolCalls as $toolCall) {
$name = $toolCall->function->name;
$arguments = json_decode($toolCall->function->arguments);
if ($name == 'describe_image') {
$visionResponse = OpenAI::chat()->create([
'model' => 'gpt-4-vision-preview',
'messages' => [
[
'role' => 'user',
'content' => [
[
"type" => "text",
"text" => $arguments?->user_message
],
[
"type" => "image_url",
"image_url" => [
"url" => $arguments?->image,
],
],
]
],
],
'max_tokens' => 2048
]);
// you get 1 choice by default
$toolOutputs[] = [
'tool_call_id' => $toolCall->id,
'output' => $visionResponse?->choices[0]?->message?->content
];
}
}
$run = OpenAI::threads()->runs()->submitToolOutputs(
threadId: $run->threadId,
runId: $run->id,
parameters: [
'tool_outputs' => $toolOutputs,
]
);
$run = $this->waitOnRun($run, $run->threadId);
}
return $run;
}
现在我们可以修改我们的主方法generateAssistantsResponse
以处理用户上传的图片,并处理助手可能调用的任何函数。我不会详细介绍如何验证和保存用户提供的图片,但请记住,图片可以作为URL或base64发送给视觉API,但URL是首选的。
public function generateAssistantsResponse(Request $request)
{
$assistantId = 'asst_someId';
$userMessage = $request->message;
// check if user uploaded a file
if($request->file('image')) {
// validate image
// store the image and get the url - url hard coded for this example
$imageURL = 'https://images.unsplash.com/photo-1575936123452-b67c3203c357';
// concatenate the URL to the message to simplify things
$userMessage = $userMessage . ' ' . $imageURL;
}
[$thread, $message, $run] = $this->createThreadAndRun($assistantId, $userMessage);
$run = $this->waitOnRun($run, $thread->id);
// check if any assistant functions should be processed
$run = $this->processRunFunctions($run);
if($run->status == 'completed') {
$messages = $this->getMessages($run->threadId, 'asc', $message->id);
接下来要做的就是添加一个函数定义。您可以通过API添加函数定义,但由于示例的简洁性,我们将通过Playground添加函数定义。以下是我们要使用的定义
{
"name": "describe_image",
"description": "Describe an image user uploaded, understand the art style, and use it as an inspiration for constructing an appropriate prompt that will be later used to generate a skybox.",
"parameters": {
"type": "object",
"properties": {
"user_message": {
"type": "string",
"description": "Message the user sent along with the image, without the URL itself"
},
"image": {
"type": "string",
"description": "URL of the image user provided"
}
},
"required": [
"user_message",
"image"
]
}
}
以下是在Playground中的样子。
<a title="添加函数" href="https://geoligard.com/storage/posts/December2023/add-function.png" target="_blank"><img title="添加函数" src="https://geoligard.com/storage/posts/December2023/add-function.png" alt="添加函数" width="100%" height="auto"></a>
<a title="描述图片函数" href="https://geoligard.com/storage/posts/December2023/describe-image-function.png" target="_blank"><img title="描述图片函数" src="https://geoligard.com/storage/posts/December2023/describe-image-function.png" alt="描述图片函数" width="100%" height="auto"></a>
下面是在Postman中整个API请求/响应流的样子。
<a title="Postman示例" href="https://geoligard.com/storage/posts/December2023/postman-example.png" target="_blank"><img title="Postman示例" src="https://geoligard.com/storage/posts/December2023/postman-example.png" alt="Postman示例" width="100%" height="auto"></a>
如果您想从AssistantController.php
复制全部代码,请前往 <a title="Gist示例" href="https://gist.github.com/goran-popovic/b104e2cc3c16db9312cd1e90d4e5e256" target="_blank" rel="noopener">Gist</a>。
总结来说,请记住这是一个基本的示例,仅用于展示一些可能性,并且代码可以通过很多方式进行改进。例如,你可能不应该硬编码你的助手ID,而是可以将其保存在你的数据库模型中。目前,唯一检查运行是否已完成的方法是定期检查,这远远不够理想,但在文档中提到这将在未来改变:[OpenAI 助理 API 限制](https://platform.openai.com/docs/assistants/how-it-works/limitations)。你不必等待单个请求完成运行,可以将代码的部分调度到 Laravel 任务中并异步执行。任务完成后,可以通过 Websockets 广播一个事件,并将助手响应发送到前端的通知,或者你可以使用 Webhooks 将结果发送给其他用户。
此外,就像我之前提到的,助手 API 目前处于测试版,并且在未来几周或几个月内可能会发生变化。
这篇文章到此结束,希望它为你使用助手 API 提供了一个良好的起点。
祝您快乐构建!
driesvints 赞了这个文章