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:
co-authored by
westey-m
stephentoub
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Dmytro Struk
parent
968621e817
commit
e82d9f5e45
@@ -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>
|
||||
Reference in New Issue
Block a user