Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@
<PackageVersion Include="Elsa.Workflows.Management" Version="$(ElsaVersion)" />
<PackageVersion Include="Elsa.Workflows.Runtime" Version="$(ElsaVersion)" />
<PackageVersion Include="Elsa.Workflows.Runtime.Distributed" Version="$(ElsaVersion)" />
<PackageVersion Include="Microsoft.SemanticKernel.PromptTemplates.Handlebars" Version="1.59.0" />
</ItemGroup>
<ItemGroup Label="Elsa Studio">
<PackageVersion Include="Elsa.Studio" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Core" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Core.BlazorWasm" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Shared" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Workflows" Version="$(ElsaStudioVersion)" />
<PackageVersion Include="Elsa.Studio.Login.BlazorWasm" Version="$(ElsaStudioVersion)"/>
<PackageVersion Include="Elsa.Studio.Login.BlazorWasm" Version="$(ElsaStudioVersion)" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Antlr4.Runtime.Standard" Version="4.13.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context

var agentInvoker = context.GetRequiredService<AgentInvoker>();
var result = await agentInvoker.InvokeAgentAsync(AgentName, functionInput, context.CancellationToken);
var json = result.FunctionResult.GetValue<string>();
var json = result.ChatMessageContent.Content?.Trim();

if (string.IsNullOrWhiteSpace(json))
throw new InvalidOperationException("The message content is empty or null.");

var outputType = context.ActivityDescriptor.Outputs.Single().Type;

// If the target type is object, we want the JSON to be deserialized into an ExpandoObject for dynamic field access.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Description>Provides Agent activities</Description>
<PackageTags>elsa extension module agents semantic kernel llm ai</PackageTags>
</PropertyGroup>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Elsa" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.SemanticKernel;
namespace Elsa.Agents;

public interface IKernelFactory
{
Kernel CreateKernel(KernelConfig kernelConfig, AgentConfig agentConfig);
Kernel CreateKernel(KernelConfig kernelConfig, string agentName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.SemanticKernel" />
<PackageReference Include="Microsoft.SemanticKernel.PromptTemplates.Handlebars" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static OpenAIPromptExecutionSettings ToOpenAIPromptExecutionSettings(this
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
ResponseFormat = agentConfig.ExecutionSettings.ResponseFormat,
ChatSystemPrompt = agentConfig.PromptTemplate,
ServiceId = "default"
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@ public static class FunctionResultExtensions
public static async Task<JsonElement> AsJsonElementAsync(this Task<InvokeAgentResult> resultTask)
{
var result = await resultTask;
return result.FunctionResult.AsJsonElement();
return result.ChatMessageContent.AsJsonElement();
}

public static async Task<JsonElement> AsJsonElementAsync(this Task<FunctionResult> resultTask)
{
var result = await resultTask;
return result.AsJsonElement();
}

public static JsonElement AsJsonElement(this FunctionResult result)

public static JsonElement AsJsonElement(this ChatMessageContent result)
{
var response = result.GetValue<string>()!;
return JsonSerializer.Deserialize<JsonElement>(response);
var content = result.Content?.Trim();

if (string.IsNullOrWhiteSpace(content))
throw new InvalidOperationException("The message content is empty.");

try
{
return JsonSerializer.Deserialize<JsonElement>(content!);
}
catch (JsonException ex)
{
throw new InvalidOperationException($"Error deserializing the message content as JSON:\n{content}", ex);
}
Comment on lines +21 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, why is it better to explicitly handle a serialization exception and then wrapping it in an IOE?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the explicit handling of JsonException and wrapped it in an InvalidOperationException mainly as a defensive programming measure.

Over the past few weeks, I’ve been doing a kind of reverse engineering to fully understand how the repository and the NuGet packages interact. I wanted to make sure I could trust and trace what's going on at every level while I was testing.

So this change was mostly precautionary, happy to revert or adjust it!

You probably have everything more aligned in your mind and clearly understand which parts interact and why. Since this is still 'new' to me, it's harder for me to reach those conclusions so quickly, haha. That's why I added that detail, more like a 'let me specify this for a moment, so if it breaks, I'll know what caused it.'

Copy link
Member

@sfmskywalker sfmskywalker Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, makes sense 👍🏻 Perhaps it'a not a bad practice actually, especially if we throw a custom exception to make it extra clear what's happening. In this case, it's failing to parse the agent's response as JSON, so that would be a nice message to include with a custom exception class called e.g. FunctionCallException or AgentInvocationException. If you agree, please add an exception class with a name that you think makes sense in this context. Alternatively, I'm happy to do so myself once this is merged.


}
}
10 changes: 3 additions & 7 deletions src/modules/agents/Elsa.Agents.Core/Models/InvokeAgentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@

namespace Elsa.Agents;

public record InvokeAgentResult(AgentConfig Function, FunctionResult FunctionResult)
public record InvokeAgentResult(AgentConfig Function, ChatMessageContent ChatMessageContent)
{
public object? ParseResult()
{
var targetType = Type.GetType(Function.OutputVariable.Type) ?? typeof(JsonElement);
var json = FunctionResult.GetValue<string>();
return JsonSerializer.Deserialize(json, targetType);
}


}
40 changes: 34 additions & 6 deletions src/modules/agents/Elsa.Agents.Core/Services/AgentInvoker.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;

#pragma warning disable SKEXP0010
#pragma warning disable SKEXP0001

namespace Elsa.Agents;

public class AgentInvoker(KernelFactory kernelFactory, IKernelConfigProvider kernelConfigProvider)
public class AgentInvoker(IKernelFactory kernelFactory, IKernelConfigProvider kernelConfigProvider)
{
public async Task<InvokeAgentResult> InvokeAgentAsync(string agentName, IDictionary<string, object?> input, CancellationToken cancellationToken = default)
{
Expand All @@ -21,9 +23,9 @@ public async Task<InvokeAgentResult> InvokeAgentAsync(string agentName, IDiction
MaxTokens = executionSettings.MaxTokens,
PresencePenalty = executionSettings.PresencePenalty,
FrequencyPenalty = executionSettings.FrequencyPenalty,
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
ResponseFormat = executionSettings.ResponseFormat,
ChatSystemPrompt = agentConfig.PromptTemplate,
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

var promptExecutionSettingsDictionary = new Dictionary<string, PromptExecutionSettings>
Expand All @@ -46,10 +48,36 @@ public async Task<InvokeAgentResult> InvokeAgentAsync(string agentName, IDiction
AllowDangerouslySetContent = true
}).ToList()
};

var kernelFunction = kernel.CreateFunctionFromPrompt(promptTemplateConfig);

var templateFactory = new HandlebarsPromptTemplateFactory();

var promptConfig = new PromptTemplateConfig
{
Template = agentConfig.PromptTemplate,
TemplateFormat = "handlebars",
Name = agentConfig.FunctionName
};

var promptTemplate = templateFactory.Create(promptConfig);

var kernelArguments = new KernelArguments(input);
var result = await kernelFunction.InvokeAsync(kernel, kernelArguments, cancellationToken: cancellationToken);
return new(agentConfig, result);
string renderedPrompt = await promptTemplate.RenderAsync(kernel, kernelArguments);

ChatHistory chatHistory = [];
chatHistory.AddUserMessage(renderedPrompt);

IChatCompletionService chatCompletion = kernel.GetRequiredService<IChatCompletionService>();

OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};

var response = await chatCompletion.GetChatMessageContentAsync(
chatHistory,
executionSettings: openAIPromptExecutionSettings,
kernel: kernel);

return new(agentConfig, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Elsa.Agents;

public class KernelFactory(IPluginDiscoverer pluginDiscoverer, IServiceDiscoverer serviceDiscoverer, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, ILogger<KernelFactory> logger)
public class KernelFactory(IPluginDiscoverer pluginDiscoverer, IServiceDiscoverer serviceDiscoverer, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, ILogger<KernelFactory> logger) : IKernelFactory
{
public Kernel CreateKernel(KernelConfig kernelConfig, string agentName)
{
Expand Down
Loading