Azure OpenAI Service 14 - Azure OpenAI Assistants API 方法完整介紹和實做

繼上一篇基本介紹 Assistants API 和基本實做之後,本文針對 Assistants API 做更詳細的用法介紹和實做。

實做

底下實做範例皆為 C# 並使用 Azure.AI.OpenAI.Assistants 套件。因為 OpenAI 和 Azure OpenAI 基本上實做跟 API 方法都差不多,所以程式基本上可以通用的,如果有少數差異我會另外說明目前有的差一點,底下介紹的方法基本上都會有同步跟非同步的方法,就不兩種都列出來了,原則上範例都使用非同步的方法來實做。

AssistantsClient

因為 Azure OpenAI 會根據每個人部署的名稱而有屬於自己的節點所已建立 Client 時候會要多了結點的網址,這部分的建立可以參考底下程式,就可以根據需求切換了。

AssistantsClient client = isAzureOpenAI
    ? new AssistantsClient(new Uri(azureResourceUrl), new AzureKeyCredential(azureApiKey))
    : new AssistantsClient(nonAzureApiKey);

Assistant

建立助理

底下程式是最基本的助理建立方法,每個參數介紹如下面的表格,後面會再詳細介紹工具的用法,最後回傳的結果可以從 Value 取得助理的物件。

Response<Assistant> assistantResponse = await client.CreateAssistantAsync(
    new AssistantCreationOptions("{Model Name}")
    {
        Name = "{Assistant Name}",
        Instructions = "{Assistant Instructions}",
        Description = "{Assistant Description}",
		Metadata = new Dictionary<string, string>() {
			// { "Demo", "Sample"},
			// { "Test", "Sample"},
		}
        Tools = {
			//new CodeInterpreterToolDefinition(), // 程式碼解譯器
			//new RetrievalToolDefinition(),	// 知識檢索
			//new FunctionToolDefinition("{FunctionName}", "{Function Description}") // 函示呼叫
		}
    });
Assistant assistant = assistantResponse.Value;
參數說明
Model輸入模型的名稱, Azure OpenAI 要填入的是在 Azure 上面自己部署的模型名稱,OpenAI 則是 OpenAI 上的模型名稱像是 gpt-4-1106-preview
Name助理的名稱。
Instructions助理介紹。系統使用的提示資料,寫的越清楚,未來回覆的結果會越符合需求。
Description可以針對助理撰寫描述,不影響回傳的結果。
Metadata可以輸入 16 組 Key Value 格式的資料,方便我們要做一些處理或是標記的時候使用。
Tools助理要使用的工具,目前支援的有 CodeInterpreterToolDefinitionRetrievalToolDefinitionFunctionToolDefinition 這三個工具,一個助理最多可以設定 128 個工具而 Azure OpenAI 目前不支援 RetrievalToolDefinition 這一個工具,如果不小心設定到就會回傳錯誤訊息說目前不支援。

回傳的物件內容就會是我們設定的參數,以及會有一組 asst 開頭的 Id,未來可以透過這組 Id 來重新取得助理物件跟調整設定。

建立助理檔案

在啟用工具的時候需要搭配檔案的話可以上傳支援的檔案類型上去,並且在建立或更新助理的時候加入,底下示範寫入一個文字檔案並且上傳,上傳之後會取得一個助理檔案的物件,Purpose 則是設定未來要給誰使用,這邊就固定設定助理。

File.WriteAllText(
		path: "sample_file_for_upload.txt",
		contents: "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457.");
Response<OpenAIFile> uploadAssistantFileResponse = await client.UploadFileAsync(
	localFilePath: "sample_file_for_upload.txt",
	purpose: OpenAIFilePurpose.Assistants);
OpenAIFile uploadedAssistantFile = uploadAssistantFileResponse.Value;

回傳的物件可以從 Value 取得對應的資料,可以取得一組 assistant 開頭的檔案 Id 供我們使用。

可以在建立助理的時候傳入檔案 Id,最多可以建立 20 個檔案,並且會根據建立時間來排序,最新的檔案會在最前面。

Response<Assistant> assistantResponse = await client.CreateAssistantAsync(
	new AssistantCreationOptions("{Model Name}")
	{
		Name = "{Name}"
		Tools = {
			new CodeInterpreterToolDefinition(),
		FileIds = { uploadedAssistantFile.Id }
	});

上傳之後的助理檔案可以在助理遊樂場新增檔案時候可以被選取到。

也可以在檔案資料這編列出來,也可以查詢到檔案 Id。

目前可以支援的檔案類型如下:

列出所有助理

沒有記錄助理 Id 的話可以透過底下方法來列出所有助理,回傳的是助理的物件清單。

PageableList<Assistant> assistants = await client.GetAssistantsAsync();

列出所有助理檔案

如果要查詢上傳或是助理產生的檔案列表可以用底下程式來取得,會得到檔案的清單物件。

var assistantFiles = await client.GetFilesAsync();

檢視助理

要取得之前建立過的助理只要傳入 Id 即可。

Response<Assistant> assistantResponse = await client.GetAssistantAsync("{Assistant id}");
Assistant assistant = assistantResponse.Value;

檢視助理檔案

可以透過底下程式來取得檔案的物件,注意這方法取得的是檔案的物件,並非檔案內容,檔案內容請參考後面的方法。

Response<OpenAIFile> assistantFile = await client.GetFileAsync("{Assistant File id}");

取得助理檔案內容

如果要取得實際的檔案內容的話,可以傳入檔案 id 來取得,但是僅有助理執行之後產生的檔案可以下載的到,類別 Purpose 會是 assistants_output,不然會出現錯誤訊息,回傳的型別會是 BinaryData,再把它轉成文字或是檔案儲存即可。

Response<BinaryData> fileResponse = await client.GetFileContentAsync("{File Id}");
BinaryData file = fileResponse.Value;

可以透過底下程式碼存成實體檔案,至於是文字的話就可以直接 ToString() 就可以了。

File.WriteAllBytes("{Path}", file.ToArray());

更新助理

要更新助理的模型或是設定的話可以用底下方法來更新,不需要更新的屬性可以不傳入,只傳入需要更新的屬性即可,會回傳更新後的助理物件。

Response<Assistant> assistantUpdateResponse = await client.UpdateAssistantAsync(
	"{Assistant Id}",
	new UpdateAssistantOptions()
	{
		Model = "{Model Name}",
		Name = "{Assistant Name}",
		Instructions = "{Assistant Instructions}",
		Description = "{Instructions}",
		Metadata = new Dictionary<string, string>()
		{
			//{ "Demo", "Sample"},
			//{ "Test", "Sample"},
		},
		Tools = {
			//new CodeInterpreterToolDefinition(),
			//new RetrievalToolDefinition(),
			//new FunctionToolDefinition("FunctionName", "Function Description")
		}
	});
Assistant assistant = assistantResponse.Value;

刪除助理

要刪除助理的話也是傳入助理 Id 即可,會回傳成功與否。

var deleteAssistant = await client.DeleteAssistantAsync("{assistant Id}");
Console.WriteLine($"Delete Assistant {assistant.Id} {deleteAssistant.Value}");

刪除助理檔案

要刪除上傳的檔案傳入檔案 Id 即可,會回傳成功與否。

var assistantFileId = "{Assistant File Id}";
var deleteAssistantFile = await client.DeleteFileAsync(assistantFileId);
Console.WriteLine($"Delete Assistant {assistantFileId} {deleteAssistantFile.Value}");

Thread

建立聊天串

建立聊天串就沒有額外參數需要設定,結果會回傳一個聊天串的物件。

Response<AssistantThread> threadResponse = await client.CreateThreadAsync();
AssistantThread thread = threadResponse.Value;

可以從 Value 取得物件內容,會可以取得一組 thread 開頭的聊天串 Id,強烈建議要把這組 Id 記錄下來,沒記錄下來又沒有刪除的話,就會無法再找到這組 Id 了,目前是沒有方法可以列出所有聊天串的,根據找到的討論原本是有這方法的,後來應該是為了隱私權或安全性等因素所以不提供,避免同組織內不同使用者取得其它人建立的聊天串,裡面可能有終端使用者的一些聊天內容,所以就被拿掉了。

檢視聊天串

使用聊天串 Id 就可以重新取得聊天串物件。

Response<AssistantThread> threadResponse = await client.GetThreadAsync("{Thread id}");
AssistantThread thread = threadResponse.Value;

更新聊天串

可以透過聊天串 Id 來更新聊天串,但是可以更新的值只有 Metadata。

Response<AssistantThread> threadResponse = await client.UpdateThreadAsync(
	"{Thread id}",
	new Dictionary<string, string>()
	{
		//{ "Demo", "Sample"},
		//{ "Test", "Sample"},
	});
AssistantThread thread = threadResponse.Value;

刪除聊天串

串入聊天串 id 就可以刪除了,會回傳刪除結果。

var deleteThread = await client.DeleteThreadAsync(thread.Id);
Console.WriteLine($"Delete Thread {thread.Id} {deleteThread.Value}");

Message

訊息沒有刪除的功能,因為需要保留上下文,所以不能直接刪除,但是訊息是綁定聊天串,所以聊天串被刪除的時候也會跟著被刪除。

訊息的物件會包含底下內容。

參數說明
Id訊息 Id,會以 msg 開頭。
CreatedAt建立時間。
ThreadId聊天串 id。
Role訊息角色,會有 User 和 Assistant 兩個角色,Assistant 是助理運行之後回覆的訊息。
ContentItems訊息內容列表,會有文字 (MessageTextContent) 和圖片 (MessageImageFileContent) 兩種類型。
AssistantId助理 Id。產生此訊息關連的助理 Id。
RunId執行 Id。產生此訊息關連的執行 Id。
FileIds檔案列表。跟此訊息相關的檔案,建立時候可以一併提供給工具使用。
Metadata可以輸入 16 組 Key Value 格式的資料,方便我們要做一些處理或是標記的時候使用。

建立訊息

建立訊息的時候需帶入聊天串 Id,建立的時候只能設定角色是 User,最後就是訊息內容,檔案為有需要給工具使用時候可以附上,回傳結果為訊息的物件。

Response<ThreadMessage> messageResponse = await client.CreateMessageAsync(
	"{Thread Id}",
	MessageRole.User,
	"{Message}",
	fileIds: new List<string>() { "assistant-qTZbuOgZF0rVxLBYWfYUKou0" },
	metadata: new Dictionary<string, string>()
	{
		//{ "Demo", "Sample"},
		//{ "Test", "Sample"},
	}
);
ThreadMessage message = messageResponse.Value;

列出所有訊息

透過傳入的聊天串 Id 就可以列出所有的訊息,會按照時間排序,最新的會在最上面。

Response<PageableList<ThreadMessage>> afterRunMessagesResponse = await client.GetMessagesAsync("{Thread Id}");
IReadOnlyList<ThreadMessage> messages = afterRunMessagesResponse.Value.Data;

後續就可以用迴圈把訊息內容輸出。

foreach (ThreadMessage threadMessage in messages)
{
	Console.Write($"{threadMessage.CreatedAt:yyyy-MM-dd HH:mm:ss} - {threadMessage.Role,10}: ");
	foreach (MessageContent contentItem in threadMessage.ContentItems)
	{
		if (contentItem is MessageTextContent textItem)
		{
			Console.Write(textItem.Text);
		}
		else if (contentItem is MessageImageFileContent imageFileItem)
		{
			Console.Write($"<image from ID: {imageFileItem.FileId}");
		}
		Console.WriteLine();
	}
}

列出所有訊息檔案

有需要的時候也可以獨立列出訊息中帶的檔案。

Response<PageableList<MessageFile>> messageFileResponse = await client.GetMessageFilesAsync("{Thread Id}", "{Message Id}");
IReadOnlyList<MessageFile> MessageFiles = messageFileResponse.Value.Data;

回傳結果是訊息檔案物件,非檔案實體內容,可以再用此助理檔案 Id 去取得實體檔案內容。

檢視訊息

有需要時候也可以單獨取得特定的訊息內容,傳入聊天串 Id 和訊息 Id 即可,會回傳訊息的物件。

Response<ThreadMessage> messageResponse = await client.GetMessageAsync("{Thread Id}", "{Message Id}");
ThreadMessage message = messageResponse.Value;

檢視訊息檔案

有需要時候也可以單獨取得特定的訊息檔案,傳入聊天串 Id 、訊息 Id 和檔案 Id 即可,會回傳訊息檔案的物件,只是大概只有建立時間有用,其它都是已知的。

Response<MessageFile> messageFileResponse = await client.GetMessageFileAsync("{Thread Id}", "{Message Id}", "{File Id}");
MessageFile messageFile = messageFileResponse.Value;

修改訊息

修改訊息內容可以透過底下程式來更新,但是可以修改的只有 Metadata。

Response<ThreadMessage> UpdateMessageResponse = await client.UpdateMessageAsync("{Thread Id}", "{Message Id}", new Dictionary<string, string>()
	{
		//{ "Demo", "Sample1"},
		//{ "Test", "Sample1"},
	});
ThreadMessage message = UpdateMessageResponse.Value;

Run

建立執行

建立執行的時候需要帶入聊天串 Id 和助理 id,設定上有些複寫的功能,可以再本次執行的時候複寫設定,對於需要在特定時候用特定設定來運行的時候會很方便。

Response<ThreadRun> runResponse = await client.CreateRunAsync(
	thread.Id,
	new CreateRunOptions(assistant.Id)
	{
		//AdditionalInstructions = "{Additional Instructions}",
		//OverrideModelName = "{Model name}",
		//OverrideInstructions = "{Override Instructions}",
		//OverrideTools = {
			//new CodeInterpreterToolDefinition(), // 程式碼解譯器
			//new RetrievalToolDefinition(),	// 知識檢索
			//new FunctionToolDefinition("{FunctionName}", "{Function Description}") // 函示呼叫
		//},
		//Metadata = new Dictionary<string, string>()
		//{
		//	   { "DemoRun", "SampleRun"},
		//	   { "TestRun", "SampleRun"},
		//}
	});
ThreadRun run = runResponse.Value;

建立完執行之後就是用一個迴圈來檢查處理的狀態,確定有結果之後才跳出。

do
{
	await Task.Delay(TimeSpan.FromMilliseconds(500));
	runResponse = await client.GetRunAsync(thread.Id, runResponse.Value.Id);
}
while (runResponse.Value.Status == RunStatus.Queued || runResponse.Value.Status == RunStatus.InProgress);

狀態的生命週期如下圖所示,最開始會是 Queued 狀態。

狀態SDK 狀態名稱說明
queuedRunStatus.Queued首次建立執行或是完成 requires_action 的處理之後都會進入 queued 狀態。通常不會存在太久,會很快進入 in_progress
in_progressRunStatus.InProgress在此狀態時助理會使用模型或是工具來執行步驟。可以透過取得執行步驟的方法來查看處理的進度。
completedRunStatus.Completed完成執行的處理。此狀態就可以去取得助理處理的結果和執行的步驟,也可以將此聊天串再繼續新增訊息來進行新的一輪執行。
requires_actionRunStatus.RequiresAction使用函示呼叫工具時如果確認需要呼叫的函示名稱和參數之後就會進入此狀態。此時需要去執行這些函示之後透過提交工具輸出給執行方法交函示處理結果提交給執行來繼續完成處理。如果到期時間 (ExpiresAt,約建立時間10分鐘後) 還沒有提交結果就會進入 expired 狀態。
expiredRunStatus.Expired如果過期時間到之前還為提供函示輸出的結果就會進入此狀態。或是執行結果太長超過到期時間也會進入此狀態。
cancellingRunStatus.Cancellingin_progress 狀態下可以提交取消執行來取消執行處理,一旦成功取消會進入 cancelled 狀態。系統會嘗試取消,但不保證一定會成功。
cancelledRunStatus.Cancelled執行成功被取消。
failedRunStatus.Failed執行失敗,可以透過執行物件的屬性 LastError 來查看錯誤的原因。處理失敗的時間會記錄在 FailedAt。

建立聊天串並執行

Assistants API 也另外提供一個方法可以同時建立聊天串並執行助理取得結果。

Response<ThreadRun> runResponse = await client.CreateThreadAndRunAsync(new CreateAndRunThreadOptions("{Assistant Id}"));

取得所有執行列表

可以透過底下方法傳入聊天串 Id 就可以列出聊天串所有的執行清單。

Response<PageableList<ThreadRun>> threadRunResponse = await client.GetRunsAsync("Thread Id");
IReadOnlyList<ThreadRun> threadRuns = threadRunResponse.Value.Data;

回傳的清單範例如下:

檢視執行

有需要的時候也可以直接取得執行的物件。

Response<ThreadRun> runResponse = await client.GetRunAsync("{Thread Id}", "{Run Id}");
ThreadRun run = runResponse.Value;

修改執行

針對執行也可以修改,但僅限於修改 Metadata 。

Response<ThreadRun> runResponse = await client.UpdateRunAsync("{Thread Id}", "{Run Id}", new Dictionary<string, string>()
		{
		   //{ "DemoRun", "SampleRun"},
		   //{ "TestRun", "SampleRun"},
		});
ThreadRun run = runResponse.Value;

取消執行

針對狀態是 InProgress 的執行,可以取消執行。

Response<ThreadRun> runResponse = await client.CancelRunAsync("{Thread Id}", "{Run Id}");
ThreadRun run = runResponse.Value;

提交工具輸出給執行

在狀態是 RequiresAction 且類別是 SubmitToolOutputsAction 的時候需要將助理判斷要呼叫的函示處理結果提交給執行,讓執行可以繼續往下處理。

await client.SubmitToolOutputsToRunAsync(runResponse.Value, toolOutputs);

完整使用範例是在確認執行狀態中提供函示處理結果。

do
{
    await Task.Delay(TimeSpan.FromMilliseconds(500));
    runResponse = await client.GetRunAsync("Thread Id", "Run Id");

    if (runResponse.Value.Status == RunStatus.RequiresAction
        && runResponse.Value.RequiredAction is SubmitToolOutputsAction submitToolOutputsAction)
    {
        List<ToolOutput> toolOutputs = new();
        foreach (RequiredToolCall toolCall in submitToolOutputsAction.ToolCalls)
        {
            toolOutputs.Add(GetResolvedToolOutput(toolCall));
        }
        runResponse = await client.SubmitToolOutputsToRunAsync(runResponse.Value, toolOutputs); // 提交工具輸出給執行
    }
}
while (runResponse.Value.Status == RunStatus.Queued || runResponse.Value.Status == RunStatus.InProgress);

RunStep

列出所有執行步驟

透過傳入 ThreadRun 物件可以列出此次執行的步驟,對於執行失敗或是執行比較久的助理執行在偵錯上就會有幫助。

Response<PageableList<RunStep>> runStepsResponse = await client.GetRunStepsAsync(run);
IReadOnlyList<RunStep> runSteps = runStepsResponse.Value.Data;

檢視執行步驟

有需要也可以單獨取得一筆執行步驟來檢視。

Response<RunStep> runStepResponse = await client.GetRunStepAsync("Thread Id","Run Id","RunStep Id");
RunStep runStep = runStepResponse.Value;

結論

本文完整介紹所有 Assistants API 使用方法,希望對於 Assistants API 感興趣的朋友會有幫助,後面會再針對 Assistants API 支援的工具做詳細的探討和實做範例。

參考資料