diff --git a/java-library/pom.xml b/java-library/pom.xml index 85a1e7a0..400f666e 100644 --- a/java-library/pom.xml +++ b/java-library/pom.xml @@ -84,7 +84,7 @@ com.azure azure-ai-openai - 1.0.0-beta.10 + 1.0.0-beta.11 compile diff --git a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantCreateRequest.java b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantCreateRequest.java index 7e621f63..f9f0c6ef 100644 --- a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantCreateRequest.java +++ b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantCreateRequest.java @@ -14,7 +14,7 @@ public class AssistantCreateRequest { private String id; private String instructions = "You are a helpful assistant."; private String chatStorageConnectionSetting; - private String collectionName = "SampleChatState"; + private String collectionName = "ChatState"; public AssistantCreateRequest(String id) { this.id = id; diff --git a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantPost.java b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantPost.java index 768b127f..67a3cc3f 100644 --- a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantPost.java +++ b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantPost.java @@ -13,7 +13,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - /** *

* Assistant post input attribute which is used to update the assistant. @@ -26,18 +25,34 @@ @CustomBinding(direction = "in", name = "", type = "assistantPost") public @interface AssistantPost { + /** + * The default storage account setting for the table storage account. + * This constant is used to specify the connection string for the table storage + * account + * where chat data will be stored. + */ + String DEFAULT_CHATSTORAGE = "AzureWebJobsStorage"; + + /** + * The default collection name for the table storage account. + * This constant is used to specify the collection name for the table storage + * account + * where chat data will be stored. + */ + String DEFAULT_COLLECTION = "ChatState"; + /** * The variable name used in function.json. * * @return The variable name used in function.json. - */ + */ String name(); - + /** * The ID of the Assistant to query. * * @return The ID of the Assistant to query. - */ + */ String id(); /** @@ -48,12 +63,27 @@ */ String model(); - /** * The user message that user has entered for assistant to respond to. * * @return The user message that user has entered for assistant to respond to. - */ + */ String userMessage(); + /** + * The configuration section name for the table settings for assistant chat + * storage. + * + * @return The configuration section name for the table settings for assistant + * chat storage. By default, it returns {@code DEFAULT_CHATSTORAGE}. + */ + String chatStorageConnectionSetting() default DEFAULT_CHATSTORAGE; + + /** + * The table collection name for assistant chat storage. + * + * @return the table collection name for assistant chat storage.By default, it + * returns {@code DEFAULT_COLLECTION}. + */ + String collectionName() default DEFAULT_COLLECTION; } diff --git a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantQuery.java b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantQuery.java index cb4831dc..caf7c2a6 100644 --- a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantQuery.java +++ b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/assistant/AssistantQuery.java @@ -12,10 +12,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - + /** *

- * Assistant query input attribute which is used query the Assistant to get current state. + * Assistant query input attribute which is used query the Assistant to get + * current state. *

* * @since 1.0.0 @@ -25,13 +26,29 @@ @CustomBinding(direction = "in", name = "", type = "assistantQuery") public @interface AssistantQuery { + /** + * The default storage account setting for the table storage account. + * This constant is used to specify the connection string for the table storage + * account + * where chat data will be stored. + */ + String DEFAULT_CHATSTORAGE = "AzureWebJobsStorage"; + + /** + * The default collection name for the table storage account. + * This constant is used to specify the collection name for the table storage + * account + * where chat data will be stored. + */ + String DEFAULT_COLLECTION = "ChatState"; + /** * The variable name used in function.json. * * @return The variable name used in function.json. */ String name(); - + /** * The ID of the Assistant to query. * @@ -41,10 +58,27 @@ /** * The timestamp of the earliest message in the chat history to fetch. - * The timestamp should be in ISO 8601 format - for example, 2023-08-01T00:00:00Z. + * The timestamp should be in ISO 8601 format - for example, + * 2023-08-01T00:00:00Z. * * @return The timestamp of the earliest message in the chat history to fetch. */ String timestampUtc(); - - } + + /** + * The configuration section name for the table settings for assistant chat + * storage. + * + * @return The configuration section name for the table settings for assistant + * chat storage. By default, it returns {@code DEFAULT_CHATSTORAGE}. + */ + String chatStorageConnectionSetting() default DEFAULT_CHATSTORAGE; + + /** + * The table collection name for assistant chat storage. + * + * @return the table collection name for assistant chat storage.By default, it + * returns {@code DEFAULT_COLLECTION}. + */ + String collectionName() default DEFAULT_COLLECTION; +} diff --git a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/embeddings/EmbeddingsContext.java b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/embeddings/EmbeddingsContext.java index 8908b33b..1627b380 100644 --- a/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/embeddings/EmbeddingsContext.java +++ b/java-library/src/main/java/com/microsoft/azure/functions/openai/annotation/embeddings/EmbeddingsContext.java @@ -13,7 +13,7 @@ public class EmbeddingsContext { private EmbeddingsOptions request; private Embeddings response; - private int count; + private int count = 0; public EmbeddingsOptions getRequest() { return request; diff --git a/samples/assistant/README.md b/samples/assistant/README.md index eda3965b..b4d0e541 100644 --- a/samples/assistant/README.md +++ b/samples/assistant/README.md @@ -26,27 +26,6 @@ This OpenAI extension internally uses the [function calling](https://platform.op * 0301 is the default and oldest model version for gpt-3.5 but it doesn't support this feature. * Model version 1106 has known issue with duplicate function calls in the OpenAI extension, check the repo issues for progress as the extension team works on it. -### Chat Storage Configuration - -If you are using a different table storage than `AzureWebJobsStorage` for chat storage, follow these steps: - -1. **Managed Identity - Assign Permissions**: - * Assign the user or function app's managed identity the role of `Storage Table Data Contributor`. - -1. **Configure Table Service URI**: - * Set the `tableServiceUri` in the configuration as follows: - - ```json - "__tableServiceUri": "tableServiceUri" - ``` - - * Replace `CONNECTION_NAME_PREFIX` with the appropriate prefix. - -1. **Update Function Code**: - * Supply the `ConnectionNamePrefix` to `ChatStorageConnectionSetting` in the function code. This will replace the default value of `AzureWebJobsStorage`. - -For additional details on using identity-based connections, refer to the [Azure Functions reference documentation](https://learn.microsoft.com/azure/azure-functions/functions-reference?#common-properties-for-identity-based-connections). - ## Defining skills You can define a skill by creating a function that uses the `AssistantSkillTrigger` binding. The following example shows a skill that adds a todo item to a database: @@ -205,18 +184,26 @@ Additionally, if you want to run the sample with Cosmos DB, then you must also d * Install the [Azure Cosmos DB Emulator](https://docs.microsoft.com/azure/cosmos-db/local-emulator), or get a connection string to a real Azure Cosmos DB resource. * Update the `CosmosDbConnectionString` setting in the `local.settings.json` file and configure it with the connection string to your Cosmos DB resource (local or Azure). -Also note that the storage of chat history is done via table storage. You may configure the `host.json` file within the project to be as follows: +### Chat Storage Configuration -```json -"extensions": { - "openai": { - "storageConnectionName": "AzureWebJobsStorage", - "collectionName": "SampleChatState" - } -} -``` +If you are using a different table storage than `AzureWebJobsStorage` for chat storage, follow these steps: + +1. **Managed Identity - Assign Permissions**: + * Assign the user or function app's managed identity the role of `Storage Table Data Contributor`. + +1. **Configure Table Service URI**: + * Set the `tableServiceUri` in the configuration as follows: -`StorageConnectionName` is the name of connection string of a storage account and `CollectionName` is the name of the table that would hold the chat state and messages. + ```json + "__tableServiceUri": "tableServiceUri" + ``` + + * Replace `CONNECTION_NAME_PREFIX` with the appropriate prefix. + +1. **Update Function Code**: + * Supply the `ConnectionNamePrefix` to `ChatStorageConnectionSetting` in the function code. This will replace the default value of `AzureWebJobsStorage`. + +For additional details on using identity-based connections, refer to the [Azure Functions reference documentation](https://learn.microsoft.com/azure/azure-functions/functions-reference?#common-properties-for-identity-based-connections). ## Running the sample diff --git a/samples/assistant/csharp-ooproc/AssistantApis.cs b/samples/assistant/csharp-ooproc/AssistantApis.cs index 60cdd9ac..a55df046 100644 --- a/samples/assistant/csharp-ooproc/AssistantApis.cs +++ b/samples/assistant/csharp-ooproc/AssistantApis.cs @@ -13,6 +13,9 @@ namespace AssistantSample; /// static class AssistantApis { + const string DefaultChatStorageConnectionSetting = "AzureWebJobsStorage"; + const string DefaultCollectionName = "ChatState"; + /// /// HTTP PUT function that creates a new assistant chat bot with the specified ID. /// @@ -37,8 +40,8 @@ public static async Task CreateAssistant( HttpResponse = new ObjectResult(new { assistantId }) { StatusCode = 202 }, ChatBotCreateRequest = new AssistantCreateRequest(assistantId, instructions) { - ChatStorageConnectionSetting = "AzureWebJobsStorage", - CollectionName = "SampleChatState", + ChatStorageConnectionSetting = DefaultChatStorageConnectionSetting, + CollectionName = DefaultCollectionName, }, }; } @@ -59,7 +62,7 @@ public class CreateChatBotOutput public static async Task PostUserQuery( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "assistants/{assistantId}")] HttpRequestData req, string assistantId, - [AssistantPostInput("{assistantId}", "{Query.message}", Model = "%CHAT_MODEL_DEPLOYMENT_NAME%")] AssistantState state) + [AssistantPostInput("{assistantId}", "{Query.message}", Model = "%CHAT_MODEL_DEPLOYMENT_NAME%", ChatStorageConnectionSetting = DefaultChatStorageConnectionSetting, CollectionName = DefaultCollectionName)] AssistantState state) { return new OkObjectResult(state.RecentMessages.LastOrDefault()?.Content ?? "No response returned."); } @@ -71,7 +74,7 @@ public static async Task PostUserQuery( public static async Task GetChatState( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "assistants/{assistantId}")] HttpRequestData req, string assistantId, - [AssistantQueryInput("{assistantId}", TimestampUtc = "{Query.timestampUTC}")] AssistantState state) + [AssistantQueryInput("{assistantId}", TimestampUtc = "{Query.timestampUTC}", ChatStorageConnectionSetting = DefaultChatStorageConnectionSetting, CollectionName = DefaultCollectionName)] AssistantState state) { return new OkObjectResult(state); } diff --git a/samples/assistant/csharp-ooproc/TodoManager.cs b/samples/assistant/csharp-ooproc/TodoManager.cs index c267925b..53d572e2 100644 --- a/samples/assistant/csharp-ooproc/TodoManager.cs +++ b/samples/assistant/csharp-ooproc/TodoManager.cs @@ -67,22 +67,15 @@ class CosmosDbTodoManager : ITodoManager public CosmosDbTodoManager(ILoggerFactory loggerFactory, CosmosClient cosmosClient) { - if (loggerFactory is null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - if (cosmosClient is null) - { - throw new ArgumentNullException(nameof(cosmosClient)); - } + ArgumentNullException.ThrowIfNull(loggerFactory, nameof(loggerFactory)); + ArgumentNullException.ThrowIfNull(cosmosClient, nameof(cosmosClient)); string? CosmosDatabaseName = Environment.GetEnvironmentVariable("CosmosDatabaseName"); string? CosmosContainerName = Environment.GetEnvironmentVariable("CosmosContainerName"); if (string.IsNullOrEmpty(CosmosDatabaseName) || string.IsNullOrEmpty(CosmosContainerName)) { - throw new ArgumentNullException("CosmosDatabaseName and CosmosContainerName must be set as environment variables or in local.settings.json"); + throw new InvalidOperationException("CosmosDatabaseName and CosmosContainerName must be set as environment variables or in local.settings.json"); } this.logger = loggerFactory.CreateLogger(); diff --git a/samples/assistant/javascript/src/functions/assistantApis.js b/samples/assistant/javascript/src/functions/assistantApis.js index 469cae21..a8eaced4 100644 --- a/samples/assistant/javascript/src/functions/assistantApis.js +++ b/samples/assistant/javascript/src/functions/assistantApis.js @@ -3,6 +3,9 @@ const { app, input, output } = require("@azure/functions"); +const CHAT_STORAGE_CONNECTION_SETTING = "AzureWebJobsStorage"; +const COLLECTION_NAME = "ChatState"; + const chatBotCreateOutput = output.generic({ type: 'assistantCreate' }) @@ -21,8 +24,8 @@ app.http('CreateAssistant', { const createRequest = { id: assistantId, instructions: instructions, - chatStorageConnectionSetting: "AzureWebJobsStorage", - collectionName: "SampleChatState" + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME } context.extraOutputs.set(chatBotCreateOutput, createRequest) return { status: 202, jsonBody: { assistantId: assistantId } } @@ -34,7 +37,9 @@ const assistantPostInput = input.generic({ type: 'assistantPost', id: '{assistantId}', model: '%CHAT_MODEL_DEPLOYMENT_NAME%', - userMessage: '{Query.message}' + userMessage: '{Query.message}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('PostUserResponse', { methods: ['POST'], @@ -58,7 +63,9 @@ app.http('PostUserResponse', { const chatBotQueryInput = input.generic({ type: 'assistantQuery', id: '{assistantId}', - timestampUtc: '{Query.timestampUTC}' + timestampUtc: '{Query.timestampUTC}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('GetChatState', { methods: ['GET'], diff --git a/samples/assistant/powershell/CreateAssistant/run.ps1 b/samples/assistant/powershell/CreateAssistant/run.ps1 index d61679a9..c18d0d0c 100644 --- a/samples/assistant/powershell/CreateAssistant/run.ps1 +++ b/samples/assistant/powershell/CreateAssistant/run.ps1 @@ -9,9 +9,9 @@ $instructions += "\nAsk for clarification if a user request is ambiguous." $create_request = @{ "id" = $assistantId - "instructions" = $instructions, - "chatStorageConnectionSetting" = "AzureWebJobsStorage", - "collectionName" = "SampleChatState" + "instructions" = $instructions + "chatStorageConnectionSetting" = "AzureWebJobsStorage" + "collectionName" = "ChatState" } Push-OutputBinding -Name Requests -Value (ConvertTo-Json $create_request) diff --git a/samples/assistant/powershell/GetChatState/function.json b/samples/assistant/powershell/GetChatState/function.json index 56e22792..0aae0b70 100644 --- a/samples/assistant/powershell/GetChatState/function.json +++ b/samples/assistant/powershell/GetChatState/function.json @@ -21,7 +21,9 @@ "direction": "in", "dataType": "string", "id": "{assistantId}", - "timestampUtc": "{Query.timestampUTC}" + "timestampUtc": "{Query.timestampUTC}", + "chatStorageConnectionSetting": "AzureWebJobsStorage", + "collectionName": "ChatState" } ] } \ No newline at end of file diff --git a/samples/assistant/powershell/PostUserQuery/function.json b/samples/assistant/powershell/PostUserQuery/function.json index c6958834..024cd24f 100644 --- a/samples/assistant/powershell/PostUserQuery/function.json +++ b/samples/assistant/powershell/PostUserQuery/function.json @@ -22,7 +22,9 @@ "dataType": "string", "id": "{assistantId}", "userMessage": "{Query.message}", - "model": "%CHAT_MODEL_DEPLOYMENT_NAME%" + "model": "%CHAT_MODEL_DEPLOYMENT_NAME%", + "chatStorageConnectionSetting": "AzureWebJobsStorage", + "collectionName": "ChatState" } ] } \ No newline at end of file diff --git a/samples/assistant/python/assistant_apis.py b/samples/assistant/python/assistant_apis.py index 86b28335..9202c690 100644 --- a/samples/assistant/python/assistant_apis.py +++ b/samples/assistant/python/assistant_apis.py @@ -17,7 +17,7 @@ def create_assistant(req: func.HttpRequest, requests: func.Out[str]) -> func.Htt "id": assistantId, "instructions": instructions, "chatStorageConnectionSection": "AzureWebJobsStorage", - "collectionName": "SampleChatState" + "collectionName": "ChatState" } requests.set(json.dumps(create_request)) response_json = {"assistantId": assistantId} diff --git a/samples/assistant/typescript/src/functions/assistantApis.ts b/samples/assistant/typescript/src/functions/assistantApis.ts index 27d2963c..35bdd59c 100644 --- a/samples/assistant/typescript/src/functions/assistantApis.ts +++ b/samples/assistant/typescript/src/functions/assistantApis.ts @@ -3,6 +3,8 @@ import { HttpRequest, InvocationContext, app, input, output } from "@azure/functions" +const CHAT_STORAGE_CONNECTION_SETTING = "AzureWebJobsStorage"; +const COLLECTION_NAME = "ChatState"; const chatBotCreateOutput = output.generic({ type: 'assistantCreate' @@ -22,8 +24,8 @@ app.http('CreateAssistant', { const createRequest = { id: assistantId, instructions: instructions, - chatStorageConnectionSetting: "AzureWebJobsStorage", - collectionName: "SampleChatState" + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME } context.extraOutputs.set(chatBotCreateOutput, createRequest) return { status: 202, jsonBody: { assistantId: assistantId } } @@ -35,7 +37,9 @@ const assistantPostInput = input.generic({ type: 'assistantPost', id: '{assistantId}', model: '%CHAT_MODEL_DEPLOYMENT_NAME%', - userMessage: '{Query.message}' + userMessage: '{Query.message}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('PostUserResponse', { methods: ['POST'], @@ -59,7 +63,9 @@ app.http('PostUserResponse', { const chatBotQueryInput = input.generic({ type: 'assistantQuery', id: '{assistantId}', - timestampUtc: '{Query.timestampUTC}' + timestampUtc: '{Query.timestampUTC}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('GetChatState', { methods: ['GET'], diff --git a/samples/chat/csharp-ooproc/ChatBot.cs b/samples/chat/csharp-ooproc/ChatBot.cs index 73025983..2a94b4b4 100644 --- a/samples/chat/csharp-ooproc/ChatBot.cs +++ b/samples/chat/csharp-ooproc/ChatBot.cs @@ -12,6 +12,9 @@ namespace ChatBot; /// public static class ChatBot { + const string DefaultChatStorageConnectionSetting = "AzureWebJobsStorage"; + const string DefaultCollectionName = "ChatState"; + public class CreateRequest { [JsonPropertyName("instructions")] @@ -44,8 +47,8 @@ public static async Task CreateChatBot( HttpResponse = new ObjectResult(responseJson) { StatusCode = 201 }, ChatBotCreateRequest = new AssistantCreateRequest(chatId, createRequestBody?.Instructions) { - ChatStorageConnectionSetting = "AzureWebJobsStorage", - CollectionName = "SampleChatState" + ChatStorageConnectionSetting = DefaultChatStorageConnectionSetting, + CollectionName = DefaultCollectionName }, }; } @@ -63,7 +66,7 @@ public class CreateChatBotOutput public static async Task PostUserResponse( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "chats/{chatId}")] HttpRequestData req, string chatId, - [AssistantPostInput("{chatId}", "{Query.message}", Model = "%CHAT_MODEL_DEPLOYMENT_NAME%")] AssistantState state) + [AssistantPostInput("{chatId}", "{Query.message}", Model = "%CHAT_MODEL_DEPLOYMENT_NAME%", ChatStorageConnectionSetting = DefaultChatStorageConnectionSetting, CollectionName = DefaultCollectionName)] AssistantState state) { return new OkObjectResult(state.RecentMessages.LastOrDefault()?.Content ?? "No response returned."); } @@ -72,7 +75,7 @@ public static async Task PostUserResponse( public static async Task GetChatState( [HttpTrigger(AuthorizationLevel.Function, "get", Route = "chats/{chatId}")] HttpRequestData req, string chatId, - [AssistantQueryInput("{chatId}", TimestampUtc = "{Query.timestampUTC}")] AssistantState state, + [AssistantQueryInput("{chatId}", TimestampUtc = "{Query.timestampUTC}", ChatStorageConnectionSetting = DefaultChatStorageConnectionSetting, CollectionName = DefaultCollectionName)] AssistantState state, FunctionContext context) { return new OkObjectResult(state); diff --git a/samples/chat/javascript/src/app.js b/samples/chat/javascript/src/app.js index 6c82941c..f0a86ddd 100644 --- a/samples/chat/javascript/src/app.js +++ b/samples/chat/javascript/src/app.js @@ -3,6 +3,9 @@ const { app, input, output } = require("@azure/functions"); +const CHAT_STORAGE_CONNECTION_SETTING = "AzureWebJobsStorage"; +const COLLECTION_NAME = "ChatState"; + const chatBotCreateOutput = output.generic({ type: 'assistantCreate' }) @@ -18,8 +21,8 @@ app.http('CreateChatBot', { const createRequest = { id: chatID, instructions: inputJson.instructions, - chatStorageConnectionSetting: "AzureWebJobsStorage", - collectionName: "SampleChatState" + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME } context.extraOutputs.set(chatBotCreateOutput, createRequest) return { status: 202, jsonBody: { chatId: chatID } } @@ -30,7 +33,9 @@ app.http('CreateChatBot', { const assistantQueryInput = input.generic({ type: 'assistantQuery', id: '{chatId}', - timestampUtc: '{Query.timestampUTC}' + timestampUtc: '{Query.timestampUTC}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('GetChatState', { methods: ['GET'], @@ -48,7 +53,9 @@ const assistantPostInput = input.generic({ type: 'assistantPost', id: '{chatID}', model: '%CHAT_MODEL_DEPLOYMENT_NAME%', - userMessage: '{Query.message}' + userMessage: '{Query.message}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('PostUserResponse', { methods: ['POST'], diff --git a/samples/chat/powershell/CreateChatBot/run.ps1 b/samples/chat/powershell/CreateChatBot/run.ps1 index e669a730..eacd061c 100644 --- a/samples/chat/powershell/CreateChatBot/run.ps1 +++ b/samples/chat/powershell/CreateChatBot/run.ps1 @@ -8,9 +8,9 @@ Write-Host "Creating chat $chatID from input parameters $($inputJson)" $createRequest = @{ id = $chatID - instructions = $inputJson.Instructions, - chatStorageConnectionSetting = "AzureWebJobsStorage", - collectionName = "SampleChatState" + instructions = $inputJson.Instructions + chatStorageConnectionSetting = "AzureWebJobsStorage" + collectionName = "ChatState" } Push-OutputBinding -Name ChatBotCreate -Value $createRequest diff --git a/samples/chat/powershell/GetChatState/function.json b/samples/chat/powershell/GetChatState/function.json index 8a51d7da..d919a1b9 100644 --- a/samples/chat/powershell/GetChatState/function.json +++ b/samples/chat/powershell/GetChatState/function.json @@ -20,7 +20,9 @@ "direction": "in", "name": "ChatBotState", "id": "{chatId}", - "timeStampUtc": "{Query.timestampUTC}" + "timeStampUtc": "{Query.timestampUTC}", + "chatStorageConnectionSetting": "AzureWebJobsStorage", + "collectionName": "ChatState" } ] } \ No newline at end of file diff --git a/samples/chat/powershell/PostUserResponse/function.json b/samples/chat/powershell/PostUserResponse/function.json index f560e806..16142e20 100644 --- a/samples/chat/powershell/PostUserResponse/function.json +++ b/samples/chat/powershell/PostUserResponse/function.json @@ -21,7 +21,9 @@ "name": "ChatBotState", "id": "{chatId}", "model": "%CHAT_MODEL_DEPLOYMENT_NAME%", - "userMessage": "{Query.message}" + "userMessage": "{Query.message}", + "chatStorageConnectionSetting": "AzureWebJobsStorage", + "collectionName": "ChatState" } ] } \ No newline at end of file diff --git a/samples/chat/python/function_app.py b/samples/chat/python/function_app.py index a366e1b6..eb23ab2a 100644 --- a/samples/chat/python/function_app.py +++ b/samples/chat/python/function_app.py @@ -17,7 +17,7 @@ def create_chat_bot(req: func.HttpRequest, requests: func.Out[str]) -> func.Http "id": chatId, "instructions": input_json.get("instructions"), "chatStorageConnectionSection": "AzureWebJobsStorage", - "collectionName": "SampleChatState" + "collectionName": "ChatState" } requests.set(json.dumps(create_request)) response_json = {"chatId": chatId} diff --git a/samples/chat/typescript/src/functions/app.ts b/samples/chat/typescript/src/functions/app.ts index 78fad462..84a12075 100644 --- a/samples/chat/typescript/src/functions/app.ts +++ b/samples/chat/typescript/src/functions/app.ts @@ -3,6 +3,8 @@ import { HttpRequest, InvocationContext, app, input, output } from "@azure/functions"; +const CHAT_STORAGE_CONNECTION_SETTING = "AzureWebJobsStorage"; +const COLLECTION_NAME = "ChatState"; const chatBotCreateOutput = output.generic({ type: 'assistantCreate' @@ -19,8 +21,8 @@ app.http('CreateChatBot', { const createRequest = { id: chatID, instructions: inputJson.instructions, - chatStorageConnectionSetting: "AzureWebJobsStorage", - collectionName: "SampleChatState" + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME } context.extraOutputs.set(chatBotCreateOutput, createRequest) return { status: 202, jsonBody: { chatId: chatID } } @@ -31,7 +33,9 @@ app.http('CreateChatBot', { const assistantQueryInput = input.generic({ type: 'assistantQuery', id: '{chatId}', - timestampUtc: '{Query.timestampUTC}' + timestampUtc: '{Query.timestampUTC}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('GetChatState', { methods: ['GET'], @@ -49,7 +53,9 @@ const assistantPostInput = input.generic({ type: 'assistantPost', id: '{chatID}', model: '%CHAT_MODEL_DEPLOYMENT_NAME%', - userMessage: '{Query.message}' + userMessage: '{Query.message}', + chatStorageConnectionSetting: CHAT_STORAGE_CONNECTION_SETTING, + collectionName: COLLECTION_NAME }) app.http('PostUserResponse', { methods: ['POST'], diff --git a/samples/rag-aisearch/csharp-ooproc/FilePrompt.cs b/samples/rag-aisearch/csharp-ooproc/FilePrompt.cs index 56fcabde..7cda09af 100644 --- a/samples/rag-aisearch/csharp-ooproc/FilePrompt.cs +++ b/samples/rag-aisearch/csharp-ooproc/FilePrompt.cs @@ -30,24 +30,33 @@ public class SemanticSearchRequest public static async Task IngestFile( [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req) { + ArgumentNullException.ThrowIfNull(req); + using StreamReader reader = new(req.Body); string request = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(request)) + { + throw new ArgumentException("Request body is empty."); + } + EmbeddingsRequest? requestBody = JsonSerializer.Deserialize(request); - if (requestBody == null || requestBody.Url == null) + if (string.IsNullOrWhiteSpace(requestBody?.Url)) { throw new ArgumentException("Invalid request body. Make sure that you pass in {\"Url\": value } as the request body."); } - Uri uri = new(requestBody.Url); - string filename = Path.GetFileName(uri.AbsolutePath); + if (!Uri.TryCreate(requestBody.Url, UriKind.Absolute, out Uri? uri)) + { + throw new ArgumentException("Invalid Url format."); + } - IActionResult result = new OkObjectResult(new { status = HttpStatusCode.OK }); + string filename = Path.GetFileName(uri.AbsolutePath); return new EmbeddingsStoreOutputResponse { - HttpResponse = result, + HttpResponse = new OkObjectResult(new { status = HttpStatusCode.OK }), SearchableDocument = new SearchableDocument(filename) }; } diff --git a/samples/rag-cosmosdb/csharp-ooproc/FilePrompt.cs b/samples/rag-cosmosdb/csharp-ooproc/FilePrompt.cs index ed0ba589..87587626 100644 --- a/samples/rag-cosmosdb/csharp-ooproc/FilePrompt.cs +++ b/samples/rag-cosmosdb/csharp-ooproc/FilePrompt.cs @@ -30,24 +30,33 @@ public class SemanticSearchRequest public static async Task IngestFile( [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req) { + ArgumentNullException.ThrowIfNull(req); + using StreamReader reader = new(req.Body); string request = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(request)) + { + throw new ArgumentException("Request body is empty."); + } + EmbeddingsRequest? requestBody = JsonSerializer.Deserialize(request); - if (requestBody == null || requestBody.Url == null) + if (string.IsNullOrWhiteSpace(requestBody?.Url)) { throw new ArgumentException("Invalid request body. Make sure that you pass in {\"Url\": value } as the request body."); } - Uri uri = new(requestBody.Url); - string filename = Path.GetFileName(uri.AbsolutePath); + if (!Uri.TryCreate(requestBody.Url, UriKind.Absolute, out Uri? uri)) + { + throw new ArgumentException("Invalid Url format."); + } - IActionResult result = new OkObjectResult(new { status = HttpStatusCode.OK }); + string filename = Path.GetFileName(uri.AbsolutePath); return new EmbeddingsStoreOutputResponse { - HttpResponse = result, + HttpResponse = new OkObjectResult(new { status = HttpStatusCode.OK }), SearchableDocument = new SearchableDocument(filename) }; } diff --git a/samples/rag-kusto/csharp-ooproc/EmailPromptDemo.cs b/samples/rag-kusto/csharp-ooproc/EmailPromptDemo.cs index b9f1ec6a..972ad200 100644 --- a/samples/rag-kusto/csharp-ooproc/EmailPromptDemo.cs +++ b/samples/rag-kusto/csharp-ooproc/EmailPromptDemo.cs @@ -30,24 +30,33 @@ public class SemanticSearchRequest public async Task IngestEmail( [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req) { + ArgumentNullException.ThrowIfNull(req); + using StreamReader reader = new(req.Body); string request = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(request)) + { + throw new ArgumentException("Request body is empty."); + } + EmbeddingsRequest? requestBody = JsonSerializer.Deserialize(request); - if (requestBody == null || requestBody.Url == null) + if (string.IsNullOrWhiteSpace(requestBody?.Url)) { throw new ArgumentException("Invalid request body. Make sure that you pass in {\"Url\": value } as the request body."); } - Uri uri = new(requestBody.Url); - string filename = Path.GetFileName(uri.AbsolutePath); + if (!Uri.TryCreate(requestBody.Url, UriKind.Absolute, out Uri? uri)) + { + throw new ArgumentException("Invalid Url format."); + } - IActionResult result = new OkObjectResult(new { status = HttpStatusCode.OK }); + string filename = Path.GetFileName(uri.AbsolutePath); return new EmbeddingsStoreOutputResponse { - HttpResponse = result, + HttpResponse = new OkObjectResult(new { status = HttpStatusCode.OK }), SearchableDocument = new SearchableDocument(filename) }; } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9fbbeeb4..ae985155 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -23,7 +23,7 @@ 0 - 17 + 18 0 $(MajorVersion).$(MinorVersion).$(PatchVersion) alpha diff --git a/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantCreateRequest.cs b/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantCreateRequest.cs index fdb5991a..243d8bfd 100644 --- a/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantCreateRequest.cs +++ b/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantCreateRequest.cs @@ -47,5 +47,5 @@ public AssistantCreateRequest(string id, string? instructions) /// /// Table collection name for chat storage. /// - public string CollectionName { get; set; } = "SampleChatState"; + public string CollectionName { get; set; } = "ChatState"; } diff --git a/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantPostInputAttribute.cs b/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantPostInputAttribute.cs index 3753ddb4..fda594e7 100644 --- a/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantPostInputAttribute.cs +++ b/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantPostInputAttribute.cs @@ -33,4 +33,14 @@ public AssistantPostInputAttribute(string id, string UserMessage) /// Gets user message that user has entered for assistant to respond to. /// public string UserMessage { get; } + + /// + /// Configuration section name for the table settings for chat storage. + /// + public string? ChatStorageConnectionSetting { get; set; } + + /// + /// Table collection name for chat storage. + /// + public string CollectionName { get; set; } = "ChatState"; } diff --git a/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantQueryInputAttribute.cs b/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantQueryInputAttribute.cs index 74128d6e..029baf14 100644 --- a/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantQueryInputAttribute.cs +++ b/src/Functions.Worker.Extensions.OpenAI/Assistants/AssistantQueryInputAttribute.cs @@ -25,4 +25,14 @@ public AssistantQueryInputAttribute(string id) /// The timestamp should be in ISO 8601 format - for example, 2023-08-01T00:00:00Z. /// public string TimestampUtc { get; set; } = string.Empty; + + /// + /// Configuration section name for the table settings for chat storage. + /// + public string? ChatStorageConnectionSetting { get; set; } + + /// + /// Table collection name for chat storage. + /// + public string CollectionName { get; set; } = "ChatState"; } diff --git a/src/Functions.Worker.Extensions.OpenAI/Functions.Worker.Extensions.OpenAI.csproj b/src/Functions.Worker.Extensions.OpenAI/Functions.Worker.Extensions.OpenAI.csproj index eb465162..ee76b15d 100644 --- a/src/Functions.Worker.Extensions.OpenAI/Functions.Worker.Extensions.OpenAI.csproj +++ b/src/Functions.Worker.Extensions.OpenAI/Functions.Worker.Extensions.OpenAI.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantBindingConverter.cs b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantBindingConverter.cs index 05f634a5..8ec12b63 100644 --- a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantBindingConverter.cs +++ b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantBindingConverter.cs @@ -33,13 +33,7 @@ public Task ConvertAsync( AssistantQueryAttribute input, CancellationToken cancellationToken) { - string timestampString = Uri.UnescapeDataString(input.TimestampUtc); - if (!DateTime.TryParse(timestampString, out DateTime timestamp)) - { - throw new ArgumentException($"Invalid timestamp '{timestampString}'"); - } - - return this.assistantService.GetStateAsync(input.Id, timestamp, cancellationToken); + return this.assistantService.GetStateAsync(input, cancellationToken); } async Task IAsyncConverter.ConvertAsync( diff --git a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantCreateAttribute.cs b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantCreateAttribute.cs index fdcbffb1..2a9dcc5e 100644 --- a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantCreateAttribute.cs +++ b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantCreateAttribute.cs @@ -68,5 +68,5 @@ public AssistantCreateRequest(string id, string? instructions) /// /// Table collection name for chat storage. /// - public string CollectionName { get; set; } = "SampleChatState"; + public string CollectionName { get; set; } = "ChatState"; } \ No newline at end of file diff --git a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantPostAttribute.cs b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantPostAttribute.cs index 6acc8e41..56ed35fa 100644 --- a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantPostAttribute.cs +++ b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantPostAttribute.cs @@ -35,4 +35,15 @@ public AssistantPostAttribute(string id, string userMessage) /// [AutoResolve] public string UserMessage { get; } + + /// + /// Configuration section name for the table settings for chat storage. + /// + public string? ChatStorageConnectionSetting { get; set; } + + /// + /// Table collection name for chat storage. + /// + [AutoResolve] + public string CollectionName { get; set; } = "ChatState"; } \ No newline at end of file diff --git a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantQueryAttribute.cs b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantQueryAttribute.cs index 28c0d3ff..9d3a8e31 100644 --- a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantQueryAttribute.cs +++ b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantQueryAttribute.cs @@ -26,4 +26,15 @@ public AssistantQueryAttribute(string id) /// [AutoResolve] public string TimestampUtc { get; set; } = string.Empty; + + /// + /// Configuration section name for the table settings for chat storage. + /// + public string? ChatStorageConnectionSetting { get; set; } + + /// + /// Table collection name for chat storage. + /// + [AutoResolve] + public string CollectionName { get; set; } = "ChatState"; } \ No newline at end of file diff --git a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantService.cs b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantService.cs index 3c783459..e31e8a3b 100644 --- a/src/WebJobs.Extensions.OpenAI/Assistants/AssistantService.cs +++ b/src/WebJobs.Extensions.OpenAI/Assistants/AssistantService.cs @@ -14,7 +14,7 @@ namespace Microsoft.Azure.WebJobs.Extensions.OpenAI.Assistants; public interface IAssistantService { Task CreateAssistantAsync(AssistantCreateRequest request, CancellationToken cancellationToken); - Task GetStateAsync(string id, DateTime since, CancellationToken cancellationToken); + Task GetStateAsync(AssistantQueryAttribute assistantQuery, CancellationToken cancellationToken); Task PostMessageAsync(AssistantPostAttribute attribute, CancellationToken cancellationToken); } @@ -63,18 +63,13 @@ public async Task CreateAssistantAsync(AssistantCreateRequest request, Cancellat request.Id, request.Instructions ?? "(none)"); - this.CreateTableClient(request); - - if (this.tableClient is null) - { - throw new ArgumentNullException(nameof(this.tableClient)); - } + TableClient tableClient = this.GetOrCreateTableClient(request.ChatStorageConnectionSetting, request.CollectionName); // Create the table if it doesn't exist - await this.tableClient.CreateIfNotExistsAsync(); + await tableClient.CreateIfNotExistsAsync(); // Check to see if the assistant has already been initialized - AsyncPageable queryResultsFilter = this.tableClient.QueryAsync( + AsyncPageable queryResultsFilter = tableClient.QueryAsync( filter: $"PartitionKey eq '{request.Id}'", cancellationToken: cancellationToken); @@ -90,7 +85,7 @@ async Task DeleteBatch() "Deleting {Count} record(s) for assistant '{Id}'.", deleteBatch.Count, request.Id); - await this.tableClient.SubmitTransactionAsync(deleteBatch); + await tableClient.SubmitTransactionAsync(deleteBatch); deleteBatch.Clear(); } } @@ -133,18 +128,27 @@ async Task DeleteBatch() batch.Add(new TableTransactionAction(TableTransactionActionType.Add, assistantStateEntity)); // Add the batch of table transaction actions to the table - await this.tableClient.SubmitTransactionAsync(batch); + await tableClient.SubmitTransactionAsync(batch); } - public async Task GetStateAsync(string id, DateTime after, CancellationToken cancellationToken) + public async Task GetStateAsync(AssistantQueryAttribute assistantQuery, CancellationToken cancellationToken) { - DateTime afterUtc = after.ToUniversalTime(); + string id = assistantQuery.Id; + string timestampString = Uri.UnescapeDataString(assistantQuery.TimestampUtc); + if (!DateTime.TryParse(timestampString, out DateTime timestamp)) + { + throw new ArgumentException($"Invalid timestamp '{assistantQuery.TimestampUtc}'"); + } + + DateTime afterUtc = timestamp.ToUniversalTime(); this.logger.LogInformation( "Reading state for assistant entity '{Id}' and getting chat messages after {Timestamp}", id, afterUtc.ToString("o")); - InternalChatState? chatState = await this.LoadChatStateAsync(id, cancellationToken); + TableClient tableClient = this.GetOrCreateTableClient(assistantQuery.ChatStorageConnectionSetting, assistantQuery.CollectionName); + + InternalChatState? chatState = await this.LoadChatStateAsync(id, tableClient, cancellationToken); if (chatState is null) { this.logger.LogWarning("No assistant exists with ID = '{Id}'", id); @@ -184,14 +188,11 @@ public async Task PostMessageAsync(AssistantPostAttribute attrib throw new ArgumentException("The assistant must have a user message", nameof(attribute)); } - if (this.tableClient is null) - { - throw new ArgumentException("The assistant must be initialized first using CreateAssistantAsync", nameof(this.tableClient)); - } - this.logger.LogInformation("Posting message to assistant entity '{Id}'", attribute.Id); - InternalChatState? chatState = await this.LoadChatStateAsync(attribute.Id, cancellationToken); + TableClient tableClient = this.GetOrCreateTableClient(attribute.ChatStorageConnectionSetting, attribute.CollectionName); + + InternalChatState? chatState = await this.LoadChatStateAsync(attribute.Id, tableClient, cancellationToken); // Check if assistant has been deactivated if (chatState is null || !chatState.Metadata.Exists) @@ -360,7 +361,7 @@ public async Task PostMessageAsync(AssistantPostAttribute attrib batch.Add(new TableTransactionAction(TableTransactionActionType.UpdateMerge, chatState.Metadata)); // Add the batch of table transaction actions to the table - await this.tableClient.SubmitTransactionAsync(batch, cancellationToken); + await tableClient.SubmitTransactionAsync(batch, cancellationToken); // return the latest assistant message in the chat state List filteredChatMessages = chatState.Messages @@ -385,15 +386,10 @@ public async Task PostMessageAsync(AssistantPostAttribute attrib return state; } - async Task LoadChatStateAsync(string id, CancellationToken cancellationToken) + async Task LoadChatStateAsync(string id, TableClient tableClient, CancellationToken cancellationToken) { - if (this.tableClient is null) - { - throw new ArgumentException("The assistant must be initialized first using CreateAssistantAsync", nameof(this.tableClient)); - } - // Check to see if any entity exists with partition id - AsyncPageable itemsWithPartitionKey = this.tableClient.QueryAsync( + AsyncPageable itemsWithPartitionKey = tableClient.QueryAsync( filter: $"PartitionKey eq '{id}'", cancellationToken: cancellationToken); @@ -451,9 +447,14 @@ static IEnumerable ToOpenAIChatRequestMessages(IEnumerable(connectionStringName); this.logger.LogInformation("using connection string for table service client"); @@ -489,7 +489,9 @@ void CreateTableClient(AssistantCreateRequest request) this.tableServiceClient = new TableServiceClient(connectionString); } - this.logger.LogInformation("Using {CollectionName} for table storage collection name", request.CollectionName); - this.tableClient = this.tableServiceClient.GetTableClient(request.CollectionName); + this.logger.LogInformation("Using {CollectionName} for table storage collection name", collectionName); + this.tableClient = this.tableServiceClient.GetTableClient(collectionName); + + return this.tableClient; } } \ No newline at end of file diff --git a/src/WebJobs.Extensions.OpenAI/Embeddings/EmbeddingsStoreConverter.cs b/src/WebJobs.Extensions.OpenAI/Embeddings/EmbeddingsStoreConverter.cs index ee76230a..f51285d5 100644 --- a/src/WebJobs.Extensions.OpenAI/Embeddings/EmbeddingsStoreConverter.cs +++ b/src/WebJobs.Extensions.OpenAI/Embeddings/EmbeddingsStoreConverter.cs @@ -48,9 +48,14 @@ public Task> ConvertAsync(EmbeddingsStoreAtt // Called by the host when processing binding requests from out-of-process workers. internal SearchableDocument ToSearchableDocument(string? json) { + if (json is null) + { + throw new ArgumentNullException(nameof(json)); + } this.logger.LogDebug("Creating searchable document from JSON string: {Text}", json); - SearchableDocument document = JsonSerializer.Deserialize(json, options); - return document ?? throw new ArgumentException("Invalid search request."); + SearchableDocument document = JsonSerializer.Deserialize(json, options) + ?? throw new ArgumentException("Invalid search request."); + return document; } sealed class SemanticDocumentCollector : IAsyncCollector diff --git a/src/WebJobs.Extensions.OpenAI/Search/SearchableDocumentJsonConverter.cs b/src/WebJobs.Extensions.OpenAI/Search/SearchableDocumentJsonConverter.cs index 2526a6c0..0bbf9b55 100644 --- a/src/WebJobs.Extensions.OpenAI/Search/SearchableDocumentJsonConverter.cs +++ b/src/WebJobs.Extensions.OpenAI/Search/SearchableDocumentJsonConverter.cs @@ -49,18 +49,18 @@ public override SearchableDocument Read(ref Utf8JsonReader reader, Type typeToCo { if (connectionInfoItem.NameEquals("connectionName"u8)) { - connectionName = connectionInfoItem.Value.GetString(); + connectionName = connectionInfoItem.Value.GetString() ?? string.Empty; } if (connectionInfoItem.NameEquals("collectionName"u8)) { - collectionName = connectionInfoItem.Value.GetString(); + collectionName = connectionInfoItem.Value.GetString() ?? string.Empty; } } } if (item.NameEquals("title"u8)) { - title = item.Value.GetString(); + title = item.Value.GetString() ?? string.Empty; } } SearchableDocument searchableDocument = new SearchableDocument(title) diff --git a/src/WebJobs.Extensions.OpenAI/WebJobs.Extensions.OpenAI.csproj b/src/WebJobs.Extensions.OpenAI/WebJobs.Extensions.OpenAI.csproj index 522080b3..65ecfa87 100644 --- a/src/WebJobs.Extensions.OpenAI/WebJobs.Extensions.OpenAI.csproj +++ b/src/WebJobs.Extensions.OpenAI/WebJobs.Extensions.OpenAI.csproj @@ -6,12 +6,12 @@ - - + + - + - +