Skip to content

Commit

Permalink
Adding file support (#59)
Browse files Browse the repository at this point in the history
* Added file support to client library.

* Updating C# sample to handle multipart requests + spec.

* partial python

* Finish non-streaming support.

* added streaming

* python feedback

* delete env

* Update samples/backend/csharp/Controllers/ChatController.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update samples/backend/csharp/Controllers/ChatController.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update samples/backend/csharp/Controllers/ChatController.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update samples/backend/csharp/Controllers/ChatController.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update samples/backend/csharp/Controllers/ChatController.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update samples/backend/csharp/Controllers/ChatController.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update samples/backend/csharp/Controllers/ChatController.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Bump System.Text.Json from 8.0.3 to 8.0.4 in /samples/backend/csharp (#67)

Bumps System.Text.Json from 8.0.3 to 8.0.4.

---
updated-dependencies:
- dependency-name: System.Text.Json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Feedback

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 13, 2024
1 parent 1138f79 commit 83324eb
Show file tree
Hide file tree
Showing 28 changed files with 1,147 additions and 217 deletions.
4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"image": "mcr.microsoft.com/devcontainers/base:jammy",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {},
"ghcr.io/devcontainers/features/node:1": {}
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers/features/python": "3.12",
"ghcr.io/devcontainers/features/azure-cli": "latest"
}

// Features to add to the dev container. More info: https://containers.dev/features.
Expand Down
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* @dargilco
* @glecaros
* @glecaros
* @rohit-ganguly
76 changes: 75 additions & 1 deletion samples/backend/csharp/Controllers/ChatController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

using Backend.Interfaces;
using Backend.Model;
using System.Text.RegularExpressions;

namespace Backend.Controllers;

[ApiController, Route("api/[controller]")]
public class ChatController : ControllerBase
public partial class ChatController : ControllerBase
{
private readonly ISemanticKernelApp _semanticKernelApp;

Expand All @@ -19,6 +20,79 @@ public ChatController(ISemanticKernelApp semanticKernelApp)
_semanticKernelApp = semanticKernelApp;
}

[GeneratedRegex(@"messages\[(\d+)\]\.files\[(\d+)\]")]
private static partial Regex MessageFilesRegex();

private (int MessageIndex, int FileIndex, IFormFile File) GetPosition(IFormFile formFile)
{
var match = MessageFilesRegex().Match(formFile.Name);
if (match.Success && int.TryParse(match.Groups[1].ValueSpan, out var messageIndex) && int.TryParse(match.Groups[2].ValueSpan, out var fileIndex))
{
return (messageIndex, fileIndex, formFile);
}

throw new ArgumentException("Malformed multipart request: Invalid file name.");
}

private async Task<AIChatRequest> RequestFromMultipart(IFormFileCollection formFiles)
{
using var jsonFileStream = formFiles
.First(f => f.Name == "json")
.OpenReadStream();
if (jsonFileStream is null)
{
throw new Exception("Malformed multipart request: Missing json part.");
}

var request = await JsonSerializer.DeserializeAsync<AIChatRequest>(jsonFileStream) ??
throw new Exception("Malformed multipart request: Invalid json part.");
foreach (var (messageIndex, fileIndex, file) in formFiles.Where(f => f.Name != "json").Select(GetPosition).OrderBy(p => p.MessageIndex).ThenBy(p => p.FileIndex))
{
if (request.Messages.Count <= messageIndex)
{
throw new Exception("Malformed multipart request: Invalid message index.");
}

var message = request.Messages[messageIndex];
message.Files ??= new List<AIChatFile>();
if (message.Files.Count != fileIndex)
{
throw new Exception("Malformed multipart request: Invalid file index.");
}

using var fileStream = file.OpenReadStream();
var fileData = await BinaryData.FromStreamAsync(fileStream);
message.Files.Add(new AIChatFile
{
ContentType = file.ContentType,
Data = fileData
});

}
return request;
}

[HttpPost]
[Consumes("multipart/form-data")]
public async Task<IActionResult> ProcessMessage(IFormFileCollection files)
{
try
{
var request = await RequestFromMultipart(files);
var session = request.SessionState switch
{
Guid sessionId => await _semanticKernelApp.GetSession(sessionId),
_ => await _semanticKernelApp.CreateSession(Guid.NewGuid())
};

return Ok(await session.ProcessRequest(request));
}
catch (Exception e)
{
return BadRequest(e.Message);
}
}

[HttpPost]
[Consumes("application/json")]
public async Task<IActionResult> ProcessMessage(AIChatRequest request)
Expand Down
15 changes: 15 additions & 0 deletions samples/backend/csharp/Model/AIChatFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Backend.Model;

public struct AIChatFile
{
[JsonPropertyName("contentType")]
public string ContentType { get; set; }

[JsonPropertyName("data")]
public BinaryData Data { get; set; }
}
3 changes: 3 additions & 0 deletions samples/backend/csharp/Model/AIChatMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ public struct AIChatMessage

[JsonPropertyName("context")]
public BinaryData? Context { get; set; }

[JsonPropertyName("files")]
public IList<AIChatFile>? Files { get; set; }
}

13 changes: 7 additions & 6 deletions samples/backend/csharp/Services/SemanticKernelApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ internal class SemanticKernelSession : ISemanticKernelSession
{
private readonly Kernel _kernel;
private readonly IStateStore<string> _stateStore;
private readonly KernelFunction _chatFunction;

public Guid Id { get; private set; }

internal SemanticKernelSession(Kernel kernel, IStateStore<string> stateStore, Guid sessionId)
{
_kernel = kernel;
_stateStore = stateStore;
_chatFunction = _kernel.CreateFunctionFromPrompt(prompt);
Id = sessionId;
}

Expand All @@ -66,16 +68,16 @@ ChatBot can have a conversation with you about any topic.
ChatBot:";

public async Task<AIChatCompletion> ProcessRequest(AIChatRequest message)
{
var chatFunction = _kernel.CreateFunctionFromPrompt(prompt);
{
var userInput = message.Messages.Last();
string history = await _stateStore.GetStateAsync(Id) ?? "";
/* TODO: Add support for text+image content */
var arguments = new KernelArguments()
{
["history"] = history,
["userInput"] = userInput.Content,
};
var botResponse = await chatFunction.InvokeAsync(_kernel, arguments);
var botResponse = await _chatFunction.InvokeAsync(_kernel, arguments);
var updatedHistory = $"{history}\nUser: {userInput.Content}\nChatBot: {botResponse}";
await _stateStore.SetStateAsync(Id, updatedHistory);
return new AIChatCompletion(Message: new AIChatMessage
Expand All @@ -89,16 +91,15 @@ public async Task<AIChatCompletion> ProcessRequest(AIChatRequest message)
}

public async IAsyncEnumerable<AIChatCompletionDelta> ProcessStreamingRequest(AIChatRequest message)
{
var chatFunction = _kernel.CreateFunctionFromPrompt(prompt);
{
var userInput = message.Messages.Last();
string history = await _stateStore.GetStateAsync(Id) ?? "";
var arguments = new KernelArguments()
{
["history"] = history,
["userInput"] = userInput.Content,
};
var streamedBotResponse = chatFunction.InvokeStreamingAsync(_kernel, arguments);
var streamedBotResponse = _chatFunction.InvokeStreamingAsync(_kernel, arguments);
StringBuilder response = new();
await foreach (var botResponse in streamedBotResponse)
{
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions samples/backend/js/expressjs/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.env
dist
Loading

0 comments on commit 83324eb

Please sign in to comment.