Skip to content

.Net: Remote Chat Completion Agent Demo #11554

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

Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5a12075
adds remote chat completion agent demo
tommasodotNET Apr 14, 2025
ff62810
adds readme
tommasodotNET Apr 14, 2025
58e3945
updates aspire app host version
tommasodotNET Apr 14, 2025
87234b7
remove blank line
tommasodotNET Apr 14, 2025
1154f23
fixes missing } in readme
tommasodotNET Apr 15, 2025
244ebdd
Merge branch 'main' into demo/remote-chat-completion-agent
tommasodotNET Apr 17, 2025
64d2d32
set dotnet target to 8.0
tommasodotNET Apr 17, 2025
16c02a6
Merge branch 'demo/remote-chat-completion-agent' of github.com:tommas…
tommasodotNET Apr 17, 2025
44ac51c
Merge branch 'main' into demo/remote-chat-completion-agent
crickman Apr 23, 2025
de38e06
Clean-up for build
crickman Apr 23, 2025
af3fb3e
Typos
crickman Apr 23, 2025
96a21f1
Typo syntax
crickman Apr 23, 2025
cb477aa
Readme
crickman Apr 23, 2025
5433445
Encoding
crickman Apr 23, 2025
93e5a6a
Encoding
crickman Apr 23, 2025
14d6e14
More encoding
crickman Apr 23, 2025
60737a5
Fix readme links
crickman Apr 23, 2025
f91f1da
Suppress comments for autogenerated code
crickman Apr 23, 2025
ffdb417
Fix project
crickman Apr 23, 2025
916f50b
adds images to doc
tommasodotNET Apr 23, 2025
203c54e
Merge branch 'demo/remote-chat-completion-agent' of github.com:tommas…
tommasodotNET Apr 23, 2025
80c6167
Merge branch 'main' into demo/remote-chat-completion-agent
crickman Apr 23, 2025
467e4a8
Fix link
crickman Apr 23, 2025
9ccbc80
Merge branch 'demo/remote-chat-completion-agent' of https://github.co…
crickman Apr 23, 2025
025bd95
Merge branch 'main' into demo/remote-chat-completion-agent
crickman May 14, 2025
4ee0705
Update _typos.toml
crickman May 21, 2025
acc056f
Merge branch 'main' into demo/remote-chat-completion-agent
crickman May 21, 2025
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
3 changes: 2 additions & 1 deletion .github/_typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ extend-exclude = [
"SK-dotnet.sln.DotSettings",
"**/azure_ai_search_hotel_samples/README.md",
"**/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/Program.cs",
"**/Demos/ProcessFrameworkWithAspire/**/*.http"
"**/Demos/ProcessFrameworkWithAspire/**/*.http",
"**/Demos/RemoteChatCompletionAgent/RemoteChatCompletionAgentDemo.GroupChat/Program.cs"
]

[default.extend-words]
Expand Down
2,025 changes: 2,025 additions & 0 deletions dotnet/SK-dotnet.sln

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions dotnet/samples/Demos/RemoteChatCompletionAgent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Remote Chat Completion Agent Test

This repo shows how we could implement a [`RemoteChatCompletionAgent`](./RemoteChatCompletionAgentDemo.GroupChat/RemoteChatCompletionAgent.cs) that can be used to interact with a remote Semantic Kernel `ChatCompletionAgent`.

The new type [`RemoteChatCompletionAgent`](./RemoteChatCompletionAgentDemo.GroupChat/RemoteChatCompletionAgent.cs) implements the `ChatHistoryKernelAgent` methods using a custom HTTP client to send requests to an API that hosts the `ChatCompletionAgent` functionality. This allows us to use the same interface as the local `ChatCompletionAgent`, but with the added benefit of being able to interact with a remote service.

## Benefits of having a remote agent
- **Polyglot Support**: The remote agent can be implemented with different programming languages and frameworks, allowing for better integration and support for different platforms.
- **Reusability**: The remote agent can be reused across different applications and services, allowing for better code reuse and maintainability.
- **Scalability**: The remote agent can be hosted on a powerful server, allowing for more complex computations and larger models.
- **Resource Management**: The remote agent can be managed and monitored more easily, allowing for better resource allocation and usage tracking.
- **Security**: The remote agent can be secured and monitored more easily, allowing for better protection of sensitive data and computations.
- **Cost Efficiency**: The remote agent can be hosted on a pay-as-you-go basis, allowing for better cost management and resource allocation.
- **Flexibility**: The remote agent can be updated and maintained more easily, allowing for better support for new features and improvements.

## Details about the sample

I am using an `AgentGroupChat` to show how to use different remote agents togethere in the same chat. It's a useful use case. Since the`RemoteChatCompletionAgent` extends the type `ChatHistoryKernelAgent`, it can be used in the same way as the local agent.

In this simple Group Chat, we have two agents:
- `TranslatorAgent`: this agent translates the text to English. [Program.cs](./RemoteChatCompletionAgentDemo.TranslatorAgent/Program.cs)
- `SummaryAgent`: this agent summarize the text. [Program.cs](./RemoteChatCompletionAgentDemo.SummaryAgent/Program.cs)

The Group Chat will call the `TranslatorAgent` first, and then the `SummaryAgent`.

I am also using [.NET Aspire]https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) to have all the different microservices start and run together. I am also leveraging the .NET Service Discovery to have the correct endpoints for each agent.

.NET Aspire is not required. It just make it easier to run the sample and monitoring all the different microservices. You can run the `RemoteChatCompletionAgent` in any .NET application.

## How to run the example

1. Configure the OpenAI integration for .NET Aspire according to the [documentation](https://learn.microsoft.com/en-us/dotnet/aspire/azureai/azureai-openai-integration?tabs=dotnet-cli#connect-to-an-existing-azure-openai-service).

Note that you can use either DefaultAzureCredentials or API Keys for authentication.

Using DefaultAzureCredentials:

```json
{
"ConnectionStrings": {
"openAiConnectionName": "https://{account_name}.openai.azure.com/"
}
}
```

Using API Keys:

```json
{
"ConnectionStrings": {
"openAiConnectionName": "Endpoint=https://{account_name}.openai.azure.com/;Key={api_key};"
},
}
```

2. Run the sample

```bash
cd RemoteAgentTest.AppHost
dotnet run
```

3. Invoke the AgentGroupChat via http:

```
http://localhost:{PORT}/remote-group-chat
```

You will see Traces for the request, showing that the Group Chat is actually calling each remote agent as intructed.

![traces](./docs/Traces.png)

The output of the Group Chat can be seen in the `groupChat` service logs:

![output](./docs/output.png)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.

var builder = DistributedApplication.CreateBuilder(args);

var openai = builder.AddConnectionString("openAiConnectionName");

var translatorAgent = builder.AddProject<Projects.RemoteChatCompletionAgentDemo_TranslatorAgent>("translatoragent")
.WithReference(openai);

var summaryAgent = builder.AddProject<Projects.RemoteChatCompletionAgentDemo_SummaryAgent>("summaryagent")
.WithReference(openai);

var remoteChatCompletionAgent = builder.AddProject<Projects.RemoteChatCompletionAgentDemo_GroupChat>("groupChat")
.WithReference(openai)
.WithReference(translatorAgent)
.WithReference(summaryAgent)
.WithHttpCommand("/remote-group-chat", "Invoke Chat",
commandOptions: new()
{
Method = HttpMethod.Get
}
);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>4e4d92b3-c7a6-4269-9119-5831f36a8063</UserSecretsId>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.CognitiveServices" />
</ItemGroup>

<ItemGroup>
<ProjectReference
Include="..\RemoteChatCompletionAgentDemo.TranslatorAgent\RemoteChatCompletionAgentDemo.TranslatorAgent.csproj" />
<ProjectReference
Include="..\RemoteChatCompletionAgentDemo.SummaryAgent\RemoteChatCompletionAgentDemo.SummaryAgent.csproj" />
<ProjectReference
Include="..\RemoteChatCompletionAgentDemo.GroupChat\RemoteChatCompletionAgentDemo.GroupChat.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.ChatCompletion;
using RemoteChatCompletionAgentDemo.GroupChat;

var builder = WebApplication.CreateBuilder(args);

AppContext.SetSwitch("Microsoft.SemanticKernel.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.AddServiceDefaults();
builder.AddAzureOpenAIClient("openAiConnectionName");
builder.Services.AddHttpClient<TranslatorAgentHttpClient>(client => { client.BaseAddress = new("https+http://translatoragent"); });
builder.Services.AddHttpClient<SummaryAgentHttpClient>(client => { client.BaseAddress = new("https+http://summaryagent"); });
builder.Services.AddKernel().AddAzureOpenAIChatCompletion("gpt-4o");
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/remote-group-chat", async (Kernel kernel, TranslatorAgentHttpClient translatorAgentHttpClient, SummaryAgentHttpClient summaryAgentHttpClient) =>
{
// Use the clients as needed here
var translatorAgent = new RemoteChatCompletionAgent(translatorAgentHttpClient);
var summaryAgent = new RemoteChatCompletionAgent(summaryAgentHttpClient);

var terminateFunction = KernelFunctionFactory.CreateFromPrompt(
"""
Determine if the text has been summarized. If so, respond with a single word: yes.

History:

{{$history}}
"""
);

var selectionFunction = KernelFunctionFactory.CreateFromPrompt(
$$$"""
Your job is to determine which participant takes the next turn in a conversation according to the action of the most recent participant.
State only the name of the participant to take the next turn.

Choose only from these participants:
- {{{translatorAgent.Name}}}
- {{{summaryAgent.Name}}}

Always follow these steps when selecting the next participant:
1) After user input, it is {{{translatorAgent.Name}}}'s turn.
2) After {{{translatorAgent.Name}}} replies, it's {{{summaryAgent.Name}}}'s turn.

History:
{{$history}}
"""
);

var chat = new AgentGroupChat(translatorAgent, summaryAgent)
{
ExecutionSettings = new()
{
TerminationStrategy = new KernelFunctionTerminationStrategy(terminateFunction, kernel)
{
Agents = [summaryAgent],
ResultParser = (result) => result.GetValue<string>()?.Contains("yes", StringComparison.OrdinalIgnoreCase) ?? false,
HistoryVariableName = "history",
MaximumIterations = 10
},
SelectionStrategy = new KernelFunctionSelectionStrategy(selectionFunction, kernel)
{
HistoryVariableName = "history"
}
}
};

var prompt = "COME I FORNITORI INFLUENZANO I TUOI COSTI Quando scegli un piano di assicurazione sanitaria, uno dei fattori più importanti da considerare è la rete di fornitori in convenzione disponibili con il piano. Northwind Standard offre un'ampia varietà di fornitori in convenzione, tra cui medici di base, specialisti, ospedali e farmacie. Questo ti permette di scegliere un fornitore comodo per te e la tua famiglia, contribuendo al contempo a mantenere bassi i tuoi costi. Se scegli un fornitore in convenzione con il tuo piano, pagherai generalmente copay e franchigie più basse rispetto a un fornitore fuori rete. Inoltre, molti servizi, come l'assistenza preventiva, possono essere coperti senza alcun costo aggiuntivo se ricevuti da un fornitore in convenzione. È importante notare, tuttavia, che Northwind Standard non copre i servizi di emergenza, l'assistenza per la salute mentale e l'abuso di sostanze, né i servizi fuori rete. Questo significa che potresti dover pagare di tasca tua per questi servizi se ricevuti da un fornitore fuori rete. Quando scegli un fornitore in convenzione, ci sono alcuni suggerimenti da tenere a mente. Verifica che il fornitore sia in convenzione con il tuo piano. Puoi confermarlo chiamando l'ufficio del fornitore e chiedendo se è in rete con Northwind Standard. Puoi anche utilizzare lo strumento di ricerca fornitori sul sito web di Northwind Health per verificare la copertura. Assicurati che il fornitore stia accettando nuovi pazienti. Alcuni fornitori potrebbero essere in convenzione ma non accettare nuovi pazienti. Considera la posizione del fornitore. Se il fornitore è troppo lontano, potrebbe essere difficile raggiungere gli appuntamenti. Valuta gli orari dell'ufficio del fornitore. Se lavori durante il giorno, potresti aver bisogno di trovare un fornitore con orari serali o nel fine settimana. Scegliere un fornitore in convenzione può aiutarti a risparmiare sui costi sanitari. Seguendo i suggerimenti sopra e facendo ricerche sulle opzioni disponibili, puoi trovare un fornitore conveniente, accessibile e in rete con il tuo piano Northwind Standard.";
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, prompt));
await foreach (var content in chat.InvokeAsync().ConfigureAwait(false))
{
Console.WriteLine();
Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
Console.WriteLine();
}

return Results.Ok();
})
.WithName("InvokeRemoteGroupChat");

app.MapDefaultEndpoints();

app.Run();

internal sealed class TranslatorAgentHttpClient : RemoteAgentHttpClient
{
public TranslatorAgentHttpClient(HttpClient httpClient) : base(httpClient)
{
}
}

internal sealed class SummaryAgentHttpClient : RemoteAgentHttpClient
{
public SummaryAgentHttpClient(HttpClient httpClient) : base(httpClient)
{
}
}
Loading
Loading