Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,7 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
# Local environment files
.env
.env.local
appsettings.local.json
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.13"/>
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.13"/>
Expand Down Expand Up @@ -103,8 +105,10 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3"/>
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3"/>
Expand Down Expand Up @@ -206,6 +210,7 @@
<PackageVersion Include="NuGet.Protocol" Version="7.0.3"/>
<PackageVersion Include="Octokit" Version="14.0.0"/>
<PackageVersion Include="Open.Linq.AsyncExtensions" Version="1.2.0"/>
<PackageVersion Include="OpenAI" Version="2.10.0"/>
<PackageVersion Include="OpenTelemetry.Api" Version="1.15.3"/>
<PackageVersion Include="Polly" Version="8.6.5"/>
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0"/>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Below is the current status of each extension. Icons indicate implementation pro
### 🤖 AI & Automation
| Status | Extension | Description | Module Name | Issue |
|--------|------------|-------------|-------------|-------|
| 🔲 | **OpenAI** | GPT-based text generation, chatbots | `Elsa.OpenAI` | [Open Issue](https://github.com/elsa-workflows/elsa-extensions/issues/new) |
| | **OpenAI** | GPT-based text generation, chatbots | `Elsa.OpenAI` | [PR #98](https://github.com/elsa-workflows/elsa-extensions/pull/98) |
| 🔲 | **Google AI** | AI-enhanced search, translation | `Elsa.GoogleAI` | [Open Issue](https://github.com/elsa-workflows/elsa-extensions/issues/new) |
| 🔲 | **AWS Comprehend** | NLP services for text analysis | `Elsa.AWSComprehend` | [Open Issue](https://github.com/elsa-workflows/elsa-extensions/issues/new) |
| 🔲 | **Azure AI** | Vision, speech, language processing | `Elsa.AzureAI` | [Open Issue](https://github.com/elsa-workflows/elsa-extensions/issues/new) |
Expand Down
94 changes: 94 additions & 0 deletions src/modules/llm/Elsa.OpenAI/Activities/Chat/CompleteChat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
using OpenAI.Chat;

namespace Elsa.OpenAI.Activities.Chat;

/// <summary>
/// Completes a chat conversation using OpenAI's Chat API.
/// Supports an optional system message, max token limit, and temperature control.
/// </summary>
[Activity(
"Elsa.OpenAI.Chat",
"OpenAI Chat",
"Completes a chat conversation using OpenAI's Chat API.",
DisplayName = "Complete Chat")]
[UsedImplicitly]
public class CompleteChat : OpenAIActivity
{
/// <summary>
/// The user message or prompt to complete.
/// </summary>
[Input(Description = "The user message or prompt to complete.")]
public Input<string> Prompt { get; set; } = null!;

/// <summary>
/// Optional system message to provide context or instructions.
/// </summary>
[Input(Description = "Optional system message to provide context or instructions.")]
public Input<string?> SystemMessage { get; set; } = null!;

/// <summary>
/// The maximum number of tokens to generate.
/// </summary>
[Input(Description = "The maximum number of tokens to generate.")]
public Input<int?> MaxTokens { get; set; } = null!;

/// <summary>
/// Controls randomness: 0.0 is deterministic, 1.0 is maximum randomness.
/// </summary>
[Input(Description = "Controls randomness: 0.0 is deterministic, 1.0 is maximum randomness.")]
public Input<float?> Temperature { get; set; } = null!;

/// <summary>
/// The completion result from the chat model.
/// </summary>
[Output(Description = "The completion result from the chat model.")]
public Output<string> Result { get; set; } = null!;

/// <summary>
/// The total tokens used in the request.
/// </summary>
[Output(Description = "The total tokens used in the request.")]
public Output<int?> TotalTokens { get; set; } = null!;

/// <summary>
/// The finish reason for the completion.
/// </summary>
[Output(Description = "The finish reason for the completion.")]
public Output<string?> FinishReason { get; set; } = null!;

/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var prompt = context.Get(Prompt)!;
var systemMessage = context.Get(SystemMessage);
var maxTokens = context.Get(MaxTokens);
var temperature = context.Get(Temperature);

var client = GetChatClient(context);

var messages = new List<ChatMessage>();

if (!string.IsNullOrWhiteSpace(systemMessage))
messages.Add(ChatMessage.CreateSystemMessage(systemMessage));

messages.Add(ChatMessage.CreateUserMessage(prompt));

var options = new ChatCompletionOptions();

if (maxTokens.HasValue)
options.MaxOutputTokenCount = maxTokens.Value;

if (temperature.HasValue)
options.Temperature = temperature.Value;

var completion = await client.CompleteChatAsync(messages, options);

context.Set(Result, completion.Value.Content[0]?.Text ?? string.Empty);
context.Set(TotalTokens, completion.Value.Usage?.TotalTokenCount);
context.Set(FinishReason, completion.Value.FinishReason.ToString());
}
}
88 changes: 88 additions & 0 deletions src/modules/llm/Elsa.OpenAI/Activities/OpenAIActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Elsa.OpenAI.Services;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using OpenAI;
using OpenAI.Audio;
using OpenAI.Chat;
using OpenAI.Embeddings;
using OpenAI.Images;
using OpenAI.Moderations;

namespace Elsa.OpenAI.Activities;

/// <summary>
/// Abstract base class for all OpenAI workflow activities.
/// Provides shared inputs for API key and model, plus convenience methods
/// for resolving typed OpenAI clients from the <see cref="OpenAIClientFactory"/>.
/// </summary>
public abstract class OpenAIActivity : Activity
{
/// <summary>
/// The OpenAI API key used to authenticate requests.
/// </summary>
[Input(Description = "The OpenAI API key.")]
public Input<string> ApiKey { get; set; } = null!;

/// <summary>
/// The OpenAI model identifier (e.g., "gpt-4o", "dall-e-3").
/// </summary>
[Input(Description = "The OpenAI model to use.")]
public Input<string> Model { get; set; } = null!;

/// <summary>
/// Resolves the <see cref="OpenAIClientFactory"/> from the current execution context.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>The registered <see cref="OpenAIClientFactory"/> instance.</returns>
protected static OpenAIClientFactory GetClientFactory(ActivityExecutionContext context) =>
context.GetRequiredService<OpenAIClientFactory>();

/// <summary>
/// Gets a base <see cref="OpenAIClient"/> using the configured API key.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>An <see cref="OpenAIClient"/> instance.</returns>
protected OpenAIClient GetClient(ActivityExecutionContext context) =>
GetClientFactory(context).GetClient(context.Get(ApiKey)!);

/// <summary>
/// Gets a <see cref="ChatClient"/> using the configured model and API key.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>A <see cref="ChatClient"/> instance.</returns>
protected ChatClient GetChatClient(ActivityExecutionContext context) =>
GetClientFactory(context).GetChatClient(context.Get(Model)!, context.Get(ApiKey)!);

/// <summary>
/// Gets an <see cref="ImageClient"/> using the configured model and API key.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>An <see cref="ImageClient"/> instance.</returns>
protected ImageClient GetImageClient(ActivityExecutionContext context) =>
GetClientFactory(context).GetImageClient(context.Get(Model)!, context.Get(ApiKey)!);

/// <summary>
/// Gets an <see cref="AudioClient"/> using the configured model and API key.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>An <see cref="AudioClient"/> instance.</returns>
protected AudioClient GetAudioClient(ActivityExecutionContext context) =>
GetClientFactory(context).GetAudioClient(context.Get(Model)!, context.Get(ApiKey)!);

/// <summary>
/// Gets an <see cref="EmbeddingClient"/> using the configured model and API key.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>An <see cref="EmbeddingClient"/> instance.</returns>
protected EmbeddingClient GetEmbeddingClient(ActivityExecutionContext context) =>
GetClientFactory(context).GetEmbeddingClient(context.Get(Model)!, context.Get(ApiKey)!);

/// <summary>
/// Gets a <see cref="ModerationClient"/> using the configured model and API key.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>A <see cref="ModerationClient"/> instance.</returns>
protected ModerationClient GetModerationClient(ActivityExecutionContext context) =>
GetClientFactory(context).GetModerationClient(context.Get(Model)!, context.Get(ApiKey)!);
}
13 changes: 13 additions & 0 deletions src/modules/llm/Elsa.OpenAI/Elsa.OpenAI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Provides OpenAI integration activities for Elsa Workflows</Description>
<PackageTags>elsa extension module openai ai chat completion</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Elsa" />
<PackageReference Include="OpenAI" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions src/modules/llm/Elsa.OpenAI/Features/OpenAIFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.OpenAI.Services;
using Microsoft.Extensions.DependencyInjection;

namespace Elsa.OpenAI.Features;

/// <summary>
/// Represents a feature for setting up OpenAI integration within the Elsa framework.
/// </summary>
public class OpenAIFeature(IModule module) : FeatureBase(module)
{
/// <summary>
/// Applies the feature to the specified service collection.
/// </summary>
public override void Apply() =>
Services
.AddSingleton<OpenAIClientFactory>();
}
4 changes: 4 additions & 0 deletions src/modules/llm/Elsa.OpenAI/FodyWeavers.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
Loading