< Summary

Information
Class: Elsa.AI.Copilot.Adapters.CopilotProvider
Assembly: Elsa.AI.Copilot
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Copilot/Adapters/CopilotProvider.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 113
Coverable lines: 113
Total lines: 190
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 56
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_Name()0%620%
CreateSessionAsync(...)0%4260%
ExecuteTurnAsync()0%7280%
CreateClient(...)100%210%
CreateRuntimeConnection(...)0%4260%
CreateOrResumeSessionAsync()0%620%
ConfigureSession(...)0%156120%
CreateTools(...)100%210%
BuildPrompt(...)0%7280%
NormalizeSessionId(...)0%2040%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Copilot/Adapters/CopilotProvider.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using System.Text;
 3using System.Threading.Channels;
 4using Elsa.AI.Abstractions.Contracts;
 5using Elsa.AI.Abstractions.Models;
 6using Elsa.AI.Copilot.Options;
 7using GitHub.Copilot;
 8using Microsoft.Extensions.AI;
 9using Microsoft.Extensions.Logging;
 10using Microsoft.Extensions.Options;
 11
 12namespace Elsa.AI.Copilot.Adapters;
 13
 014public class CopilotProvider(
 015    IOptions<CopilotOptions> options,
 016    CopilotSessionEventMapper eventMapper,
 017    ILogger<CopilotProvider> logger) : IAIProvider
 18{
 019    public string Name => options.Value.ProviderName ?? "copilot";
 20
 21    public ValueTask<AISessionHandle> CreateSessionAsync(CreateAISessionRequest request, CancellationToken cancellationT
 22    {
 023        var providerName = request.ProviderConfiguration?.Name ?? options.Value.ProviderName ?? "copilot";
 24
 025        return ValueTask.FromResult(new AISessionHandle
 026        {
 027            Id = request.ConversationId,
 028            ProviderSessionId = $"{providerName}:{request.ConversationId}"
 029        });
 30    }
 31
 32    public async IAsyncEnumerable<AIProviderEvent> ExecuteTurnAsync(
 33        AITurnRequest request,
 34        IAIProviderToolInvoker toolInvoker,
 35        [EnumeratorCancellation] CancellationToken cancellationToken = default)
 36    {
 037        var copilotOptions = options.Value;
 038        await using var client = CreateClient(copilotOptions);
 039        await client.StartAsync(cancellationToken);
 40
 041        await using var session = await CreateOrResumeSessionAsync(client, request, toolInvoker, cancellationToken);
 042        var events = Channel.CreateUnbounded<AIProviderEvent>(new UnboundedChannelOptions
 043        {
 044            SingleReader = true,
 045            SingleWriter = false
 046        });
 47
 048        using var subscription = session.On<SessionEvent>(sessionEvent =>
 049        {
 050            foreach (var providerEvent in eventMapper.Map(sessionEvent))
 051                events.Writer.TryWrite(providerEvent);
 052
 053            if (sessionEvent is SessionIdleEvent or SessionErrorEvent)
 054                events.Writer.TryComplete();
 055        });
 56
 57        try
 58        {
 059            await session.SendAsync(new MessageOptions
 060            {
 061                Prompt = BuildPrompt(request),
 062                DisplayPrompt = request.Message
 063            }, cancellationToken);
 064        }
 065        catch (Exception e) when (e is not OperationCanceledException)
 66        {
 067            events.Writer.TryComplete(e);
 068        }
 69
 070        await foreach (var providerEvent in events.Reader.ReadAllAsync(cancellationToken))
 071            yield return providerEvent;
 072    }
 73
 74    private CopilotClient CreateClient(CopilotOptions copilotOptions)
 75    {
 076        var clientOptions = new CopilotClientOptions
 077        {
 078            Connection = CreateRuntimeConnection(copilotOptions),
 079            WorkingDirectory = copilotOptions.WorkingDirectory,
 080            BaseDirectory = copilotOptions.BaseDirectory,
 081            GitHubToken = copilotOptions.GitHubToken,
 082            UseLoggedInUser = copilotOptions.UseLoggedInUser,
 083            Logger = logger
 084        };
 85
 086        return new CopilotClient(clientOptions);
 87    }
 88
 89    private static RuntimeConnection? CreateRuntimeConnection(CopilotOptions copilotOptions)
 90    {
 091        if (!string.IsNullOrWhiteSpace(copilotOptions.RuntimeUrl))
 092            return RuntimeConnection.ForUri(copilotOptions.RuntimeUrl, copilotOptions.ConnectionToken);
 93
 094        if (!string.IsNullOrWhiteSpace(copilotOptions.RuntimePath) || copilotOptions.RuntimeArguments.Count > 0)
 095            return RuntimeConnection.ForStdio(copilotOptions.RuntimePath, copilotOptions.RuntimeArguments.ToList());
 96
 097        return null;
 98    }
 99
 100    private async Task<CopilotSession> CreateOrResumeSessionAsync(CopilotClient client, AITurnRequest request, IAIProvid
 101    {
 0102        var providerSessionId = NormalizeSessionId(request.ProviderSessionId) ?? request.ConversationId;
 0103        var resumeConfig = ConfigureSession(new ResumeSessionConfig
 0104        {
 0105            ContinuePendingWork = true,
 0106            SuppressResumeEvent = true
 0107        }, request, toolInvoker);
 108
 109        try
 110        {
 0111            return await client.ResumeSessionAsync(providerSessionId, resumeConfig, cancellationToken);
 112        }
 0113        catch (Exception e) when (e is not OperationCanceledException)
 114        {
 0115            logger.LogDebug(e, "Copilot session {ProviderSessionId} could not be resumed; creating a new session.", prov
 0116        }
 117
 0118        var createConfig = ConfigureSession(new SessionConfig
 0119        {
 0120            SessionId = providerSessionId
 0121        }, request, toolInvoker);
 122
 0123        return await client.CreateSessionAsync(createConfig, cancellationToken);
 0124    }
 125
 126    private T ConfigureSession<T>(T config, AITurnRequest request, IAIProviderToolInvoker toolInvoker) where T : Session
 127    {
 0128        var copilotOptions = options.Value;
 0129        var providerConfiguration = request.ProviderConfiguration;
 0130        var model = providerConfiguration?.Model ?? copilotOptions.Model;
 131
 0132        config.ClientName = "Elsa Weaver";
 0133        config.Model = model;
 0134        config.ReasoningEffort = copilotOptions.ReasoningEffort;
 0135        config.Streaming = copilotOptions.EnableStreaming;
 0136        config.IncludeSubAgentStreamingEvents = copilotOptions.IncludeSubAgentStreamingEvents;
 0137        config.Tools = CreateTools(request.Tools, toolInvoker);
 0138        config.AvailableTools = request.Tools.Select(x => x.Name).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
 0139        config.OnPermissionRequest = PermissionHandler.ApproveAll;
 140
 0141        if (!string.IsNullOrWhiteSpace(providerConfiguration?.Endpoint))
 0142            config.Provider = new ProviderConfig
 0143            {
 0144                Type = providerConfiguration.Provider,
 0145                BaseUrl = providerConfiguration.Endpoint,
 0146                ModelId = model
 0147            };
 148
 0149        return config;
 150    }
 151
 152    private static ICollection<AIFunctionDeclaration> CreateTools(IReadOnlyCollection<AIToolDefinition> tools, IAIProvid
 0153        tools
 0154            .Where(x => !string.IsNullOrWhiteSpace(x.Name))
 0155            .Select(x => (AIFunctionDeclaration)new ElsaCopilotToolFunction(x, toolInvoker))
 0156            .ToList();
 157
 158    private static string BuildPrompt(AITurnRequest request)
 159    {
 0160        if (request.Context.Count == 0)
 0161            return request.Message;
 162
 0163        var prompt = new StringBuilder();
 0164        prompt.AppendLine(request.Message);
 0165        prompt.AppendLine();
 0166        prompt.AppendLine("Elsa context references resolved by the server:");
 167
 0168        foreach (var context in request.Context)
 169        {
 0170            prompt.AppendLine();
 0171            prompt.AppendLine($"- Kind: {context.Kind}");
 0172            prompt.AppendLine($"  ReferenceId: {context.ReferenceId}");
 0173            if (!string.IsNullOrWhiteSpace(context.Summary))
 0174                prompt.AppendLine($"  Summary: {context.Summary}");
 0175            if (context.Data.Count > 0)
 0176                prompt.AppendLine($"  Data: {context.Data}");
 177        }
 178
 0179        return prompt.ToString();
 180    }
 181
 182    private static string? NormalizeSessionId(string? providerSessionId)
 183    {
 0184        if (string.IsNullOrWhiteSpace(providerSessionId))
 0185            return null;
 186
 0187        var separatorIndex = providerSessionId.IndexOf(':', StringComparison.Ordinal);
 0188        return separatorIndex < 0 ? providerSessionId : providerSessionId[(separatorIndex + 1)..];
 189    }
 190}