diff --git a/Elsa.Extensions.sln b/Elsa.Extensions.sln
index fdf72b48..fc9e2d27 100644
--- a/Elsa.Extensions.sln
+++ b/Elsa.Extensions.sln
@@ -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
@@ -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
@@ -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}
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/ClaudeActivity.cs b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/ClaudeActivity.cs
new file mode 100644
index 00000000..b79b5cc4
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/ClaudeActivity.cs
@@ -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;
+
+///
+/// Base class for all Claude-related activities.
+///
+public abstract class ClaudeActivity : Activity
+{
+ ///
+ /// The Anthropic Claude API key.
+ ///
+ [Input(
+ Description = "The Anthropic Claude API key. Get your API key from https://console.anthropic.com/",
+ UIHint = "password")]
+ public Input ApiKey { get; set; } = null!;
+
+ ///
+ /// Gets a configured Claude API client.
+ ///
+ /// The activity execution context.
+ /// A configured Claude API client.
+ protected ClaudeApiClient GetClient(ActivityExecutionContext context)
+ {
+ var clientFactory = context.GetRequiredService();
+ var apiKey = context.Get(ApiKey)!;
+ return clientFactory.GetClient(apiKey);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/CreatePrompt.cs b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/CreatePrompt.cs
new file mode 100644
index 00000000..4bf4dc09
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/CreatePrompt.cs
@@ -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;
+
+///
+/// Creates a prompt and sends it to Claude AI for completion.
+///
+[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
+{
+ ///
+ /// The model to use for the completion (e.g., claude-3-sonnet-20240229, claude-3-haiku-20240307).
+ ///
+ [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 Model { get; set; } = new("claude-3-sonnet-20240229");
+
+ ///
+ /// The system prompt that defines Claude's behavior and context.
+ ///
+ [Input(
+ Description = "System prompt that defines Claude's behavior, role, and context for the conversation.",
+ UIHint = "multiline")]
+ public Input SystemPrompt { get; set; } = null!;
+
+ ///
+ /// The user message or prompt to send to Claude.
+ ///
+ [Input(
+ Description = "The user message or prompt to send to Claude.",
+ UIHint = "multiline")]
+ public Input UserMessage { get; set; } = null!;
+
+ ///
+ /// Previous messages in the conversation (JSON array of messages with 'role' and 'content' properties).
+ ///
+ [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 PreviousMessages { get; set; } = null!;
+
+ ///
+ /// Maximum number of tokens to generate in the response.
+ ///
+ [Input(
+ Description = "Maximum number of tokens to generate in the response (1-4096).",
+ DefaultValue = 1024)]
+ public Input MaxTokens { get; set; } = new(1024);
+
+ ///
+ /// Temperature for controlling randomness (0.0 = focused, 1.0 = creative).
+ ///
+ [Input(
+ Description = "Temperature for controlling response randomness. 0.0 = very focused, 1.0 = very creative.",
+ DefaultValue = 0.7)]
+ public Input Temperature { get; set; } = new(0.7);
+
+ ///
+ /// Stop sequences that will end the generation when encountered.
+ ///
+ [Input(
+ Description = "Stop sequences that will end generation when encountered (comma-separated).")]
+ public Input StopSequences { get; set; } = null!;
+
+ ///
+ /// The generated response from Claude.
+ ///
+ [Output(Description = "The text response generated by Claude.")]
+ public Output Response { get; set; } = null!;
+
+ ///
+ /// The full response object containing additional metadata.
+ ///
+ [Output(Description = "The complete response object with metadata including token usage.")]
+ public Output FullResponse { get; set; } = null!;
+
+ ///
+ /// The number of input tokens used.
+ ///
+ [Output(Description = "The number of input tokens used for this request.")]
+ public Output InputTokens { get; set; } = null!;
+
+ ///
+ /// The number of output tokens generated.
+ ///
+ [Output(Description = "The number of output tokens generated in the response.")]
+ public Output OutputTokens { get; set; } = null!;
+
+ ///
+ /// Executes the activity.
+ ///
+ 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();
+
+ // Add previous messages if provided
+ if (!string.IsNullOrWhiteSpace(previousMessagesJson))
+ {
+ try
+ {
+ var previousMessages = System.Text.Json.JsonSerializer.Deserialize>(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? 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);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/MakeAPICall.cs b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/MakeAPICall.cs
new file mode 100644
index 00000000..a4fb70b1
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Activities/MakeAPICall.cs
@@ -0,0 +1,122 @@
+using Elsa.Workflows;
+using Elsa.Workflows.Attributes;
+using Elsa.Workflows.Models;
+using JetBrains.Annotations;
+
+namespace Elsa.Integrations.AnthropicClaude.Activities;
+
+///
+/// Makes an arbitrary HTTP call to the Anthropic Claude API.
+///
+[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
+{
+ ///
+ /// The HTTP method to use for the API call.
+ ///
+ [Input(
+ Description = "The HTTP method to use (GET, POST, PUT, DELETE, etc.).",
+ DefaultValue = "POST")]
+ public Input HttpMethod { get; set; } = new("POST");
+
+ ///
+ /// The API endpoint to call (relative to the Claude API base URL).
+ ///
+ [Input(
+ Description = "The API endpoint to call, relative to https://api.anthropic.com/v1/ (e.g., 'messages', 'models').",
+ DefaultValue = "messages")]
+ public Input Endpoint { get; set; } = new("messages");
+
+ ///
+ /// The request body as JSON string (for POST, PUT requests).
+ ///
+ [Input(
+ Description = "The request body as a JSON string. Leave empty for GET requests.",
+ UIHint = "multiline")]
+ public Input RequestBody { get; set; } = null!;
+
+ ///
+ /// Whether to validate that the request body is valid JSON before sending.
+ ///
+ [Input(
+ Description = "Whether to validate that the request body is valid JSON before sending the request.",
+ DefaultValue = true)]
+ public Input ValidateJson { get; set; } = new(true);
+
+ ///
+ /// The raw response body from the API call.
+ ///
+ [Output(Description = "The raw response body from the Claude API call.")]
+ public Output ResponseBody { get; set; } = null!;
+
+ ///
+ /// Indicates whether the API call was successful (2xx status code).
+ ///
+ [Output(Description = "True if the API call was successful (2xx status code), false otherwise.")]
+ public Output Success { get; set; } = null!;
+
+ ///
+ /// Executes the activity.
+ ///
+ 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
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Elsa.Integrations.AnthropicClaude.csproj b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Elsa.Integrations.AnthropicClaude.csproj
new file mode 100644
index 00000000..3e228ea3
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Elsa.Integrations.AnthropicClaude.csproj
@@ -0,0 +1,18 @@
+ο»Ώ
+
+
+ net8.0
+
+
+ Provides integration with Anthropic Claude AI for Elsa Workflows.
+
+ elsa extension module anthropic claude ai
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Extensions/ModuleExtensions.cs b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Extensions/ModuleExtensions.cs
new file mode 100644
index 00000000..67363c03
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Extensions/ModuleExtensions.cs
@@ -0,0 +1,22 @@
+using Elsa.Features.Abstractions;
+using Elsa.Integrations.AnthropicClaude.Features;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+///
+/// Extensions for configuring Anthropic Claude integration.
+///
+public static class ModuleExtensions
+{
+ ///
+ /// Adds Anthropic Claude integration to Elsa.
+ ///
+ /// The Elsa module.
+ /// The module for further configuration.
+ public static IModule UseAnthropicClaude(this IModule module)
+ {
+ module.Configure();
+ return module;
+ }
+}
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Features/AnthropicClaudeFeature.cs b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Features/AnthropicClaudeFeature.cs
new file mode 100644
index 00000000..933faa00
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Features/AnthropicClaudeFeature.cs
@@ -0,0 +1,27 @@
+using Elsa.Features.Abstractions;
+using Elsa.Features.Services;
+using Elsa.Integrations.AnthropicClaude.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Integrations.AnthropicClaude.Features;
+
+///
+/// Feature for setting up Anthropic Claude integration within the Elsa framework.
+///
+public class AnthropicClaudeFeature(IModule module) : FeatureBase(module)
+{
+ ///
+ /// Applies the feature to the specified service collection.
+ ///
+ public override void Apply()
+ {
+ Services
+ .AddHttpClient("Claude", client =>
+ {
+ client.BaseAddress = new Uri("https://api.anthropic.com/v1/");
+ client.Timeout = TimeSpan.FromMinutes(5); // Claude API calls can take time
+ })
+ .Services
+ .AddSingleton();
+ }
+}
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/FodyWeavers.xml b/src/modules/ai/Elsa.Integrations.AnthropicClaude/FodyWeavers.xml
new file mode 100644
index 00000000..822ae5e5
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/FodyWeavers.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Models/ClaudeModels.cs b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Models/ClaudeModels.cs
new file mode 100644
index 00000000..e48aa4cd
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Models/ClaudeModels.cs
@@ -0,0 +1,153 @@
+using System.Text.Json.Serialization;
+
+namespace Elsa.Integrations.AnthropicClaude.Models;
+
+///
+/// Represents a message in a Claude conversation.
+///
+public class ClaudeMessage
+{
+ ///
+ /// The role of the message sender (user, assistant, or system).
+ ///
+ [JsonPropertyName("role")]
+ public string Role { get; set; } = null!;
+
+ ///
+ /// The content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public string Content { get; set; } = null!;
+}
+
+///
+/// Represents a request to the Claude API for creating completions.
+///
+public class ClaudeCompletionRequest
+{
+ ///
+ /// The model to use for the completion.
+ ///
+ [JsonPropertyName("model")]
+ public string Model { get; set; } = "claude-3-sonnet-20240229";
+
+ ///
+ /// Maximum number of tokens to generate.
+ ///
+ [JsonPropertyName("max_tokens")]
+ public int MaxTokens { get; set; } = 1024;
+
+ ///
+ /// The messages to send to Claude.
+ ///
+ [JsonPropertyName("messages")]
+ public List Messages { get; set; } = new();
+
+ ///
+ /// System prompt for the conversation.
+ ///
+ [JsonPropertyName("system")]
+ public string? System { get; set; }
+
+ ///
+ /// Temperature for response randomness (0.0 to 1.0).
+ ///
+ [JsonPropertyName("temperature")]
+ public double? Temperature { get; set; }
+
+ ///
+ /// Stop sequences to end generation.
+ ///
+ [JsonPropertyName("stop_sequences")]
+ public List? StopSequences { get; set; }
+}
+
+///
+/// Represents the content of a Claude API response.
+///
+public class ClaudeResponseContent
+{
+ ///
+ /// The type of content (usually "text").
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = null!;
+
+ ///
+ /// The text content of the response.
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; set; } = null!;
+}
+
+///
+/// Represents a response from the Claude API.
+///
+public class ClaudeCompletionResponse
+{
+ ///
+ /// The unique identifier for this response.
+ ///
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = null!;
+
+ ///
+ /// The type of object returned.
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = null!;
+
+ ///
+ /// The role of the responder (should be "assistant").
+ ///
+ [JsonPropertyName("role")]
+ public string Role { get; set; } = null!;
+
+ ///
+ /// The content of the response.
+ ///
+ [JsonPropertyName("content")]
+ public List Content { get; set; } = new();
+
+ ///
+ /// The model that generated this response.
+ ///
+ [JsonPropertyName("model")]
+ public string Model { get; set; } = null!;
+
+ ///
+ /// The reason the response ended.
+ ///
+ [JsonPropertyName("stop_reason")]
+ public string? StopReason { get; set; }
+
+ ///
+ /// The stop sequence that ended the response, if any.
+ ///
+ [JsonPropertyName("stop_sequence")]
+ public string? StopSequence { get; set; }
+
+ ///
+ /// Usage statistics for this response.
+ ///
+ [JsonPropertyName("usage")]
+ public ClaudeUsage? Usage { get; set; }
+}
+
+///
+/// Represents usage statistics from a Claude API response.
+///
+public class ClaudeUsage
+{
+ ///
+ /// Number of input tokens used.
+ ///
+ [JsonPropertyName("input_tokens")]
+ public int InputTokens { get; set; }
+
+ ///
+ /// Number of output tokens generated.
+ ///
+ [JsonPropertyName("output_tokens")]
+ public int OutputTokens { get; set; }
+}
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/README.md b/src/modules/ai/Elsa.Integrations.AnthropicClaude/README.md
new file mode 100644
index 00000000..ac2567d0
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/README.md
@@ -0,0 +1,212 @@
+# Elsa.Integrations.AnthropicClaude
+
+This package extends [Elsa Workflows](https://github.com/elsa-workflows/elsa-core) with support for **Anthropic Claude AI**. It introduces custom activities that make it easy to integrate Claude's powerful language models directly into your workflow logic.
+
+## β¨ Key Features
+
+- **CreatePrompt** - Creates structured prompts and gets AI-generated responses from Claude
+- **MakeAPICall** - Performs arbitrary authorized API calls to the Anthropic Claude API
+- Support for all Claude models (Claude-3 Sonnet, Haiku, Opus)
+- Configurable temperature, token limits, and stop sequences
+- Comprehensive error handling and logging
+- Token usage tracking for cost monitoring
+
+---
+
+## β‘ Getting Started
+
+### π Prerequisites
+
+- Elsa Workflows **3.7.0+** installed in your project
+- Anthropic Claude API key from [Anthropic Console](https://console.anthropic.com/)
+
+### π Installation
+
+Add the Anthropic Claude extension to your project:
+
+```bash
+dotnet add package Elsa.Integrations.AnthropicClaude
+```
+
+### π§ Registration
+
+Register the Anthropic Claude extension in your Elsa builder:
+
+```csharp
+// Program.cs or Startup.cs
+services
+ .AddElsa(elsa =>
+ {
+ elsa.UseAnthropicClaude();
+ // Other Elsa configurations...
+ });
+```
+
+## π Authentication
+
+The Claude activities require an API key from Anthropic for authentication. You can obtain an API key from the [Anthropic Console](https://console.anthropic.com/).
+
+**Important**: Store your API key securely using Elsa's secrets management system or environment variables. Never hard-code API keys in your workflows.
+
+---
+
+## π Available Activities
+
+### CreatePrompt
+
+Creates a structured prompt and sends it to Claude AI for completion.
+
+**Inputs:**
+- **ApiKey** (required) - Your Anthropic Claude API key
+- **Model** - Claude model to use (default: claude-3-sonnet-20240229)
+- **UserMessage** (required) - The user message/prompt to send
+- **SystemPrompt** - System prompt that defines Claude's behavior
+- **PreviousMessages** - JSON array of previous conversation messages
+- **MaxTokens** - Maximum tokens to generate (default: 1024)
+- **Temperature** - Response randomness 0.0-1.0 (default: 0.7)
+- **StopSequences** - Comma-separated stop sequences
+
+**Outputs:**
+- **Response** - The text response from Claude
+- **FullResponse** - Complete response object with metadata
+- **InputTokens** - Number of input tokens used
+- **OutputTokens** - Number of output tokens generated
+
+### MakeAPICall
+
+Performs an arbitrary HTTP call to the Anthropic Claude API.
+
+**Inputs:**
+- **ApiKey** (required) - Your Anthropic Claude API key
+- **HttpMethod** - HTTP method to use (default: POST)
+- **Endpoint** - API endpoint relative to base URL (default: messages)
+- **RequestBody** - JSON request body for POST/PUT requests
+- **ValidateJson** - Whether to validate JSON before sending (default: true)
+
+**Outputs:**
+- **ResponseBody** - Raw response body from the API
+- **Success** - Whether the call was successful (2xx status)
+
+---
+
+## π Example Usage
+
+### Basic Text Generation
+
+```csharp
+// Workflow definition example
+public class ClaudeWorkflow : WorkflowBase
+{
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ builder
+ .Root(prompt => prompt
+ .Set(x => x.ApiKey, "your-api-key-here")
+ .Set(x => x.UserMessage, "Explain quantum computing in simple terms")
+ .Set(x => x.SystemPrompt, "You are a helpful science teacher.")
+ .Set(x => x.MaxTokens, 500))
+ .Then(write => write
+ .Set(x => x.Text, prompt => prompt.Response));
+ }
+}
+```
+
+### Conversation with Context
+
+```csharp
+public class ConversationWorkflow : WorkflowBase
+{
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ var previousMessages = """
+ [
+ {"role": "user", "content": "What is machine learning?"},
+ {"role": "assistant", "content": "Machine learning is a subset of AI..."}
+ ]
+ """;
+
+ builder
+ .Root(prompt => prompt
+ .Set(x => x.ApiKey, context => context.GetVariable("ClaudeApiKey"))
+ .Set(x => x.UserMessage, "Can you give me a practical example?")
+ .Set(x => x.PreviousMessages, previousMessages)
+ .Set(x => x.Temperature, 0.5));
+ }
+}
+```
+
+### Custom API Call
+
+```csharp
+public class CustomApiWorkflow : WorkflowBase
+{
+ protected override void Build(IWorkflowBuilder builder)
+ {
+ var customRequest = """
+ {
+ "model": "claude-3-haiku-20240307",
+ "max_tokens": 100,
+ "messages": [
+ {"role": "user", "content": "Hello Claude!"}
+ ]
+ }
+ """;
+
+ builder
+ .Root(call => call
+ .Set(x => x.ApiKey, "your-api-key-here")
+ .Set(x => x.Endpoint, "messages")
+ .Set(x => x.RequestBody, customRequest))
+ .Then(write => write
+ .Set(x => x.Text, call => call.ResponseBody));
+ }
+}
+```
+
+---
+
+## ποΈ Notes & Best Practices
+
+### Model Selection
+- **claude-3-opus-20240229** - Most capable, highest cost, slowest
+- **claude-3-sonnet-20240229** - Balanced performance and cost (recommended)
+- **claude-3-haiku-20240307** - Fastest, lowest cost, good for simple tasks
+
+### Token Management
+- Monitor `InputTokens` and `OutputTokens` to track API usage costs
+- Set appropriate `MaxTokens` limits to control response length
+- Use shorter prompts when possible to reduce input token costs
+
+### Error Handling
+- Activities will throw exceptions for API errors (invalid keys, rate limits, etc.)
+- Use Elsa's error handling activities to gracefully handle failures
+- Check the `Success` output on `MakeAPICall` for custom error handling
+
+### Security
+- Always use Elsa's secrets management for API keys
+- Never log or expose API keys in workflow outputs
+- Consider using environment-specific API keys for different deployment stages
+
+---
+
+## π References
+
+- [Anthropic Claude API Documentation](https://docs.anthropic.com/)
+- [Anthropic Console](https://console.anthropic.com/)
+- [Claude Model Comparison](https://docs.anthropic.com/claude/docs/models-overview)
+- [Elsa Workflows Documentation](https://elsa-workflows.github.io/elsa-core/)
+
+---
+
+## πΊοΈ Planned Features
+
+- [ ] Add streaming response support
+- [ ] Add function calling capabilities
+- [ ] Add image input support (when available)
+- [ ] Add async retry/backoff support
+- [ ] Add batch processing activities
+
+---
+
+This extension was developed to add Anthropic Claude AI functionality to Elsa Workflows.
+If you have ideas for improvement, encounter issues, or want to share how you're using it, feel free to open an issue or start a discussion!
\ No newline at end of file
diff --git a/src/modules/ai/Elsa.Integrations.AnthropicClaude/Services/ClaudeApiClient.cs b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Services/ClaudeApiClient.cs
new file mode 100644
index 00000000..ca447cb1
--- /dev/null
+++ b/src/modules/ai/Elsa.Integrations.AnthropicClaude/Services/ClaudeApiClient.cs
@@ -0,0 +1,166 @@
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using Elsa.Integrations.AnthropicClaude.Models;
+using Microsoft.Extensions.Logging;
+
+namespace Elsa.Integrations.AnthropicClaude.Services;
+
+///
+/// Service for communicating with the Anthropic Claude API.
+///
+public class ClaudeApiClient
+{
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+ private const string BaseUrl = "https://api.anthropic.com/v1";
+
+ ///
+ /// Initializes a new instance of the Claude API client.
+ ///
+ /// The HTTP client to use for API calls.
+ /// The logger instance.
+ public ClaudeApiClient(HttpClient httpClient, ILogger logger)
+ {
+ _httpClient = httpClient;
+ _logger = logger;
+
+ // Set base address if not already set
+ if (_httpClient.BaseAddress == null)
+ {
+ _httpClient.BaseAddress = new Uri(BaseUrl);
+ }
+ }
+
+ ///
+ /// Configures the HTTP client with the provided API key.
+ ///
+ /// The Anthropic API key.
+ public void SetApiKey(string apiKey)
+ {
+ _httpClient.DefaultRequestHeaders.Clear();
+ _httpClient.DefaultRequestHeaders.Add("x-api-key", apiKey);
+ _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
+ _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ }
+
+ ///
+ /// Creates a completion using the Claude API.
+ ///
+ /// The completion request.
+ /// Cancellation token.
+ /// The completion response.
+ public async Task CreateCompletionAsync(ClaudeCompletionRequest request, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
+ });
+
+ _logger.LogDebug("Sending Claude API request: {Request}", json);
+
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync("/messages", content, cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
+ _logger.LogError("Claude API request failed with status {StatusCode}: {Error}",
+ response.StatusCode, errorContent);
+ throw new HttpRequestException($"Claude API request failed: {response.StatusCode} - {errorContent}");
+ }
+
+ var responseJson = await response.Content.ReadAsStringAsync(cancellationToken);
+ _logger.LogDebug("Received Claude API response: {Response}", responseJson);
+
+ var result = JsonSerializer.Deserialize(responseJson, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
+ });
+
+ return result ?? throw new InvalidOperationException("Failed to deserialize Claude API response");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error occurred while calling Claude API");
+ throw;
+ }
+ }
+
+ ///
+ /// Makes an arbitrary HTTP request to the Claude API.
+ ///
+ /// The HTTP method to use.
+ /// The API endpoint (relative to base URL).
+ /// The request content (JSON string).
+ /// Cancellation token.
+ /// The response content as a string.
+ public async Task MakeApiCallAsync(HttpMethod method, string endpoint, string? content = null, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var request = new HttpRequestMessage(method, endpoint);
+
+ if (!string.IsNullOrEmpty(content))
+ {
+ request.Content = new StringContent(content, Encoding.UTF8, "application/json");
+ }
+
+ _logger.LogDebug("Making Claude API call: {Method} {Endpoint}", method, endpoint);
+
+ var response = await _httpClient.SendAsync(request, cancellationToken);
+ var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogError("Claude API call failed with status {StatusCode}: {Error}",
+ response.StatusCode, responseContent);
+ throw new HttpRequestException($"Claude API call failed: {response.StatusCode} - {responseContent}");
+ }
+
+ _logger.LogDebug("Claude API call successful: {Response}", responseContent);
+ return responseContent;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error occurred while making Claude API call");
+ throw;
+ }
+ }
+}
+
+///
+/// Factory for creating Claude API clients with proper configuration.
+///
+public class ClaudeClientFactory
+{
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the Claude client factory.
+ ///
+ /// The HTTP client factory.
+ /// The logger instance.
+ public ClaudeClientFactory(IHttpClientFactory httpClientFactory, ILogger logger)
+ {
+ _httpClientFactory = httpClientFactory;
+ _logger = logger;
+ }
+
+ ///
+ /// Creates a Claude API client configured with the specified API key.
+ ///
+ /// The Anthropic API key.
+ /// A configured Claude API client.
+ public ClaudeApiClient GetClient(string apiKey)
+ {
+ var httpClient = _httpClientFactory.CreateClient("Claude");
+ var client = new ClaudeApiClient(httpClient, _logger);
+ client.SetApiKey(apiKey);
+ return client;
+ }
+}
\ No newline at end of file
diff --git a/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeApiClientTests.cs b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeApiClientTests.cs
new file mode 100644
index 00000000..fd32b5ba
--- /dev/null
+++ b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeApiClientTests.cs
@@ -0,0 +1,80 @@
+using Elsa.Integrations.AnthropicClaude.Services;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+
+namespace Elsa.Integrations.AnthropicClaude.Tests;
+
+///
+/// Tests for Claude API client functionality.
+///
+public class ClaudeApiClientTests
+{
+ private readonly ClaudeApiClient _client;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ public ClaudeApiClientTests()
+ {
+ _httpClient = new HttpClient();
+ _logger = Substitute.For>();
+ _client = new ClaudeApiClient(_httpClient, _logger);
+ }
+
+ [Fact]
+ public void SetApiKey_ConfiguresHttpClientHeaders_Correctly()
+ {
+ // Arrange
+ const string apiKey = "test-api-key";
+
+ // Act
+ _client.SetApiKey(apiKey);
+
+ // Assert
+ Assert.Contains(_httpClient.DefaultRequestHeaders, h =>
+ h.Key == "x-api-key" && h.Value.Contains(apiKey));
+ Assert.Contains(_httpClient.DefaultRequestHeaders, h =>
+ h.Key == "anthropic-version" && h.Value.Contains("2023-06-01"));
+ Assert.True(_httpClient.DefaultRequestHeaders.Accept.Any(a =>
+ a.MediaType == "application/json"));
+ }
+
+ [Fact]
+ public void Constructor_SetsBaseAddress_WhenNotAlreadySet()
+ {
+ // Arrange
+ var httpClient = new HttpClient();
+ var logger = Substitute.For>();
+
+ // Act
+ var client = new ClaudeApiClient(httpClient, logger);
+
+ // Assert
+ Assert.Equal("https://api.anthropic.com/v1/", httpClient.BaseAddress?.ToString());
+ }
+
+ [Fact]
+ public void Constructor_DoesNotOverrideBaseAddress_WhenAlreadySet()
+ {
+ // Arrange
+ var httpClient = new HttpClient { BaseAddress = new Uri("https://custom.api.com/") };
+ var logger = Substitute.For>();
+
+ // Act
+ var client = new ClaudeApiClient(httpClient, logger);
+
+ // Assert
+ Assert.Equal("https://custom.api.com/", httpClient.BaseAddress?.ToString());
+ }
+
+ [Fact]
+ public void Dispose_DisposesHttpClient()
+ {
+ // Arrange
+ var httpClient = new HttpClient();
+ var logger = Substitute.For>();
+ var client = new ClaudeApiClient(httpClient, logger);
+
+ // Act & Assert - Should not throw
+ httpClient.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeClientFactoryTests.cs b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeClientFactoryTests.cs
new file mode 100644
index 00000000..2b95f031
--- /dev/null
+++ b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeClientFactoryTests.cs
@@ -0,0 +1,76 @@
+using Elsa.Integrations.AnthropicClaude.Services;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+
+namespace Elsa.Integrations.AnthropicClaude.Tests;
+
+///
+/// Tests for Claude client factory functionality.
+///
+public class ClaudeClientFactoryTests
+{
+ [Fact]
+ public void GetClient_ReturnsConfiguredClient_WithApiKey()
+ {
+ // Arrange
+ var httpClientFactory = Substitute.For();
+ var logger = Substitute.For>();
+ var httpClient = new HttpClient();
+
+ httpClientFactory.CreateClient("Claude").Returns(httpClient);
+
+ var factory = new ClaudeClientFactory(httpClientFactory, logger);
+ const string apiKey = "test-api-key";
+
+ // Act
+ var client = factory.GetClient(apiKey);
+
+ // Assert
+ Assert.NotNull(client);
+
+ // Verify the HTTP client was configured with the API key
+ Assert.Contains(httpClient.DefaultRequestHeaders, h =>
+ h.Key == "x-api-key" && h.Value.Contains(apiKey));
+ }
+
+ [Fact]
+ public void GetClient_CallsHttpClientFactory_WithCorrectName()
+ {
+ // Arrange
+ var httpClientFactory = Substitute.For();
+ var logger = Substitute.For>();
+ var httpClient = new HttpClient();
+
+ httpClientFactory.CreateClient("Claude").Returns(httpClient);
+
+ var factory = new ClaudeClientFactory(httpClientFactory, logger);
+
+ // Act
+ factory.GetClient("test-key");
+
+ // Assert
+ httpClientFactory.Received(1).CreateClient("Claude");
+ }
+
+ [Fact]
+ public void GetClient_ReturnsNewInstance_EachTime()
+ {
+ // Arrange
+ var httpClientFactory = Substitute.For();
+ var logger = Substitute.For>();
+
+ httpClientFactory.CreateClient("Claude").Returns(new HttpClient(), new HttpClient());
+
+ var factory = new ClaudeClientFactory(httpClientFactory, logger);
+
+ // Act
+ var client1 = factory.GetClient("test-key-1");
+ var client2 = factory.GetClient("test-key-2");
+
+ // Assert
+ Assert.NotNull(client1);
+ Assert.NotNull(client2);
+ // Note: We can't directly compare instances since they wrap different HttpClient instances
+ httpClientFactory.Received(2).CreateClient("Claude");
+ }
+}
\ No newline at end of file
diff --git a/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeModelsTests.cs b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeModelsTests.cs
new file mode 100644
index 00000000..7db43954
--- /dev/null
+++ b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/ClaudeModelsTests.cs
@@ -0,0 +1,143 @@
+using Elsa.Integrations.AnthropicClaude.Models;
+using System.Text.Json;
+
+namespace Elsa.Integrations.AnthropicClaude.Tests;
+
+///
+/// Tests for Claude model serialization and deserialization.
+///
+public class ClaudeModelsTests
+{
+ [Fact]
+ public void ClaudeMessage_SerializesToJson_Correctly()
+ {
+ // Arrange
+ var message = new ClaudeMessage
+ {
+ Role = "user",
+ Content = "Hello, Claude!"
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(message);
+
+ // Assert
+ Assert.Contains("\"role\":\"user\"", json);
+ Assert.Contains("\"content\":\"Hello, Claude!\"", json);
+ }
+
+ [Fact]
+ public void ClaudeCompletionRequest_SerializesToJson_WithSnakeCaseNaming()
+ {
+ // Arrange
+ var request = new ClaudeCompletionRequest
+ {
+ Model = "claude-3-sonnet-20240229",
+ MaxTokens = 1024,
+ Messages = new List
+ {
+ new() { Role = "user", Content = "Test message" }
+ },
+ Temperature = 0.7
+ };
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(request, options);
+
+ // Assert
+ Assert.Contains("\"max_tokens\":1024", json);
+ Assert.Contains("\"model\":\"claude-3-sonnet-20240229\"", json);
+ Assert.Contains("\"temperature\":0.7", json);
+ Assert.Contains("\"messages\":", json);
+ }
+
+ [Fact]
+ public void ClaudeCompletionResponse_DeserializesFromJson_Correctly()
+ {
+ // Arrange
+ var json = """
+ {
+ "id": "msg_123",
+ "type": "message",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "text",
+ "text": "Hello! How can I help you today?"
+ }
+ ],
+ "model": "claude-3-sonnet-20240229",
+ "stop_reason": "end_turn",
+ "usage": {
+ "input_tokens": 10,
+ "output_tokens": 20
+ }
+ }
+ """;
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
+ };
+
+ // Act
+ var response = JsonSerializer.Deserialize(json, options);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal("msg_123", response.Id);
+ Assert.Equal("message", response.Type);
+ Assert.Equal("assistant", response.Role);
+ Assert.Equal("claude-3-sonnet-20240229", response.Model);
+ Assert.Equal("end_turn", response.StopReason);
+
+ Assert.NotNull(response.Content);
+ Assert.Single(response.Content);
+ Assert.Equal("text", response.Content[0].Type);
+ Assert.Equal("Hello! How can I help you today?", response.Content[0].Text);
+
+ Assert.NotNull(response.Usage);
+ Assert.Equal(10, response.Usage.InputTokens);
+ Assert.Equal(20, response.Usage.OutputTokens);
+ }
+
+ [Fact]
+ public void ClaudeCompletionRequest_WithNullValues_SerializesCorrectly()
+ {
+ // Arrange
+ var request = new ClaudeCompletionRequest
+ {
+ Model = "claude-3-haiku-20240307",
+ MaxTokens = 512,
+ Messages = new List
+ {
+ new() { Role = "user", Content = "Simple test" }
+ },
+ System = null,
+ Temperature = null,
+ StopSequences = null
+ };
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(request, options);
+
+ // Assert
+ Assert.DoesNotContain("\"system\":", json);
+ Assert.DoesNotContain("\"temperature\":", json);
+ Assert.DoesNotContain("\"stop_sequences\":", json);
+ Assert.Contains("\"model\":\"claude-3-haiku-20240307\"", json);
+ Assert.Contains("\"max_tokens\":512", json);
+ }
+}
\ No newline at end of file
diff --git a/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/Elsa.Integrations.AnthropicClaude.Tests.csproj b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/Elsa.Integrations.AnthropicClaude.Tests.csproj
new file mode 100644
index 00000000..a6f409b9
--- /dev/null
+++ b/test/modules/ai/Elsa.Integrations.AnthropicClaude.Tests/Elsa.Integrations.AnthropicClaude.Tests.csproj
@@ -0,0 +1,22 @@
+ο»Ώ
+
+
+ net8.0
+
+
+ Unit tests for Elsa Anthropic Claude integration.
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file