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

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

實做

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

AssistantClient

因為 Azure OpenAI 會根據每個人部署的名稱而有屬於自己的節點所已建立 Client 時候會要多了結點的網址,這部分的建立可以參考底下程式,就可以根據需求切換了,另外因為目前還是實驗性功能,所以會有警告,需要把警告關閉。

#pragma warning disable OPENAI001
OpenAIClient openAIClient = isAzureOpenAI
	? new AzureOpenAIClient(new Uri(azureResourceUrl), new AzureKeyCredential(azureApiKey))
	: new OpenAIClient("{OPENAI KEY}");
AssistantClient assistantClient = openAIClient.GetAssistantClient();

Assistant

建立助理

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

Assistant assistant = await assistantClient.CreateAssistantAsync(
model: deploymentName,
options: new AssistantCreationOptions()
{
	Name = "DEMO 助理",
	Instructions = "你是 Azure 專家,會回覆關於 Azure 的問題。",
	Description = "DEMO 用的助理",
	Tools = {
		ToolDefinition.CreateCodeInterpreter(), // 程式碼解譯器
		ToolDefinition.CreateFileSearch(), // 知識檢索
		ToolDefinition.CreateFunction("{Function Name}", "{Function Description}"), // 函示呼叫
		// 另一種寫法
		// new CodeInterpreterToolDefinition(),  // 程式碼解譯器
		// new FileSearchToolDefinition(), // 知識檢索
		// new FunctionToolDefinition("{Function Name}", "{Function Description}") // 函示呼叫
	},
});
參數說明
Model輸入模型的名稱, Azure OpenAI 要填入的是在 Azure 上面自己部署的模型名稱,OpenAI 則是 OpenAI 上的模型名稱像是 gpt-4-1106-preview
Name助理的名稱。
Instructions助理介紹。系統使用的提示資料,寫的越清楚,未來回覆的結果會越符合需求。
Description可以針對助理撰寫描述,不影響回傳的結果。
Tools助理要使用的工具,目前支援的有 CodeInterpreterToolDefinitionFileSearchToolDefinitionFunctionToolDefinition 這三個工具,一個助理最多可以設定 128 個工具。

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

建立助理檔案

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

File.WriteAllText(
	path: "sample_file_for_upload.txt",
	contents: "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457.");

FileClient fileClient = openAIClient.GetFileClient();
OpenAIFileInfo assistantFile = await fileClient.UploadFileAsync(
	filePath: "sample_file_for_upload.txt",
	purpose: FileUploadPurpose.Assistants);

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

可以在建立助理的時候可以針對工具設定檔案 Id,程式碼解譯器最多可以建立 20 個檔案,知識檢索則可以支援 10000 個檔案,會處理成向量資料來搜尋檔案內容。

Assistant assistant = await assistantClient.CreateAssistantAsync(
model: deploymentName,
options: new AssistantCreationOptions()
{
	Name = "{Name}",
	Tools = {
		ToolDefinition.CreateCodeInterpreter(), // 程式碼解譯器
		ToolDefinition.CreateFileSearch(), // 知識檢索
	},
	ToolResources = new()
	{
		FileSearch = new()
		{
			NewVectorStores =
				{
					new VectorStoreCreationHelper([assistantFile.Id]),
				}
		},
		CodeInterpreter = new()
		{
			FileIds = [assistantFile.Id]
		}
	}
});

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

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

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

列出所有助理

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

var assistants = assistantClient.GetAssistantsAsync();

列出所有助理檔案

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

var assistantFiles = fileClient.GetFilesAsync(purpose: OpenAIFilePurpose.Assistants);

檢視助理

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

Assistant assistant = await assistantClient.GetAssistantAsync("{Assistant id}");

檢視助理檔案

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

OpenAIFileInfo openAIFileInfo = await fileClient.GetFileAsync("{Assistant File id}");

取得助理檔案內容

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

BinaryData file = await fileClient.DownloadFileAsync({File Id});

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

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

修改助理

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

Assistant assistant = await assistantClient.ModifyAssistantAsync("{Assistant Id}", new AssistantModificationOptions()
{
	Model = "{Model Name}",
	Name = "{Assistant Name}",
	Instructions = "{Assistant Instructions}",
	Description = "{Instructions}",
	DefaultTools = {
		new CodeInterpreterToolDefinition(),
		new FileSearchToolDefinition(),
		new FunctionToolDefinition("{FunctionName}", "{Function Description}")
	}
});

刪除助理

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

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

刪除助理檔案

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

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

Thread

建立聊天串

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

AssistantThread thread = await assistantClient.CreateThreadAsync();

也可以在一開始建立的時候就設定初始的設定和 Message。

AssistantThread thread = await assistantClient.CreateThreadAsync(new ThreadCreationOptions()
{
	InitialMessages = { new ThreadInitializationMessage(
	[
		"{Message}"
	]) },
	ToolResources = new()
	{
		FileSearch = new()
		{
			NewVectorStores =
			{
				new VectorStoreCreationHelper(["{File Id}"]),
			}
		},
		CodeInterpreter = new()
		{
			FileIds = ["{File Id}"]
		}
	}
});

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

檢視聊天串

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

AssistantThread thread = await assistantClient.GetThreadAsync("{Thread Id}")

更新聊天串

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

AssistantThread thread = await assistantClient.ModifyThreadAsync("{Thread Id}", new ThreadModificationOptions()
{
	ToolResources = new()
	{
		FileSearch = new()
		{
			NewVectorStores =
		{
			new VectorStoreCreationHelper(["{File Id}"]),
		}
		},
		CodeInterpreter = new()
		{
			FileIds = ["{File Id}"]
		}
	}
});

刪除聊天串

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

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

Message

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

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

參數說明
Id訊息 Id,會以 msg 開頭。
CreatedAt建立時間。
ThreadId聊天串 id。
Role訊息角色,會有 User 和 Assistant 兩個角色,Assistant 是助理運行之後回覆的訊息。
Content

訊息內容列表。

AssistantId助理 Id。產生此訊息關連的助理 Id。
RunId執行 Id。產生此訊息關連的執行 Id。
FileIds檔案列表。跟此訊息相關的檔案,建立時候可以一併提供給工具使用。

建立訊息

建立訊息的時候需帶入聊天串 Id,回傳結果為訊息的物件。

ThreadMessage threadMessage = await assistantClient.CreateMessageAsync(
	thread.Id,
	["{Message}"]
);

列出所有訊息

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

var threadMessages = await assistantClient.GetMessagesAsync(thread);

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

await foreach (ThreadMessage threadMessage in threadMessages)
{
	Console.Write($"{threadMessage.CreatedAt:yyyy-MM-dd HH:mm:ss} - {threadMessage.Role,10}: ");
	foreach (MessageContent contentItem in threadMessage.Content)
	{
		if (!string.IsNullOrEmpty(contentItem.Text))
		{
			Console.Write(contentItem.Text);
		}
		else if (!string.IsNullOrEmpty(contentItem.ImageFileId))
		{
			Console.Write($"<image from ID: {contentItem.ImageFileId}");
		}
		Console.WriteLine();
	}
}

列出所有訊息檔案

有需要的時候也可以透過 Attachments 列出訊息中帶的檔案。

IReadOnlyList<MessageCreationAttachment> MessageFiles = threadMessages.Attachments;

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

檢視訊息

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

ThreadMessage threadMessages = await assistantClient.GetMessageAsync("{Thread Id}", "{Message Id}");

Run

建立執行

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

RunCreationOptions runOptions = new RunCreationOptions()
{
	// AdditionalInstructions = "{Additional Instructions}",
	
	// ModelOverride = "{Model name}",
	// InstructionsOverride = "{Override Instructions}",
	// ToolsOverride = {
	//	 new CodeInterpreterToolDefinition(), // 程式碼解譯器
	//	 new FileSearchToolDefinition(),	// 知識檢索
	//	 new FunctionToolDefinition("{FunctionName}", "{Function Description}") // 函示呼叫
	}
};
var runResponse = await assistantClient.CreateRunAsync(thread.Id, assistant.Id, runOptions);
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 也另外提供一個方法可以同時建立聊天串並執行助理取得結果,就可以直接透過串流方式來取得回覆結果。

ThreadRun runResponse1 = await assistantClient.CreateThreadAndRunAsync(assistant);

取得所有執行列表

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

var threadRunResponse = await client.GetRunsAsync("{Thread Id}");

檢視執行

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

ThreadRun threadRun = await assistantClient.GetRunAsync("{Thread Id}", "{Run Id}");

取消執行

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

ThreadRun threadRun = await assistantClient.CancelRunAsync("{Thread Id}", "{Run Id}");

提交工具輸出給執行

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

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

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

do
{
	await Task.Delay(TimeSpan.FromMilliseconds(500));
	threadRun = await assistantClient.GetRunAsync(thread.Id, threadRun.Id);

	if (threadRun.Status == RunStatus.RequiresAction)
	{
		List<ToolOutput> toolOutputs = new();
		foreach (RequiredAction action in threadRun.RequiredActions)
		{
			toolOutputs.Add(GetResolvedToolOutput(action));
		}
		
		threadRun = await assistantClient.SubmitToolOutputsToRunAsync(threadRun, toolOutputs); // 提交工具輸出給執行
	}
}
while (threadRun.Status == RunStatus.Queued || threadRun.Status == RunStatus.InProgress);

RunStep

列出所有執行步驟

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

var runStepsResponse = assistantClient.GetRunStepsAsync(threadRun);

檢視執行步驟

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

RunStep runStepResponse = await assistantClient.GetRunStepAsync("{Thread Id}","{Run Id}","{RunStep Id}");

結論

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

參考資料