mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add AIAgent implementation for GitHub Copilot SDK (#3395)
* Initial plan * Add GitHub Copilot SDK AIAgent implementation with tests Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Add projects to solution and fix sample imports Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Improve pragma comment clarity in GithubCopilotAgentThread Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Address PR feedback: internal constructor/setter, remove CopilotClientOptions ctor, streaming improvements, better sample, container warning Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Add ownsClient parameter to allow caller control over client disposal Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Fix unit tests by removing await using to avoid StreamJsonRpc disposal issues Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Fix file encoding: add UTF-8 BOM to Program.cs Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Fix dotnet-format errors: UTF-8 BOM, remove unused logger, add this qualifier, remove unnecessary usings Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Fix test file encoding and remove redundant cast Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Add AsAIAgent extension methods for CopilotClient with tests Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> * Remove IL suppressions, use TryComplete for channel writer, remove TCS from streaming Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Keep session alive across calls, add tools overload, add tests Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Revert session persistence changes - sessions dispose after each call Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Add CreatedAt property mapping using DateTimeOffset.UtcNow Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Add DataContent handling via temp files and attachments Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix formatting: remove extra indentation, simplify Path references, remove unused using Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Refactor: extract helper methods to reduce duplication in DataContent handling Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Updated sample and session config mapping * Added instructions parameter * Updated README * Address PR feedback: reorder params, optimize dictionary, update prefix, remove InternalsVisibleTo, update sample prompts, add defaults Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Remove StreamJsonRpc reference from sample project Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix parameter ordering: tools now after description, rename to s_mediaTypeExtensions, simplify extension logic, update prompts, fix test expectations Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix streaming prompt: change Python to C# for Fibonacci example Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Handle all SDK events, add UsageContent support, fix model name, remove AutoStart, add using for Channels Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Resolved build errors * Addressed comments * Small fix * Addressed comment * Small fix * Addressed comments * Added integration tests * Small update --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
968621e817
commit
e82d9f5e45
@@ -89,6 +89,7 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.18" />
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
|
||||
<!-- M365 Agents SDK -->
|
||||
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Agent_With_CustomImplementation.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GithubCopilot/Agent_With_GithubCopilot.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
|
||||
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
|
||||
@@ -395,6 +396,7 @@
|
||||
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.GithubCopilot/Microsoft.Agents.AI.GithubCopilot.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
|
||||
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
|
||||
@@ -424,6 +426,7 @@
|
||||
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests/Microsoft.Agents.AI.GithubCopilot.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
|
||||
@@ -438,6 +441,7 @@
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.GithubCopilot.UnitTests/Microsoft.Agents.AI.GithubCopilot.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitHub.Copilot.SDK" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.GithubCopilot\Microsoft.Agents.AI.GithubCopilot.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to create a GitHub Copilot agent with shell command permissions.
|
||||
|
||||
using GitHub.Copilot.SDK;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
// Permission handler that prompts the user for approval
|
||||
static Task<PermissionRequestResult> PromptPermission(PermissionRequest request, PermissionInvocation invocation)
|
||||
{
|
||||
Console.WriteLine($"\n[Permission Request: {request.Kind}]");
|
||||
Console.Write("Approve? (y/n): ");
|
||||
|
||||
string? input = Console.ReadLine()?.Trim().ToUpperInvariant();
|
||||
string kind = input is "Y" or "YES" ? "approved" : "denied-interactively-by-user";
|
||||
|
||||
return Task.FromResult(new PermissionRequestResult { Kind = kind });
|
||||
}
|
||||
|
||||
// Create and start a Copilot client
|
||||
await using CopilotClient copilotClient = new();
|
||||
await copilotClient.StartAsync();
|
||||
|
||||
// Create an agent with a session config that enables permission handling
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
OnPermissionRequest = PromptPermission,
|
||||
};
|
||||
|
||||
AIAgent agent = copilotClient.AsAIAgent(sessionConfig, ownsClient: true);
|
||||
|
||||
// Toggle between streaming and non-streaming modes
|
||||
bool useStreaming = true;
|
||||
|
||||
string prompt = "List all files in the current directory";
|
||||
Console.WriteLine($"User: {prompt}\n");
|
||||
|
||||
if (useStreaming)
|
||||
{
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(prompt))
|
||||
{
|
||||
Console.Write(update);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
AgentResponse response = await agent.RunAsync(prompt);
|
||||
Console.WriteLine(response);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
# Prerequisites
|
||||
|
||||
> **⚠️ WARNING: Container Recommendation**
|
||||
>
|
||||
> GitHub Copilot can execute tools and commands that may interact with your system. For safety, it is strongly recommended to run this sample in a containerized environment (e.g., Docker, Dev Container) to avoid unintended consequences to your machine.
|
||||
|
||||
Before you begin, ensure you have the following prerequisites:
|
||||
|
||||
- .NET 10 SDK or later
|
||||
- GitHub Copilot CLI installed and available in your PATH (or provide a custom path)
|
||||
|
||||
## Setting up GitHub Copilot CLI
|
||||
|
||||
To use this sample, you need to have the GitHub Copilot CLI installed. You can install it by following the instructions at:
|
||||
https://github.com/github/copilot-sdk
|
||||
|
||||
Once installed, ensure the `copilot` command is available in your PATH, or configure a custom path using `CopilotClientOptions`.
|
||||
|
||||
## Running the Sample
|
||||
|
||||
No additional environment variables are required if using default configuration. The sample will:
|
||||
|
||||
1. Create a GitHub Copilot client with default options
|
||||
2. Create an AI agent using the Copilot SDK
|
||||
3. Send a message to the agent
|
||||
4. Display the response
|
||||
|
||||
Run the sample:
|
||||
|
||||
```powershell
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
You can customize the agent by providing additional configuration:
|
||||
|
||||
```csharp
|
||||
using GitHub.Copilot.SDK;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
// Create and start a Copilot client
|
||||
await using CopilotClient copilotClient = new();
|
||||
await copilotClient.StartAsync();
|
||||
|
||||
// Create session configuration with specific model
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
Model = "claude-opus-4.5",
|
||||
Streaming = false
|
||||
};
|
||||
|
||||
// Create an agent with custom configuration using the extension method
|
||||
AIAgent agent = copilotClient.AsAIAgent(
|
||||
sessionConfig,
|
||||
ownsClient: true,
|
||||
id: "my-copilot-agent",
|
||||
name: "My Copilot Assistant",
|
||||
description: "A helpful AI assistant powered by GitHub Copilot"
|
||||
);
|
||||
|
||||
// Use the agent - ask it to write code for us
|
||||
AgentResponse response = await agent.RunAsync("Write a small .NET 10 C# hello world single file application");
|
||||
Console.WriteLine(response);
|
||||
```
|
||||
|
||||
## Streaming Responses
|
||||
|
||||
To get streaming responses:
|
||||
|
||||
```csharp
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("Write a C# function to calculate Fibonacci numbers"))
|
||||
{
|
||||
Console.Write(update.Text);
|
||||
}
|
||||
```
|
||||
@@ -22,6 +22,7 @@ See the README.md for each sample for the prerequisites for that sample.
|
||||
|[Creating an AIAgent with Azure OpenAI ChatCompletion](./Agent_With_AzureOpenAIChatCompletion/)|This sample demonstrates how to create an AIAgent using Azure OpenAI ChatCompletion as the underlying inference service|
|
||||
|[Creating an AIAgent with Azure OpenAI Responses](./Agent_With_AzureOpenAIResponses/)|This sample demonstrates how to create an AIAgent using Azure OpenAI Responses as the underlying inference service|
|
||||
|[Creating an AIAgent with a custom implementation](./Agent_With_CustomImplementation/)|This sample demonstrates how to create an AIAgent with a custom implementation|
|
||||
|[Creating an AIAgent with GitHub Copilot](./Agent_With_GithubCopilot/)|This sample demonstrates how to create an AIAgent using GitHub Copilot SDK as the underlying inference service|
|
||||
|[Creating an AIAgent with Ollama](./Agent_With_Ollama/)|This sample demonstrates how to create an AIAgent using Ollama as the underlying inference service|
|
||||
|[Creating an AIAgent with ONNX](./Agent_With_ONNX/)|This sample demonstrates how to create an AIAgent using ONNX as the underlying inference service|
|
||||
|[Creating an AIAgent with OpenAI Assistants](./Agent_With_OpenAIAssistants/)|This sample demonstrates how to create an AIAgent using OpenAI Assistants as the underlying inference service.</br>WARNING: The Assistants API is deprecated and will be shut down. For more information see the OpenAI documentation: https://platform.openai.com/docs/assistants/migration|
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.GithubCopilot;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace GitHub.Copilot.SDK;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="CopilotClient"/>
|
||||
/// to simplify the creation of GitHub Copilot agents.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These extensions bridge the gap between GitHub Copilot SDK client objects
|
||||
/// and the Microsoft Agent Framework.
|
||||
/// <para>
|
||||
/// They allow developers to easily create AI agents that can interact
|
||||
/// with GitHub Copilot by handling the conversion from Copilot clients to
|
||||
/// <see cref="GithubCopilotAgent"/> instances that implement the <see cref="AIAgent"/> interface.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class CopilotClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an instance of <see cref="AIAgent"/> for a GitHub Copilot client.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="CopilotClient"/> to use for the agent.</param>
|
||||
/// <param name="sessionConfig">Optional session configuration for the agent.</param>
|
||||
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
/// <returns>An <see cref="AIAgent"/> instance backed by the GitHub Copilot client.</returns>
|
||||
public static AIAgent AsAIAgent(
|
||||
this CopilotClient client,
|
||||
SessionConfig? sessionConfig = null,
|
||||
bool ownsClient = false,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
string? description = null)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return new GithubCopilotAgent(client, sessionConfig, ownsClient, id, name, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an instance of <see cref="AIAgent"/> for a GitHub Copilot client.
|
||||
/// </summary>
|
||||
/// <param name="client">The <see cref="CopilotClient"/> to use for the agent.</param>
|
||||
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
/// <param name="tools">The tools to make available to the agent.</param>
|
||||
/// <param name="instructions">Optional instructions to append as a system message.</param>
|
||||
/// <returns>An <see cref="AIAgent"/> instance backed by the GitHub Copilot client.</returns>
|
||||
public static AIAgent AsAIAgent(
|
||||
this CopilotClient client,
|
||||
bool ownsClient = false,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
IList<AITool>? tools = null,
|
||||
string? instructions = null)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return new GithubCopilotAgent(client, ownsClient, id, name, description, tools, instructions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Copilot.SDK;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.GithubCopilot;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an <see cref="AIAgent"/> that uses the GitHub Copilot SDK to provide agentic capabilities.
|
||||
/// </summary>
|
||||
public sealed class GithubCopilotAgent : AIAgent, IAsyncDisposable
|
||||
{
|
||||
private const string DefaultName = "GitHub Copilot Agent";
|
||||
private const string DefaultDescription = "An AI agent powered by GitHub Copilot";
|
||||
|
||||
private readonly CopilotClient _copilotClient;
|
||||
private readonly string? _id;
|
||||
private readonly string _name;
|
||||
private readonly string _description;
|
||||
private readonly SessionConfig? _sessionConfig;
|
||||
private readonly bool _ownsClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GithubCopilotAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="copilotClient">The Copilot client to use for interacting with GitHub Copilot.</param>
|
||||
/// <param name="sessionConfig">Optional session configuration for the agent.</param>
|
||||
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
public GithubCopilotAgent(
|
||||
CopilotClient copilotClient,
|
||||
SessionConfig? sessionConfig = null,
|
||||
bool ownsClient = false,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
string? description = null)
|
||||
{
|
||||
_ = Throw.IfNull(copilotClient);
|
||||
|
||||
this._copilotClient = copilotClient;
|
||||
this._sessionConfig = sessionConfig;
|
||||
this._ownsClient = ownsClient;
|
||||
this._id = id;
|
||||
this._name = name ?? DefaultName;
|
||||
this._description = description ?? DefaultDescription;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GithubCopilotAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="copilotClient">The Copilot client to use for interacting with GitHub Copilot.</param>
|
||||
/// <param name="ownsClient">Whether the agent owns the client and should dispose it. Default is false.</param>
|
||||
/// <param name="id">The unique identifier for the agent.</param>
|
||||
/// <param name="name">The name of the agent.</param>
|
||||
/// <param name="description">The description of the agent.</param>
|
||||
/// <param name="tools">The tools to make available to the agent.</param>
|
||||
/// <param name="instructions">Optional instructions to append as a system message.</param>
|
||||
public GithubCopilotAgent(
|
||||
CopilotClient copilotClient,
|
||||
bool ownsClient = false,
|
||||
string? id = null,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
IList<AITool>? tools = null,
|
||||
string? instructions = null)
|
||||
: this(
|
||||
copilotClient,
|
||||
GetSessionConfig(tools, instructions),
|
||||
ownsClient,
|
||||
id,
|
||||
name,
|
||||
description)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override ValueTask<AgentSession> GetNewSessionAsync(CancellationToken cancellationToken = default)
|
||||
=> new(new GithubCopilotAgentSession());
|
||||
|
||||
/// <summary>
|
||||
/// Get a new <see cref="AgentSession"/> instance using an existing session id, to continue that conversation.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session id to continue.</param>
|
||||
/// <returns>A new <see cref="AgentSession"/> instance.</returns>
|
||||
public ValueTask<AgentSession> GetNewSessionAsync(string sessionId)
|
||||
=> new(new GithubCopilotAgentSession() { SessionId = sessionId });
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<AgentSession> DeserializeSessionAsync(
|
||||
JsonElement serializedSession,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> new(new GithubCopilotAgentSession(serializedSession, jsonSerializerOptions));
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<AgentResponse> RunCoreAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this.RunCoreStreamingAsync(messages, session, options, cancellationToken).ToAgentResponseAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentSession? session = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
// Ensure we have a valid session
|
||||
session ??= await this.GetNewSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (session is not GithubCopilotAgentSession typedSession)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The provided session type {session.GetType()} is not compatible with the agent. Only GitHub Copilot agent created sessions are supported.");
|
||||
}
|
||||
|
||||
// Ensure the client is started
|
||||
await this.EnsureClientStartedAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Create or resume a session with streaming enabled
|
||||
SessionConfig sessionConfig = this._sessionConfig != null
|
||||
? new SessionConfig
|
||||
{
|
||||
Model = this._sessionConfig.Model,
|
||||
Tools = this._sessionConfig.Tools,
|
||||
SystemMessage = this._sessionConfig.SystemMessage,
|
||||
AvailableTools = this._sessionConfig.AvailableTools,
|
||||
ExcludedTools = this._sessionConfig.ExcludedTools,
|
||||
Provider = this._sessionConfig.Provider,
|
||||
OnPermissionRequest = this._sessionConfig.OnPermissionRequest,
|
||||
McpServers = this._sessionConfig.McpServers,
|
||||
CustomAgents = this._sessionConfig.CustomAgents,
|
||||
SkillDirectories = this._sessionConfig.SkillDirectories,
|
||||
DisabledSkills = this._sessionConfig.DisabledSkills,
|
||||
Streaming = true
|
||||
}
|
||||
: new SessionConfig { Streaming = true };
|
||||
|
||||
CopilotSession copilotSession;
|
||||
if (typedSession.SessionId is not null)
|
||||
{
|
||||
copilotSession = await this._copilotClient.ResumeSessionAsync(
|
||||
typedSession.SessionId,
|
||||
this.CreateResumeConfig(),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
copilotSession = await this._copilotClient.CreateSessionAsync(sessionConfig, cancellationToken).ConfigureAwait(false);
|
||||
typedSession.SessionId = copilotSession.SessionId;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Channel<AgentResponseUpdate> channel = Channel.CreateUnbounded<AgentResponseUpdate>();
|
||||
|
||||
// Subscribe to session events
|
||||
using IDisposable subscription = copilotSession.On(evt =>
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AssistantMessageDeltaEvent deltaEvent:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(deltaEvent));
|
||||
break;
|
||||
|
||||
case AssistantMessageEvent assistantMessage:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(assistantMessage));
|
||||
break;
|
||||
|
||||
case AssistantUsageEvent usageEvent:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(usageEvent));
|
||||
break;
|
||||
|
||||
case SessionIdleEvent idleEvent:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(idleEvent));
|
||||
channel.Writer.TryComplete();
|
||||
break;
|
||||
|
||||
case SessionErrorEvent errorEvent:
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(errorEvent));
|
||||
channel.Writer.TryComplete(new InvalidOperationException(
|
||||
$"Session error: {errorEvent.Data?.Message ?? "Unknown error"}"));
|
||||
break;
|
||||
|
||||
default:
|
||||
// Handle all other event types by storing as RawRepresentation
|
||||
channel.Writer.TryWrite(this.ConvertToAgentResponseUpdate(evt));
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
List<string> tempFiles = [];
|
||||
try
|
||||
{
|
||||
// Build prompt from text content
|
||||
string prompt = string.Join("\n", messages.Select(m => m.Text));
|
||||
|
||||
// Handle DataContent as attachments
|
||||
List<UserMessageDataAttachmentsItem>? attachments = await ProcessDataContentAttachmentsAsync(
|
||||
messages,
|
||||
tempFiles,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Send the message with attachments
|
||||
MessageOptions messageOptions = new() { Prompt = prompt };
|
||||
if (attachments is not null)
|
||||
{
|
||||
messageOptions.Attachments = [.. attachments];
|
||||
}
|
||||
|
||||
await copilotSession.SendAsync(messageOptions, cancellationToken).ConfigureAwait(false);
|
||||
// Yield updates as they arrive
|
||||
await foreach (AgentResponseUpdate update in channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanupTempFiles(tempFiles);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await copilotSession.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string? IdCore => this._id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => this._name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Description => this._description;
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the agent and releases resources.
|
||||
/// </summary>
|
||||
/// <returns>A value task representing the asynchronous dispose operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (this._ownsClient)
|
||||
{
|
||||
await this._copilotClient.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureClientStartedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._copilotClient.State != ConnectionState.Connected)
|
||||
{
|
||||
await this._copilotClient.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private ResumeSessionConfig CreateResumeConfig()
|
||||
{
|
||||
return new ResumeSessionConfig
|
||||
{
|
||||
Tools = this._sessionConfig?.Tools,
|
||||
Provider = this._sessionConfig?.Provider,
|
||||
OnPermissionRequest = this._sessionConfig?.OnPermissionRequest,
|
||||
McpServers = this._sessionConfig?.McpServers,
|
||||
CustomAgents = this._sessionConfig?.CustomAgents,
|
||||
SkillDirectories = this._sessionConfig?.SkillDirectories,
|
||||
DisabledSkills = this._sessionConfig?.DisabledSkills,
|
||||
Streaming = true
|
||||
};
|
||||
}
|
||||
|
||||
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageDeltaEvent deltaEvent)
|
||||
{
|
||||
TextContent textContent = new(deltaEvent.Data?.DeltaContent ?? string.Empty)
|
||||
{
|
||||
RawRepresentation = deltaEvent
|
||||
};
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
MessageId = deltaEvent.Data?.MessageId,
|
||||
CreatedAt = deltaEvent.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
|
||||
{
|
||||
TextContent textContent = new(assistantMessage.Data?.Content ?? string.Empty)
|
||||
{
|
||||
RawRepresentation = assistantMessage
|
||||
};
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = assistantMessage.Data?.MessageId,
|
||||
MessageId = assistantMessage.Data?.MessageId,
|
||||
CreatedAt = assistantMessage.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantUsageEvent usageEvent)
|
||||
{
|
||||
UsageDetails usageDetails = new()
|
||||
{
|
||||
InputTokenCount = (int?)(usageEvent.Data?.InputTokens),
|
||||
OutputTokenCount = (int?)(usageEvent.Data?.OutputTokens),
|
||||
TotalTokenCount = (int?)((usageEvent.Data?.InputTokens ?? 0) + (usageEvent.Data?.OutputTokens ?? 0)),
|
||||
CachedInputTokenCount = (int?)(usageEvent.Data?.CacheReadTokens),
|
||||
AdditionalCounts = GetAdditionalCounts(usageEvent),
|
||||
};
|
||||
|
||||
UsageContent usageContent = new(usageDetails)
|
||||
{
|
||||
RawRepresentation = usageEvent
|
||||
};
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [usageContent])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
CreatedAt = usageEvent.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
private static AdditionalPropertiesDictionary<long>? GetAdditionalCounts(AssistantUsageEvent usageEvent)
|
||||
{
|
||||
if (usageEvent.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AdditionalPropertiesDictionary<long>? additionalCounts = null;
|
||||
|
||||
if (usageEvent.Data.CacheWriteTokens is double cacheWriteTokens)
|
||||
{
|
||||
additionalCounts ??= [];
|
||||
additionalCounts[nameof(AssistantUsageData.CacheWriteTokens)] = (long)cacheWriteTokens;
|
||||
}
|
||||
|
||||
if (usageEvent.Data.Cost is double cost)
|
||||
{
|
||||
additionalCounts ??= [];
|
||||
additionalCounts[nameof(AssistantUsageData.Cost)] = (long)cost;
|
||||
}
|
||||
|
||||
if (usageEvent.Data.Duration is double duration)
|
||||
{
|
||||
additionalCounts ??= [];
|
||||
additionalCounts[nameof(AssistantUsageData.Duration)] = (long)duration;
|
||||
}
|
||||
|
||||
return additionalCounts;
|
||||
}
|
||||
|
||||
private AgentResponseUpdate ConvertToAgentResponseUpdate(SessionEvent sessionEvent)
|
||||
{
|
||||
// Handle arbitrary events by storing as RawRepresentation
|
||||
AIContent content = new()
|
||||
{
|
||||
RawRepresentation = sessionEvent
|
||||
};
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [content])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
CreatedAt = sessionEvent.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
private static SessionConfig? GetSessionConfig(IList<AITool>? tools, string? instructions)
|
||||
{
|
||||
List<AIFunction>? mappedTools = tools is { Count: > 0 } ? tools.OfType<AIFunction>().ToList() : null;
|
||||
SystemMessageConfig? systemMessage = instructions is not null ? new SystemMessageConfig { Mode = SystemMessageMode.Append, Content = instructions } : null;
|
||||
|
||||
if (mappedTools is null && systemMessage is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SessionConfig { Tools = mappedTools, SystemMessage = systemMessage };
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> s_mediaTypeExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["image/png"] = ".png",
|
||||
["image/jpeg"] = ".jpg",
|
||||
["image/jpg"] = ".jpg",
|
||||
["image/gif"] = ".gif",
|
||||
["image/webp"] = ".webp",
|
||||
["image/svg+xml"] = ".svg",
|
||||
["text/plain"] = ".txt",
|
||||
["text/html"] = ".html",
|
||||
["text/markdown"] = ".md",
|
||||
["application/json"] = ".json",
|
||||
["application/xml"] = ".xml",
|
||||
["application/pdf"] = ".pdf"
|
||||
};
|
||||
|
||||
private static string GetExtensionForMediaType(string? mediaType)
|
||||
{
|
||||
return mediaType is not null && s_mediaTypeExtensions.TryGetValue(mediaType, out string? extension) ? extension : ".dat";
|
||||
}
|
||||
|
||||
private static async Task<List<UserMessageDataAttachmentsItem>?> ProcessDataContentAttachmentsAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
List<string> tempFiles,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<UserMessageDataAttachmentsItem>? attachments = null;
|
||||
foreach (ChatMessage message in messages)
|
||||
{
|
||||
foreach (AIContent content in message.Contents)
|
||||
{
|
||||
if (content is DataContent dataContent)
|
||||
{
|
||||
// Write DataContent to a temp file
|
||||
string tempFilePath = Path.Combine(Path.GetTempPath(), $"agentframework_copilot_data_{Guid.NewGuid()}{GetExtensionForMediaType(dataContent.MediaType)}");
|
||||
await File.WriteAllBytesAsync(tempFilePath, dataContent.Data.ToArray(), cancellationToken).ConfigureAwait(false);
|
||||
tempFiles.Add(tempFilePath);
|
||||
|
||||
// Create attachment
|
||||
attachments ??= [];
|
||||
attachments.Add(new UserMessageDataAttachmentsItem
|
||||
{
|
||||
Type = UserMessageDataAttachmentsItemType.File,
|
||||
Path = tempFilePath,
|
||||
DisplayName = Path.GetFileName(tempFilePath)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
private static void CleanupTempFiles(List<string> tempFiles)
|
||||
{
|
||||
foreach (string tempFile in tempFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Agents.AI.GithubCopilot;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a session for a GitHub Copilot agent conversation.
|
||||
/// </summary>
|
||||
public sealed class GithubCopilotAgentSession : AgentSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the session ID for the GitHub Copilot conversation.
|
||||
/// </summary>
|
||||
public string? SessionId { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GithubCopilotAgentSession"/> class.
|
||||
/// </summary>
|
||||
internal GithubCopilotAgentSession()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GithubCopilotAgentSession"/> class from serialized data.
|
||||
/// </summary>
|
||||
/// <param name="serializedThread">The serialized thread data.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional JSON serialization options.</param>
|
||||
internal GithubCopilotAgentSession(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
// The JSON serialization uses camelCase
|
||||
if (serializedThread.TryGetProperty("sessionId", out JsonElement sessionIdElement))
|
||||
{
|
||||
this.SessionId = sessionIdElement.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
State state = new()
|
||||
{
|
||||
SessionId = this.SessionId
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToElement(
|
||||
state,
|
||||
GithubCopilotJsonUtilities.DefaultOptions.GetTypeInfo(typeof(State)));
|
||||
}
|
||||
|
||||
internal sealed class State
|
||||
{
|
||||
public string? SessionId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Agents.AI.GithubCopilot;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods and configurations for JSON serialization operations within the GitHub Copilot agent implementation.
|
||||
/// </summary>
|
||||
internal static partial class GithubCopilotJsonUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="JsonSerializerOptions"/> instance used for JSON serialization operations.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Creates and configures the default JSON serialization options.
|
||||
/// </summary>
|
||||
/// <returns>The configured options.</returns>
|
||||
private static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
// Copy the configuration from the source generated context.
|
||||
JsonSerializerOptions options = new(JsonContext.Default.Options)
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
// Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context.
|
||||
options.TypeInfoResolverChain.Clear();
|
||||
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
|
||||
options.TypeInfoResolverChain.Add(JsonContext.Default.Options.TypeInfoResolver!);
|
||||
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
|
||||
[JsonSerializable(typeof(GithubCopilotAgentSession.State))]
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed partial class JsonContext : JsonSerializerContext;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionSuffix>preview</VersionSuffix>
|
||||
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitHub.Copilot.SDK" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework GitHub Copilot</Title>
|
||||
<Description>Provides Microsoft Agent Framework support for GitHub Copilot SDK.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Copilot.SDK;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.GithubCopilot.IntegrationTests;
|
||||
|
||||
public class GithubCopilotAgentTests
|
||||
{
|
||||
private const string SkipReason = "Integration tests require GitHub Copilot CLI installed. For local execution only.";
|
||||
|
||||
private static Task<PermissionRequestResult> OnPermissionRequestAsync(PermissionRequest request, PermissionInvocation invocation)
|
||||
=> Task.FromResult(new PermissionRequestResult { Kind = "approved" });
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithSimplePrompt_ReturnsResponseAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
await using GithubCopilotAgent agent = new(client, sessionConfig: null);
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync("What is 2 + 2? Answer with just the number.");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.NotEmpty(response.Messages);
|
||||
Assert.Contains("4", response.Text);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunStreamingAsync_WithSimplePrompt_ReturnsUpdatesAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
await using GithubCopilotAgent agent = new(client, sessionConfig: null);
|
||||
|
||||
// Act
|
||||
List<AgentResponseUpdate> updates = [];
|
||||
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("What is 2 + 2? Answer with just the number."))
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(updates);
|
||||
string fullText = string.Join("", updates.Select(u => u.Text));
|
||||
Assert.Contains("4", fullText);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithFunctionTool_InvokesToolAsync()
|
||||
{
|
||||
// Arrange
|
||||
bool toolInvoked = false;
|
||||
|
||||
AIFunction weatherTool = AIFunctionFactory.Create((string location) =>
|
||||
{
|
||||
toolInvoked = true;
|
||||
return $"The weather in {location} is sunny with a high of 25C.";
|
||||
}, "GetWeather", "Get the weather for a given location.");
|
||||
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
await using GithubCopilotAgent agent = new(
|
||||
client,
|
||||
tools: [weatherTool],
|
||||
instructions: "You are a helpful weather agent. Use the GetWeather tool to answer weather questions.");
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync("What's the weather like in Seattle?");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.NotEmpty(response.Messages);
|
||||
Assert.True(toolInvoked);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithSession_MaintainsContextAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
await using GithubCopilotAgent agent = new(
|
||||
client,
|
||||
instructions: "You are a helpful assistant. Keep your answers short.");
|
||||
|
||||
AgentSession session = await agent.GetNewSessionAsync();
|
||||
|
||||
// Act - First turn
|
||||
AgentResponse response1 = await agent.RunAsync("My name is Alice.", session);
|
||||
Assert.NotNull(response1);
|
||||
|
||||
// Act - Second turn using same session
|
||||
AgentResponse response2 = await agent.RunAsync("What is my name?", session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response2);
|
||||
Assert.Contains("Alice", response2.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithSessionResume_ContinuesConversationAsync()
|
||||
{
|
||||
// Arrange - First agent instance starts a conversation
|
||||
string? sessionId;
|
||||
|
||||
await using CopilotClient client1 = new(new CopilotClientOptions());
|
||||
await client1.StartAsync();
|
||||
|
||||
await using GithubCopilotAgent agent1 = new(
|
||||
client1,
|
||||
instructions: "You are a helpful assistant. Keep your answers short.");
|
||||
|
||||
AgentSession session1 = await agent1.GetNewSessionAsync();
|
||||
await agent1.RunAsync("Remember this number: 42.", session1);
|
||||
|
||||
sessionId = ((GithubCopilotAgentSession)session1).SessionId;
|
||||
Assert.NotNull(sessionId);
|
||||
|
||||
// Act - Second agent instance resumes the session
|
||||
await using CopilotClient client2 = new(new CopilotClientOptions());
|
||||
await client2.StartAsync();
|
||||
|
||||
await using GithubCopilotAgent agent2 = new(
|
||||
client2,
|
||||
instructions: "You are a helpful assistant. Keep your answers short.");
|
||||
|
||||
AgentSession session2 = await agent2.GetNewSessionAsync(sessionId);
|
||||
AgentResponse response = await agent2.RunAsync("What number did I ask you to remember?", session2);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Contains("42", response.Text);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithShellPermissions_ExecutesCommandAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
OnPermissionRequest = OnPermissionRequestAsync,
|
||||
};
|
||||
|
||||
await using GithubCopilotAgent agent = new(client, sessionConfig);
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync("Run a shell command to print 'hello world'");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.NotEmpty(response.Messages);
|
||||
Assert.Contains("hello", response.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithUrlPermissions_FetchesContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
OnPermissionRequest = OnPermissionRequestAsync,
|
||||
};
|
||||
|
||||
await using GithubCopilotAgent agent = new(client, sessionConfig);
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync(
|
||||
"Fetch https://learn.microsoft.com/agent-framework/tutorials/quick-start and summarize its contents in one sentence");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Contains("Agent Framework", response.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithLocalMcpServer_UsesServerToolsAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
OnPermissionRequest = OnPermissionRequestAsync,
|
||||
McpServers = new Dictionary<string, object>
|
||||
{
|
||||
["filesystem"] = new McpLocalServerConfig
|
||||
{
|
||||
Type = "stdio",
|
||||
Command = "npx",
|
||||
Args = ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
||||
Tools = ["*"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await using GithubCopilotAgent agent = new(client, sessionConfig);
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync("List the files in the current directory");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.NotEmpty(response.Messages);
|
||||
Assert.NotEmpty(response.Text);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task RunAsync_WithRemoteMcpServer_UsesServerToolsAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using CopilotClient client = new(new CopilotClientOptions());
|
||||
await client.StartAsync();
|
||||
|
||||
SessionConfig sessionConfig = new()
|
||||
{
|
||||
OnPermissionRequest = OnPermissionRequestAsync,
|
||||
McpServers = new Dictionary<string, object>
|
||||
{
|
||||
["microsoft-learn"] = new McpRemoteServerConfig
|
||||
{
|
||||
Type = "http",
|
||||
Url = "https://learn.microsoft.com/api/mcp",
|
||||
Tools = ["*"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await using GithubCopilotAgent agent = new(client, sessionConfig);
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync("Search Microsoft Learn for 'Azure Functions' and summarize the top result");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Contains("Azure Functions", response.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.GithubCopilot\Microsoft.Agents.AI.GithubCopilot.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.Copilot.SDK;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.GithubCopilot.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="CopilotClientExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class CopilotClientExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void AsAIAgent_WithAllParameters_ReturnsGithubCopilotAgentWithSpecifiedProperties()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
|
||||
const string TestId = "test-agent-id";
|
||||
const string TestName = "Test Agent";
|
||||
const string TestDescription = "This is a test agent description";
|
||||
|
||||
// Act
|
||||
var agent = copilotClient.AsAIAgent(ownsClient: false, id: TestId, name: TestName, description: TestDescription, tools: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<GithubCopilotAgent>(agent);
|
||||
Assert.Equal(TestId, agent.Id);
|
||||
Assert.Equal(TestName, agent.Name);
|
||||
Assert.Equal(TestDescription, agent.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAIAgent_WithMinimalParameters_ReturnsGithubCopilotAgent()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
|
||||
// Act
|
||||
var agent = copilotClient.AsAIAgent(ownsClient: false, tools: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<GithubCopilotAgent>(agent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAIAgent_WithNullClient_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient? copilotClient = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => copilotClient!.AsAIAgent(sessionConfig: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAIAgent_WithOwnsClient_ReturnsAgentThatOwnsClient()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
|
||||
// Act
|
||||
var agent = copilotClient.AsAIAgent(ownsClient: true, tools: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<GithubCopilotAgent>(agent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsAIAgent_WithTools_ReturnsAgentWithTools()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
List<AITool> tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")];
|
||||
|
||||
// Act
|
||||
var agent = copilotClient.AsAIAgent(tools: tools);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.IsType<GithubCopilotAgent>(agent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Copilot.SDK;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.GithubCopilot.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="GithubCopilotAgent"/> class.
|
||||
/// </summary>
|
||||
public sealed class GithubCopilotAgentTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithCopilotClient_InitializesPropertiesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
const string TestId = "test-id";
|
||||
const string TestName = "test-name";
|
||||
const string TestDescription = "test-description";
|
||||
|
||||
// Act
|
||||
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, id: TestId, name: TestName, description: TestDescription, tools: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TestId, agent.Id);
|
||||
Assert.Equal(TestName, agent.Name);
|
||||
Assert.Equal(TestDescription, agent.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullCopilotClient_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new GithubCopilotAgent(copilotClient: null!, sessionConfig: null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDefaultParameters_UsesBaseProperties()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
|
||||
// Act
|
||||
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent.Id);
|
||||
Assert.NotEmpty(agent.Id);
|
||||
Assert.Equal("GitHub Copilot Agent", agent.Name);
|
||||
Assert.Equal("An AI agent powered by GitHub Copilot", agent.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNewSessionAsync_ReturnsGithubCopilotAgentSessionAsync()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null);
|
||||
|
||||
// Act
|
||||
var session = await agent.GetNewSessionAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(session);
|
||||
Assert.IsType<GithubCopilotAgentSession>(session);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNewSessionAsync_WithSessionId_ReturnsSessionWithSessionIdAsync()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
var agent = new GithubCopilotAgent(copilotClient, ownsClient: false, tools: null);
|
||||
const string TestSessionId = "test-session-id";
|
||||
|
||||
// Act
|
||||
var session = await agent.GetNewSessionAsync(TestSessionId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(session);
|
||||
var typedSession = Assert.IsType<GithubCopilotAgentSession>(session);
|
||||
Assert.Equal(TestSessionId, typedSession.SessionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithTools_InitializesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
List<AITool> tools = [AIFunctionFactory.Create(() => "test", "TestFunc", "Test function")];
|
||||
|
||||
// Act
|
||||
var agent = new GithubCopilotAgent(copilotClient, tools: tools);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.NotNull(agent.Id);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- GitHub.Copilot.SDK only supports .NET 8.0+ -->
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.GithubCopilot\Microsoft.Agents.AI.GithubCopilot.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user