Skip to content

Add embeddings support and further improve samples #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion OpenAI-Extension.sln
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ooproc", "ooproc", "{E6B8C0
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Worker.Extensions.OpenAI", "src\Functions.Worker.Extensions.OpenAI\Functions.Worker.Extensions.OpenAI.csproj", "{EBFED369-EBBB-49F8-B2F2-236AF3063271}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpIsolatedSamples", "samples\other\dotnet\csharp-ooproc\CSharpIsolatedSamples\CSharpIsolatedSamples.csproj", "{537FD9B3-1288-461B-BEFD-8DF323A50BF1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpIsolatedSamples", "samples\other\dotnet\csharp-ooproc\CSharpIsolatedSamples\CSharpIsolatedSamples.csproj", "{537FD9B3-1288-461B-BEFD-8DF323A50BF1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
Expand All @@ -7,11 +7,9 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.19.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.15.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.0.*" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.15.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Functions.Worker.Extensions.OpenAI\Functions.Worker.Extensions.OpenAI.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Functions.Worker.Extensions.OpenAI.Chat;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;

namespace CSharpIsolatedSamples;

// IMPORTANT: This sample unfortunately doesn't work. The chat bot will always enter a "Failed" state after creation.
// Tracking issue: https://github.com/cgillum/azure-functions-openai-extension/issues/21

public class ChatBots
{
public record CreateRequest(string Instructions);

[Function(nameof(CreateChatBot))]
public ChatBotCreateResult CreateChatBot(
[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "chats/{chatId}")] HttpRequest httpReq,
[FromBody] CreateRequest createReq,
string chatId)
{
var responseJson = new { chatId };
return new ChatBotCreateResult(
new ChatBotCreateRequest(chatId, createReq.Instructions),
new ObjectResult(responseJson) { StatusCode = 202 });
}

public class ChatBotCreateResult
{
public ChatBotCreateResult(ChatBotCreateRequest createChatBotRequest, IActionResult httpResponse)
{
this.CreateRequest = createChatBotRequest;
this.HttpResponse = httpResponse;
}

[ChatBotCreateOutput]
public ChatBotCreateRequest CreateRequest { get; set; }
public IActionResult HttpResponse { get; set; }
}

[Function(nameof(GetChatState))]
public ChatBotState GetChatState(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "chats/{chatId}")] HttpRequest req,
string chatId,
[ChatBotQueryInput("{chatId}", TimestampUtc = "{Query.timestampUTC}")] ChatBotState state)
{
return state;
}

[Function(nameof(PostUserResponse))]
public async Task<ChatBotPostResult> PostUserResponse(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "chats/{chatId}")] HttpRequest req,
string chatId)
{
// Get the message from the raw request body
using StreamReader reader = new(req.Body);
string userMessage = await reader.ReadToEndAsync();

if (string.IsNullOrEmpty(userMessage))
{
return new ChatBotPostResult(null, new BadRequestObjectResult(new { message = "Request body is empty" }));
}

return new ChatBotPostResult(
new ChatBotPostRequest(userMessage),
new AcceptedResult());
}

public class ChatBotPostResult
{
public ChatBotPostResult(ChatBotPostRequest? postRequest, IActionResult httpResponse)
{
this.PostRequest = postRequest;
this.HttpResponse = httpResponse;
}

[ChatBotPostOutput("{chatId}")]
public ChatBotPostRequest? PostRequest { get; set; }
public IActionResult HttpResponse { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Functions.Worker.Extensions.OpenAI;
using Functions.Worker.Extensions.OpenAI.Embeddings;
using Functions.Worker.Extensions.OpenAI.Search;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;

namespace CSharpIsolatedSamples;

class DocumentSearch
{
public record EmbeddingsRequest(string FilePath);
public record SemanticSearchRequest(string Prompt);

// REVIEW: There are several assumptions about how the Embeddings binding and the SemanticSearch bindings
// work together. We should consider creating a higher-level of abstraction for this.
[Function(nameof(Ingest))]
public IngestResult Ingest(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
[FromBody] EmbeddingsRequest input,
[EmbeddingsInput("{FilePath}", InputType.FilePath, Model = "text-embedding-ada-002-private")] EmbeddingsContext embeddings)
{
string title = Path.GetFileNameWithoutExtension(input.FilePath);
return new IngestResult(
new SearchableDocument(title, embeddings),
new OkObjectResult(new { status = "success", title, chunks = embeddings.Count }));
}

public class IngestResult
{
public IngestResult(SearchableDocument? doc, IActionResult httpResponse)
{
this.Document = doc;
this.HttpResponse = httpResponse;
}

[SemanticSearchOutput("KustoConnectionString", "Documents")]
public SearchableDocument? Document { get; set; }
public IActionResult HttpResponse { get; set; }
}

[Function(nameof(Prompt))]
public static IActionResult Prompt(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
[FromBody] SemanticSearchRequest unused,
[SemanticSearchInput("KustoConnectionString", "Documents", Query = "{Prompt}", EmbeddingsModel = "text-embedding-ada-002-private", ChatModel = "gpt-35-turbo")] SemanticSearchContext result)
{
return new ContentResult { Content = result.Response, ContentType = "text/plain" };
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
using Microsoft.Extensions.Hosting;


var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureFunctionsWebApplication()
.Build();

host.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"profiles": {
"CSharpIsolatedSamples": {
"commandName": "Project",
"commandLineArgs": "--port 7179",
"launchBrowser": false
}
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,60 @@
using Functions.Worker.Extensions.OpenAI;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using OpenAI.ObjectModels.ResponseModels;
using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;

namespace CSharpIsolatedSamples;

/// <summary>
/// These samples show how to use the OpenAI Completions APIs. For more details on the Completions APIs, see
/// https://platform.openai.com/docs/guides/completion.
/// </summary>
public static class TextCompletions
public class TextCompletions
{
readonly ILogger logger;

public TextCompletions(ILogger<TextCompletions> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

/// <summary>
/// This sample demonstrates the "templating" pattern, where the function takes a parameter
/// and embeds it into a text prompt, which is then sent to the OpenAI completions API.
/// </summary>
[Function(nameof(WhoIs))]
public static string WhoIs(
[HttpTrigger(AuthorizationLevel.Function, Route = "whois/{name}")] HttpRequestData req,
public IActionResult WhoIs(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "whois/{name}")] HttpRequest req,
[TextCompletionInput("Who is {name}?")] CompletionCreateResponse response)
{
return response.Choices[0].Text;
return new ContentResult
{
Content = response.Choices[0].Text.Trim(),
ContentType = "text/plain; charset=utf-8",
};
}

/// <summary>
/// This sample takes a prompt as input, sends it directly to the OpenAI completions API, and results the
/// response as the output.
/// </summary>
[Function(nameof(GenericCompletion))]
public static IActionResult GenericCompletion(
[HttpTrigger(AuthorizationLevel.Function, "post")] PromptPayload payload,
[TextCompletionInput("{Prompt}", Model = "text-davinci-003")] CompletionCreateResponse response,
ILogger log)
public IActionResult GenericCompletion(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest request,
[FromBody] PromptPayload payload,
[TextCompletionInput("{Prompt}", Model = "text-davinci-003")] CompletionCreateResponse response)
{
if (!response.Successful)
{
Error error = response.Error ?? new Error() { MessageObject = "OpenAI returned an unspecified error" };
return new ObjectResult(error) { StatusCode = 500 };
}

log.LogInformation("Prompt = {prompt}, Response = {response}", payload.Prompt, response);
string text = response.Choices[0].Text;
this.logger.LogInformation("Prompt = {prompt}, Response = {response}", payload.Prompt, response);
string text = response.Choices[0].Text.Trim();
return new OkObjectResult(text);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Functions.Worker.Extensions.OpenAI;
using Functions.Worker.Extensions.OpenAI.Embeddings;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;

namespace CSharpIsolatedSamples;

/// <summary>
/// Examples of working with OpenAI embeddings.
/// </summary>
public class TextEmbeddings
{
readonly ILogger<TextEmbeddings> logger;

public TextEmbeddings(ILogger<TextEmbeddings> logger)
{
this.logger = logger;
}

public record EmbeddingsRequest(string RawText, string FilePath);

/// <summary>
/// Example showing how to use the <see cref="EmbeddingsInputAttribute"/> input binding to generate embeddings
/// for a raw text string.
/// </summary>
[Function(nameof(GenerateEmbeddings_Http_Request))]
public IActionResult GenerateEmbeddings_Http_Request(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "embeddings")] HttpRequest req,
[FromBody] EmbeddingsRequest input,
[EmbeddingsInput("{RawText}", InputType.RawText)] EmbeddingsContext embeddings)
{
this.logger.LogInformation(
"Received {count} embedding(s) for input text containing {length} characters.",
embeddings.Count,
input.RawText.Length);

// TODO: Store the embeddings into a database or other storage.

return new OkObjectResult($"Generated {embeddings.Count} chunk(s) from source text");
}

/// <summary>
/// Example showing how to use the <see cref="EmbeddingsInputAttribute"/> input binding to generate embeddings
/// for text contained in a file on the file system.
/// </summary>
[Function(nameof(GetEmbeddings_Http_FilePath))]
public IActionResult GetEmbeddings_Http_FilePath(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "embeddings-from-file")] HttpRequest req,
[FromBody] EmbeddingsRequest input,
[EmbeddingsInput("{FilePath}", InputType.FilePath, MaxChunkLength = 512)] EmbeddingsContext embeddings)
{
this.logger.LogInformation(
"Received {count} embedding(s) for input file '{path}'.",
embeddings.Response.Data.Count,
input.FilePath);

// TODO: Store the embeddings into a database or other storage.

return new OkObjectResult($"Generated {embeddings.Count} chunk(s) from source file");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# For more info on HTTP files go to https://aka.ms/vs/httpfile

### Demo 1.1: WhoIs API

POST http://localhost:7179/api/whois/pikachu


### Demo 1.2: Generic Text Completion API

POST http://localhost:7179/api/GenericCompletion
Content-Type: application/json

{"Prompt": "Once upon a time"}


### Demo 2.1: Embeddings from HTTP request
POST http://localhost:7179/api/embeddings
Content-Type: application/json

{"RawText": "Hello world"}


### Demo 2.2: Embeddings from file (NOTE: replace path/to/file.txt with a real file path)
POST http://localhost:7179/api/embeddings-from-file
Content-Type: application/json

{"FilePath": "path/to/file.txt"}


## Demo 3: Chat bots (NOTE: Currently broken: https://github.com/cgillum/azure-functions-openai-extension/issues/21)

### Create a chat bot
PUT http://localhost:7179/api/chats/test123
Content-Type: application/json

{
"instructions": "You are a helpful chatbot. In all your English responses, speak as if you are Shakespeare."
}


### Send the first message to the chatbot
POST http://localhost:7179/api/chats/test123
Content-Type: text/plain

Who won SuperBowl XLVIII in 2014?


### Get the chatbot's response
GET http://localhost:7179/api/chats/test123?timestampUTC=2023-08-10T07:51:10Z


### Send the second message to the chatbot
POST http://localhost:7179/api/chats/test123
Content-Type: text/plain

Amazing! Do you know who performed the halftime show?
Loading