Skip to content
Draft
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
20 changes: 20 additions & 0 deletions Elsa.Extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "diagnostics", "diagnostics"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.OpenTelemetry", "src\modules\diagnostics\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj", "{28D04FA3-4DCC-4137-8ED4-9F6F1A815909}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ai", "ai", "{E0C1E9D1-ECAF-4C86-B623-E3C748E82BCF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.AnthropicClaude", "src\modules\ai\Elsa.Integrations.AnthropicClaude\Elsa.Integrations.AnthropicClaude.csproj", "{A83B6B99-58AA-4DB2-801A-514361789759}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ai", "ai", "{5F052B47-53DB-43B8-852D-D52DC967DE7B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Integrations.AnthropicClaude.Tests", "test\modules\ai\Elsa.Integrations.AnthropicClaude.Tests\Elsa.Integrations.AnthropicClaude.Tests.csproj", "{793F0184-6F08-4D9A-8EC2-16A5560AD9B7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -631,6 +639,14 @@ Global
{28D04FA3-4DCC-4137-8ED4-9F6F1A815909}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28D04FA3-4DCC-4137-8ED4-9F6F1A815909}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28D04FA3-4DCC-4137-8ED4-9F6F1A815909}.Release|Any CPU.Build.0 = Release|Any CPU
{A83B6B99-58AA-4DB2-801A-514361789759}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A83B6B99-58AA-4DB2-801A-514361789759}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A83B6B99-58AA-4DB2-801A-514361789759}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A83B6B99-58AA-4DB2-801A-514361789759}.Release|Any CPU.Build.0 = Release|Any CPU
{793F0184-6F08-4D9A-8EC2-16A5560AD9B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{793F0184-6F08-4D9A-8EC2-16A5560AD9B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{793F0184-6F08-4D9A-8EC2-16A5560AD9B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{793F0184-6F08-4D9A-8EC2-16A5560AD9B7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -755,6 +771,10 @@ Global
{8D0EC628-350F-47FC-8B36-DD98B4B7CC93} = {30CF0330-4B09-4784-B499-46BED303810B}
{28D04FA3-4DCC-4137-8ED4-9F6F1A815909} = {8D0EC628-350F-47FC-8B36-DD98B4B7CC93}
{04265302-AF6B-4627-807C-DE9E1699D7C9} = {30CF0330-4B09-4784-B499-46BED303810B}
{E0C1E9D1-ECAF-4C86-B623-E3C748E82BCF} = {30CF0330-4B09-4784-B499-46BED303810B}
{A83B6B99-58AA-4DB2-801A-514361789759} = {E0C1E9D1-ECAF-4C86-B623-E3C748E82BCF}
{5F052B47-53DB-43B8-852D-D52DC967DE7B} = {3DDE6F89-531C-47F8-9CD7-7A4E6984FA48}
{793F0184-6F08-4D9A-8EC2-16A5560AD9B7} = {5F052B47-53DB-43B8-852D-D52DC967DE7B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {11A771DA-B728-445E-8A88-AE1C84C3B3A6}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Elsa.Integrations.AnthropicClaude.Services;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;

namespace Elsa.Integrations.AnthropicClaude.Activities;

/// <summary>
/// Base class for all Claude-related activities.
/// </summary>
public abstract class ClaudeActivity : Activity
{
/// <summary>
/// The Anthropic Claude API key.
/// </summary>
[Input(
Description = "The Anthropic Claude API key. Get your API key from https://console.anthropic.com/",
UIHint = "password")]
public Input<string> ApiKey { get; set; } = null!;

/// <summary>
/// Gets a configured Claude API client.
/// </summary>
/// <param name="context">The activity execution context.</param>
/// <returns>A configured Claude API client.</returns>
protected ClaudeApiClient GetClient(ActivityExecutionContext context)
{
var clientFactory = context.GetRequiredService<ClaudeClientFactory>();
var apiKey = context.Get(ApiKey)!;
return clientFactory.GetClient(apiKey);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using Elsa.Integrations.AnthropicClaude.Models;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using JetBrains.Annotations;

namespace Elsa.Integrations.AnthropicClaude.Activities;

/// <summary>
/// Creates a prompt and sends it to Claude AI for completion.
/// </summary>
[Activity(
"Elsa.AnthropicClaude.Completions",
"Anthropic Claude",
"Creates a prompt based on structured input messages and gets a response from Claude AI.",
DisplayName = "Create Prompt")]
[UsedImplicitly]
public class CreatePrompt : ClaudeActivity
{
/// <summary>
/// The model to use for the completion (e.g., claude-3-sonnet-20240229, claude-3-haiku-20240307).
/// </summary>
[Input(
Description = "The Claude model to use for completion. Popular options: claude-3-sonnet-20240229, claude-3-haiku-20240307, claude-3-opus-20240229",
DefaultValue = "claude-3-sonnet-20240229")]
public Input<string> Model { get; set; } = new("claude-3-sonnet-20240229");

/// <summary>
/// The system prompt that defines Claude's behavior and context.
/// </summary>
[Input(
Description = "System prompt that defines Claude's behavior, role, and context for the conversation.",
UIHint = "multiline")]
public Input<string?> SystemPrompt { get; set; } = null!;

/// <summary>
/// The user message or prompt to send to Claude.
/// </summary>
[Input(
Description = "The user message or prompt to send to Claude.",
UIHint = "multiline")]
public Input<string> UserMessage { get; set; } = null!;

/// <summary>
/// Previous messages in the conversation (JSON array of messages with 'role' and 'content' properties).
/// </summary>
[Input(
Description = "Previous messages in the conversation as a JSON array. Each message should have 'role' (user/assistant) and 'content' properties.",
UIHint = "multiline")]
public Input<string?> PreviousMessages { get; set; } = null!;

/// <summary>
/// Maximum number of tokens to generate in the response.
/// </summary>
[Input(
Description = "Maximum number of tokens to generate in the response (1-4096).",
DefaultValue = 1024)]
public Input<int> MaxTokens { get; set; } = new(1024);

/// <summary>
/// Temperature for controlling randomness (0.0 = focused, 1.0 = creative).
/// </summary>
[Input(
Description = "Temperature for controlling response randomness. 0.0 = very focused, 1.0 = very creative.",
DefaultValue = 0.7)]
public Input<double?> Temperature { get; set; } = new(0.7);

/// <summary>
/// Stop sequences that will end the generation when encountered.
/// </summary>
[Input(
Description = "Stop sequences that will end generation when encountered (comma-separated).")]
public Input<string?> StopSequences { get; set; } = null!;

/// <summary>
/// The generated response from Claude.
/// </summary>
[Output(Description = "The text response generated by Claude.")]
public Output<string> Response { get; set; } = null!;

/// <summary>
/// The full response object containing additional metadata.
/// </summary>
[Output(Description = "The complete response object with metadata including token usage.")]
public Output<ClaudeCompletionResponse> FullResponse { get; set; } = null!;

/// <summary>
/// The number of input tokens used.
/// </summary>
[Output(Description = "The number of input tokens used for this request.")]
public Output<int> InputTokens { get; set; } = null!;

/// <summary>
/// The number of output tokens generated.
/// </summary>
[Output(Description = "The number of output tokens generated in the response.")]
public Output<int> OutputTokens { get; set; } = null!;

/// <summary>
/// Executes the activity.
/// </summary>
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var model = context.Get(Model)!;
var systemPrompt = context.Get(SystemPrompt);
var userMessage = context.Get(UserMessage)!;
var previousMessagesJson = context.Get(PreviousMessages);
var maxTokens = context.Get(MaxTokens);
var temperature = context.Get(Temperature);
var stopSequencesInput = context.Get(StopSequences);

var client = GetClient(context);

// Build the messages list
var messages = new List<ClaudeMessage>();

// Add previous messages if provided
if (!string.IsNullOrWhiteSpace(previousMessagesJson))
{
try
{
var previousMessages = System.Text.Json.JsonSerializer.Deserialize<List<ClaudeMessage>>(previousMessagesJson);
if (previousMessages != null)
{
messages.AddRange(previousMessages);
}
}
catch (System.Text.Json.JsonException)
{
// If JSON parsing fails, ignore previous messages and continue
}
}

// Add the current user message
messages.Add(new ClaudeMessage
{
Role = "user",
Content = userMessage
});

// Parse stop sequences
List<string>? stopSequences = null;
if (!string.IsNullOrWhiteSpace(stopSequencesInput))
{
stopSequences = stopSequencesInput.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToList();
}

// Create the request
var request = new ClaudeCompletionRequest
{
Model = model,
MaxTokens = maxTokens,
Messages = messages,
System = systemPrompt,
Temperature = temperature,
StopSequences = stopSequences
};

// Make the API call
var response = await client.CreateCompletionAsync(request, context.CancellationToken);

// Extract the response text
var responseText = response.Content.FirstOrDefault()?.Text ?? string.Empty;

// Set outputs
context.Set(Response, responseText);
context.Set(FullResponse, response);
context.Set(InputTokens, response.Usage?.InputTokens ?? 0);
context.Set(OutputTokens, response.Usage?.OutputTokens ?? 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using JetBrains.Annotations;

namespace Elsa.Integrations.AnthropicClaude.Activities;

/// <summary>
/// Makes an arbitrary HTTP call to the Anthropic Claude API.
/// </summary>
[Activity(
"Elsa.AnthropicClaude.API",
"Anthropic Claude",
"Performs an arbitrary authorized API call to the Anthropic Claude API.",
DisplayName = "Make API Call")]
[UsedImplicitly]
public class MakeAPICall : ClaudeActivity
{
/// <summary>
/// The HTTP method to use for the API call.
/// </summary>
[Input(
Description = "The HTTP method to use (GET, POST, PUT, DELETE, etc.).",
DefaultValue = "POST")]
public Input<string> HttpMethod { get; set; } = new("POST");

/// <summary>
/// The API endpoint to call (relative to the Claude API base URL).
/// </summary>
[Input(
Description = "The API endpoint to call, relative to https://api.anthropic.com/v1/ (e.g., 'messages', 'models').",
DefaultValue = "messages")]
public Input<string> Endpoint { get; set; } = new("messages");

/// <summary>
/// The request body as JSON string (for POST, PUT requests).
/// </summary>
[Input(
Description = "The request body as a JSON string. Leave empty for GET requests.",
UIHint = "multiline")]
public Input<string?> RequestBody { get; set; } = null!;

/// <summary>
/// Whether to validate that the request body is valid JSON before sending.
/// </summary>
[Input(
Description = "Whether to validate that the request body is valid JSON before sending the request.",
DefaultValue = true)]
public Input<bool> ValidateJson { get; set; } = new(true);

/// <summary>
/// The raw response body from the API call.
/// </summary>
[Output(Description = "The raw response body from the Claude API call.")]
public Output<string> ResponseBody { get; set; } = null!;

/// <summary>
/// Indicates whether the API call was successful (2xx status code).
/// </summary>
[Output(Description = "True if the API call was successful (2xx status code), false otherwise.")]
public Output<bool> Success { get; set; } = null!;

/// <summary>
/// Executes the activity.
/// </summary>
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var httpMethodString = context.Get(HttpMethod)!;
var endpoint = context.Get(Endpoint)!;
var requestBody = context.Get(RequestBody);
var validateJson = context.Get(ValidateJson);

var client = GetClient(context);

// Validate HTTP method
if (!System.Net.Http.HttpMethod.TryParse(httpMethodString, out var method))
{
throw new ArgumentException($"Invalid HTTP method: {httpMethodString}");
}

// Validate JSON if requested and content is provided
if (validateJson && !string.IsNullOrWhiteSpace(requestBody))
{
try
{
System.Text.Json.JsonDocument.Parse(requestBody);
}
catch (System.Text.Json.JsonException ex)
{
throw new ArgumentException($"Invalid JSON in request body: {ex.Message}", ex);
}
}

// Ensure endpoint doesn't start with slash (we'll add it in the service)
if (endpoint.StartsWith("/"))
{
endpoint = endpoint.Substring(1);
}

try
{
// Make the API call
var responseBody = await client.MakeApiCallAsync(
method,
endpoint,
requestBody,
context.CancellationToken);

// Set outputs
context.Set(ResponseBody, responseBody);
context.Set(Success, true);
}
catch (HttpRequestException)
{
// For HTTP errors, we still want to set Success to false but not throw
// The error details should be in the exception message
context.Set(ResponseBody, string.Empty);
context.Set(Success, false);
throw; // Re-throw to let the workflow handle the error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Description>
Provides integration with Anthropic Claude AI for Elsa Workflows.
</Description>
<PackageTags>elsa extension module anthropic claude ai</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Elsa" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>

</Project>
Loading