diff --git a/GermanApp/Application/Models/MistralRequest.cs b/GermanApp/Application/Models/MistralRequest.cs
new file mode 100644
index 0000000..f4ebb4b
--- /dev/null
+++ b/GermanApp/Application/Models/MistralRequest.cs
@@ -0,0 +1,236 @@
+namespace GermanApp.Application.Models;
+
+///
+/// Request model for Mistral completion API.
+/// Used for text generation (stories, feedback, etc.).
+///
+///
+/// See: https://docs.mistral.ai/api/#operation/createCompletion
+///
+public record MistralRequest
+{
+ ///
+ /// ID of the model to use.
+ ///
+ public string Model { get; init; } = "mistral-medium";
+
+ ///
+ /// The prompt(s) to generate completions for.
+ ///
+ public string Prompt { get; init; } = string.Empty;
+
+ ///
+ /// The maximum number of tokens to generate in the completion.
+ /// Default: 256, Max: 2048 for mistral-medium
+ ///
+ public int? MaxTokens { get; init; } = 512;
+
+ ///
+ /// What sampling temperature to use.
+ /// Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer.
+ /// Default: 0.7
+ ///
+ public double? Temperature { get; init; } = 0.7;
+
+ ///
+ /// Nucleus sampling, where the model considers the results of the tokens with top_p probability mass.
+ /// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
+ /// Default: 1.0
+ ///
+ public double? TopP { get; init; } = 1.0;
+
+ ///
+ /// How many completions to generate for each prompt.
+ /// Default: 1
+ ///
+ public int? N { get; init; } = 1;
+
+ ///
+ /// Whether to stream partial responses.
+ /// Not supported by this connector (use streaming endpoint separately if needed).
+ ///
+ public bool? Stream { get; init; } = false;
+
+ ///
+ /// Whether to echo the prompt in the response.
+ /// Default: false
+ ///
+ public bool? Echo { get; init; } = false;
+
+ ///
+ /// Up to 4 sequences where the API will stop generating further tokens.
+ ///
+ public IReadOnlyList? Stop { get; init; }
+
+ ///
+ /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their frequency in the text so far.
+ ///
+ public double? FrequencyPenalty { get; init; }
+
+ ///
+ /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far.
+ ///
+ public double? PresencePenalty { get; init; }
+
+ ///
+ /// User identifier for tracking usage.
+ ///
+ public string? User { get; init; }
+
+ ///
+ /// Factory method to create a completion request with default settings.
+ ///
+ public static MistralRequest CreateCompletion(string prompt, string model = "mistral-medium", int maxTokens = 512)
+ {
+ return new MistralRequest
+ {
+ Model = model,
+ Prompt = prompt,
+ MaxTokens = maxTokens,
+ Temperature = 0.7,
+ TopP = 1.0,
+ N = 1,
+ Stream = false,
+ Echo = false
+ };
+ }
+}
+
+///
+/// Request model for Mistral chat completion API.
+/// Used for conversational text generation.
+///
+///
+/// See: https://docs.mistral.ai/api/#operation/createChatCompletion
+///
+public record MistralChatRequest
+{
+ ///
+ /// ID of the model to use.
+ ///
+ public string Model { get; init; } = "mistral-medium";
+
+ ///
+ /// A list of messages comprising the conversation so far.
+ ///
+ public IReadOnlyList Messages { get; init; } = new List();
+
+ ///
+ /// The maximum number of tokens to generate in the completion.
+ ///
+ public int? MaxTokens { get; init; } = 512;
+
+ ///
+ /// What sampling temperature to use.
+ ///
+ public double? Temperature { get; init; } = 0.7;
+
+ ///
+ /// Nucleus sampling.
+ ///
+ public double? TopP { get; init; } = 1.0;
+
+ ///
+ /// How many chat completions to generate for each input message.
+ ///
+ public int? N { get; init; } = 1;
+
+ ///
+ /// Whether to stream partial responses.
+ ///
+ public bool? Stream { get; init; } = false;
+
+ ///
+ /// Up to 4 sequences where the API will stop generating further tokens.
+ ///
+ public IReadOnlyList? Stop { get; init; }
+
+ ///
+ /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their frequency.
+ ///
+ public double? FrequencyPenalty { get; init; }
+
+ ///
+ /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear.
+ ///
+ public double? PresencePenalty { get; init; }
+
+ ///
+ /// User identifier for tracking usage.
+ ///
+ public string? User { get; init; }
+
+ ///
+ /// Factory method to create a chat request.
+ ///
+ public static MistralChatRequest CreateChat(IReadOnlyList messages, string model = "mistral-medium", int maxTokens = 512)
+ {
+ return new MistralChatRequest
+ {
+ Model = model,
+ Messages = messages,
+ MaxTokens = maxTokens,
+ Temperature = 0.7,
+ TopP = 1.0,
+ N = 1,
+ Stream = false
+ };
+ }
+}
+
+///
+/// Represents a message in a Mistral chat conversation.
+///
+public record MistralMessage
+{
+ ///
+ /// The role of the message author.
+ ///
+ public string Role { get; init; } = string.Empty;
+
+ ///
+ /// The content of the message.
+ ///
+ public string Content { get; init; } = string.Empty;
+
+ ///
+ /// Factory method to create a user message.
+ ///
+ public static MistralMessage User(string content) => new() { Role = "user", Content = content };
+
+ ///
+ /// Factory method to create an assistant message.
+ ///
+ public static MistralMessage Assistant(string content) => new() { Role = "assistant", Content = content };
+
+ ///
+ /// Factory method to create a system message.
+ ///
+ public static MistralMessage System(string content) => new() { Role = "system", Content = content };
+}
+
+///
+/// Model information from Mistral API.
+///
+public record MistralModel
+{
+ ///
+ /// The model identifier.
+ ///
+ public string Id { get; init; } = string.Empty;
+
+ ///
+ /// The model object type.
+ ///
+ public string Object { get; init; } = string.Empty;
+
+ ///
+ /// When the model was created.
+ ///
+ public long Created { get; init; }
+
+ ///
+ /// The model's description.
+ ///
+ public string Description { get; init; } = string.Empty;
+}
diff --git a/GermanApp/Application/Models/MistralResponse.cs b/GermanApp/Application/Models/MistralResponse.cs
new file mode 100644
index 0000000..4369dd6
--- /dev/null
+++ b/GermanApp/Application/Models/MistralResponse.cs
@@ -0,0 +1,232 @@
+using System.Text.Json.Serialization;
+using GermanApp.Domain.Interfaces;
+
+namespace GermanApp.Application.Models;
+
+///
+/// Response model for Mistral completion and chat completion APIs.
+///
+///
+/// See: https://docs.mistral.ai/api/#operation/createCompletion
+/// See: https://docs.mistral.ai/api/#operation/createChatCompletion
+///
+public record MistralResponse
+{
+ ///
+ /// Unique identifier for the completion.
+ ///
+ [JsonPropertyName("id")]
+ public string Id { get; init; } = string.Empty;
+
+ ///
+ /// The object type (always "text_completion" or "chat.completion").
+ ///
+ [JsonPropertyName("object")]
+ public string Object { get; init; } = string.Empty;
+
+ ///
+ /// The Unix timestamp (in seconds) of when the completion was created.
+ ///
+ [JsonPropertyName("created")]
+ public long Created { get; init; }
+
+ ///
+ /// The model used for the completion.
+ ///
+ [JsonPropertyName("model")]
+ public string Model { get; init; } = string.Empty;
+
+ ///
+ /// The list of completion choices generated by the model.
+ ///
+ [JsonPropertyName("choices")]
+ public IReadOnlyList Choices { get; init; } = new List();
+
+ ///
+ /// Usage statistics for the completion.
+ ///
+ [JsonPropertyName("usage")]
+ public MistralUsage Usage { get; init; } = new();
+
+ ///
+ /// Gets the first choice's text content.
+ /// Convenience property for single-completion requests.
+ ///
+ [JsonIgnore]
+ public string FirstChoiceText => Choices.Count > 0 ? Choices[0].Text : string.Empty;
+
+ ///
+ /// Gets the first choice's message content (for chat completions).
+ ///
+ [JsonIgnore]
+ public string FirstChoiceMessageContent =>
+ Choices.Count > 0 && Choices[0].Message != null
+ ? Choices[0].Message.Content
+ : string.Empty;
+
+ ///
+ /// Gets all generated texts from all choices.
+ ///
+ [JsonIgnore]
+ public IReadOnlyList AllChoiceTexts => Choices.Select(c => c.Text).ToList();
+
+ ///
+ /// Checks if the response contains any valid completions.
+ ///
+ [JsonIgnore]
+ public bool HasValidChoices => Choices != null && Choices.Count > 0 &&
+ Choices.Any(c => !string.IsNullOrWhiteSpace(c.Text) || c.Message != null);
+}
+
+///
+/// A single completion choice returned by Mistral API.
+///
+public record MistralChoice
+{
+ ///
+ /// The generated text for this choice.
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; init; } = string.Empty;
+
+ ///
+ /// For chat completions, this contains the message object.
+ ///
+ [JsonPropertyName("message")]
+ public MistralChatMessage? Message { get; init; }
+
+ ///
+ /// The reason the model stopped generating tokens (e.g., "stop", "length", "error").
+ ///
+ [JsonPropertyName("finish_reason")]
+ public string FinishReason { get; init; } = string.Empty;
+
+ ///
+ /// The index of this choice in the list of choices.
+ ///
+ [JsonPropertyName("index")]
+ public int Index { get; init; }
+
+ ///
+ /// Log probability information for the generated tokens.
+ ///
+ [JsonPropertyName("logprobs")]
+ public object? LogProbs { get; init; }
+}
+
+///
+/// Message object returned in chat completion choices.
+///
+public record MistralChatMessage
+{
+ ///
+ /// The role of the message author.
+ ///
+ [JsonPropertyName("role")]
+ public string Role { get; init; } = string.Empty;
+
+ ///
+ /// The content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public string Content { get; init; } = string.Empty;
+}
+
+///
+/// Token usage statistics from Mistral API.
+///
+public record MistralUsage
+{
+ ///
+ /// Number of tokens in the prompt.
+ ///
+ [JsonPropertyName("prompt_tokens")]
+ public int PromptTokens { get; init; }
+
+ ///
+ /// Number of tokens generated in the completion.
+ ///
+ [JsonPropertyName("completion_tokens")]
+ public int CompletionTokens { get; init; }
+
+ ///
+ /// Total number of tokens used (prompt + completion).
+ ///
+ [JsonPropertyName("total_tokens")]
+ public int TotalTokens { get; init; }
+
+ ///
+ /// Gets the total cost estimate based on token usage.
+ /// Note: Actual pricing may vary based on Mistral's pricing model.
+ ///
+ [JsonIgnore]
+ public decimal EstimatedCost => TotalTokens * 0.0000025m; // Approx $0.0000025 per token for mistral-medium
+}
+
+///
+/// Error response from Mistral API.
+///
+public record MistralErrorResponse
+{
+ ///
+ /// The object type (always "error").
+ ///
+ [JsonPropertyName("object")]
+ public string Object { get; init; } = string.Empty;
+
+ ///
+ /// The error message.
+ ///
+ [JsonPropertyName("message")]
+ public string Message { get; init; } = string.Empty;
+
+ ///
+ /// The type of error that occurred.
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; init; } = string.Empty;
+
+ ///
+ /// The HTTP status code associated with the error.
+ ///
+ [JsonIgnore]
+ public int? StatusCode { get; init; }
+
+ ///
+ /// Additional error details.
+ ///
+ [JsonPropertyName("details")]
+ public object? Details { get; init; }
+
+ ///
+ /// Maps the error type to an AiErrorCode.
+ ///
+ [JsonIgnore]
+ public AiErrorCode ErrorCode => Type switch
+ {
+ "rate_limit_exceeded" or "rate_limit" => AiErrorCode.RateLimited,
+ "invalid_request_error" => AiErrorCode.InvalidRequest,
+ "authentication_error" or "invalid_api_key" => AiErrorCode.AuthenticationError,
+ "server_error" or "internal_server_error" => AiErrorCode.Temporary,
+ "timeout" => AiErrorCode.Timeout,
+ _ => AiErrorCode.Unknown
+ };
+}
+
+///
+/// Response wrapper for listing Mistral models.
+///
+public record MistralListModelsResponse
+{
+ ///
+ /// The object type (always "list").
+ ///
+ [JsonPropertyName("object")]
+ public string Object { get; init; } = string.Empty;
+
+ ///
+ /// List of available models.
+ ///
+ [JsonPropertyName("data")]
+ public IReadOnlyList Data { get; init; } = new List();
+}
diff --git a/GermanApp/Domain/Interfaces/IMistralConnector.cs b/GermanApp/Domain/Interfaces/IMistralConnector.cs
new file mode 100644
index 0000000..9518017
--- /dev/null
+++ b/GermanApp/Domain/Interfaces/IMistralConnector.cs
@@ -0,0 +1,102 @@
+using GermanApp.Application.Models;
+
+namespace GermanApp.Domain.Interfaces;
+
+///
+/// Interface for Mistral API connector.
+/// This is part of the Domain layer - defines the contract for communicating with Mistral API.
+///
+public interface IMistralConnector
+{
+ ///
+ /// Sends a completion request to Mistral API.
+ ///
+ /// The completion request containing prompt and parameters
+ /// Cancellation token
+ /// Mistral API response containing generated text
+ /// Thrown when Mistral API request fails
+ Task CompleteAsync(MistralRequest request,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Sends a chat completion request to Mistral API.
+ ///
+ /// The chat completion request containing messages and parameters
+ /// Cancellation token
+ /// Mistral API chat response containing generated message
+ /// Thrown when Mistral API request fails
+ Task ChatAsync(MistralChatRequest request,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Lists available models from Mistral API.
+ ///
+ /// Cancellation token
+ /// List of available Mistral models
+ /// Thrown when Mistral API request fails
+ Task> ListModelsAsync(
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets information about a specific model.
+ ///
+ /// The model identifier
+ /// Cancellation token
+ /// Information about the specified model
+ /// Thrown when Mistral API request fails
+ Task GetModelAsync(string modelId,
+ CancellationToken cancellationToken = default);
+}
+
+///
+/// Custom exception for AI service errors.
+///
+public class AiServiceException : Exception
+{
+ ///
+ /// Error code for categorizing AI service errors.
+ ///
+ public AiErrorCode ErrorCode { get; }
+
+ ///
+ /// Creates a new AI service exception.
+ ///
+ public AiServiceException(string message, AiErrorCode errorCode = AiErrorCode.Unknown)
+ : base(message)
+ {
+ ErrorCode = errorCode;
+ }
+
+ ///
+ /// Creates a new AI service exception with inner exception.
+ ///
+ public AiServiceException(string message, Exception innerException,
+ AiErrorCode errorCode = AiErrorCode.Unknown)
+ : base(message, innerException)
+ {
+ ErrorCode = errorCode;
+ }
+}
+
+///
+/// Error codes for AI service exceptions.
+///
+public enum AiErrorCode
+{
+ /// Unknown or unspecified error
+ Unknown = 0,
+ /// API rate limit exceeded
+ RateLimited = 1,
+ /// Temporary API failure, may succeed on retry
+ Temporary = 2,
+ /// Request timeout
+ Timeout = 3,
+ /// Invalid API key or authentication error
+ AuthenticationError = 4,
+ /// Invalid request parameters
+ InvalidRequest = 5,
+ /// API returned invalid/empty response
+ InvalidResponse = 6,
+ /// Service is temporarily unavailable (circuit breaker open)
+ ServiceUnavailable = 7
+}
diff --git a/GermanApp/GermanApp.csproj b/GermanApp/GermanApp.csproj
index 5da0698..7eeaa54 100644
--- a/GermanApp/GermanApp.csproj
+++ b/GermanApp/GermanApp.csproj
@@ -15,6 +15,7 @@
+
diff --git a/GermanApp/Infrastructure/Configuration/MistralConfig.cs b/GermanApp/Infrastructure/Configuration/MistralConfig.cs
new file mode 100644
index 0000000..d27ce74
--- /dev/null
+++ b/GermanApp/Infrastructure/Configuration/MistralConfig.cs
@@ -0,0 +1,119 @@
+namespace GermanApp.Infrastructure.Configuration;
+
+///
+/// Configuration settings for Mistral API connector.
+/// This is part of the Infrastructure layer.
+///
+public class MistralConfig
+{
+ ///
+ /// The Mistral API key for authentication.
+ ///
+ public string ApiKey { get; set; } = string.Empty;
+
+ ///
+ /// The base URL for the Mistral API.
+ /// Default: https://api.mistral.ai/v1/
+ ///
+ public string BaseUrl { get; set; } = "https://api.mistral.ai/v1/";
+
+ ///
+ /// The default model to use for completions.
+ /// Default: mistral-medium
+ ///
+ public string DefaultModel { get; set; } = "mistral-medium";
+
+ ///
+ /// Timeout in seconds for HTTP requests.
+ /// Default: 30 seconds
+ ///
+ public int TimeoutSeconds { get; set; } = 30;
+
+ ///
+ /// Maximum number of retry attempts for failed requests.
+ /// Default: 3
+ ///
+ public int MaxRetries { get; set; } = 3;
+
+ ///
+ /// Maximum requests per minute rate limit.
+ /// Default: 10 requests/minute
+ ///
+ public int RateLimitPerMinute { get; set; } = 10;
+
+ ///
+ /// Whether to enable response caching.
+ /// Default: true
+ ///
+ public bool EnableCaching { get; set; } = true;
+
+ ///
+ /// Cache time-to-live in minutes.
+ /// Default: 60 minutes
+ ///
+ public int CacheTTLMinutes { get; set; } = 60;
+
+ ///
+ /// Circuit breaker failure threshold (number of consecutive failures before opening).
+ /// Default: 5
+ ///
+ public int CircuitBreakerFailureThreshold { get; set; } = 5;
+
+ ///
+ /// Circuit breaker reset timeout in minutes.
+ /// Default: 1 minute
+ ///
+ public int CircuitBreakerResetMinutes { get; set; } = 1;
+
+ ///
+ /// Validates the configuration.
+ ///
+ /// Thrown when configuration is invalid
+ public void Validate()
+ {
+ if (string.IsNullOrWhiteSpace(ApiKey))
+ {
+ throw new ArgumentException("Mistral API key is required");
+ }
+
+ if (string.IsNullOrWhiteSpace(BaseUrl))
+ {
+ throw new ArgumentException("Mistral BaseUrl is required");
+ }
+
+ if (!Uri.TryCreate(BaseUrl, UriKind.Absolute, out _))
+ {
+ throw new ArgumentException("Mistral BaseUrl must be a valid URL");
+ }
+
+ if (TimeoutSeconds <= 0)
+ {
+ throw new ArgumentException("TimeoutSeconds must be greater than 0");
+ }
+
+ if (MaxRetries < 0)
+ {
+ throw new ArgumentException("MaxRetries must be non-negative");
+ }
+
+ if (RateLimitPerMinute <= 0)
+ {
+ throw new ArgumentException("RateLimitPerMinute must be greater than 0");
+ }
+
+ if (CacheTTLMinutes < 0)
+ {
+ throw new ArgumentException("CacheTTLMinutes must be non-negative");
+ }
+
+ if (CircuitBreakerFailureThreshold <= 0)
+ {
+ throw new ArgumentException("CircuitBreakerFailureThreshold must be greater than 0");
+ }
+
+ if (CircuitBreakerResetMinutes <= 0)
+ {
+ throw new ArgumentException("CircuitBreakerResetMinutes must be greater than 0");
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Services/MistralCircuitBreaker.cs b/GermanApp/Infrastructure/Services/MistralCircuitBreaker.cs
new file mode 100644
index 0000000..ba1eeb9
--- /dev/null
+++ b/GermanApp/Infrastructure/Services/MistralCircuitBreaker.cs
@@ -0,0 +1,125 @@
+using System;
+
+namespace GermanApp.Infrastructure.Services;
+
+///
+/// Circuit breaker implementation for Mistral API.
+/// Prevents cascading failures by temporarily blocking requests after too many failures.
+/// This is part of the Infrastructure layer.
+///
+public class MistralCircuitBreaker
+{
+ private readonly int _failureThreshold;
+ private readonly TimeSpan _resetTimeout;
+ private int _failureCount = 0;
+ private DateTime _lastFailureTime = DateTime.MinValue;
+ private readonly object _lock = new();
+
+ ///
+ /// Creates a new circuit breaker.
+ ///
+ /// Number of consecutive failures before opening the circuit
+ /// Time to wait before attempting to close the circuit
+ public MistralCircuitBreaker(int failureThreshold, TimeSpan resetTimeout)
+ {
+ _failureThreshold = failureThreshold;
+ _resetTimeout = resetTimeout;
+ }
+
+ ///
+ /// Gets whether the circuit is currently closed (allowing requests).
+ ///
+ public bool IsClosed
+ {
+ get
+ {
+ if (_failureCount >= _failureThreshold)
+ {
+ // Circuit is open, check if reset timeout has elapsed
+ if (DateTime.UtcNow - _lastFailureTime > _resetTimeout)
+ {
+ // Reset the circuit
+ lock (_lock)
+ {
+ if (_failureCount >= _failureThreshold &&
+ DateTime.UtcNow - _lastFailureTime > _resetTimeout)
+ {
+ _failureCount = 0;
+ _lastFailureTime = DateTime.MinValue;
+ }
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ ///
+ /// Gets the current failure count.
+ ///
+ public int FailureCount => _failureCount;
+
+ ///
+ /// Gets the last failure time.
+ ///
+ public DateTime LastFailureTime => _lastFailureTime;
+
+ ///
+ /// Records a successful request, resetting the failure count.
+ ///
+ public void RecordSuccess()
+ {
+ lock (_lock)
+ {
+ _failureCount = 0;
+ _lastFailureTime = DateTime.MinValue;
+ }
+ }
+
+ ///
+ /// Records a failed request, incrementing the failure count.
+ ///
+ public void RecordFailure()
+ {
+ lock (_lock)
+ {
+ _failureCount++;
+ _lastFailureTime = DateTime.UtcNow;
+ }
+ }
+
+ ///
+ /// Resets the circuit breaker to its initial state.
+ ///
+ public void Reset()
+ {
+ lock (_lock)
+ {
+ _failureCount = 0;
+ _lastFailureTime = DateTime.MinValue;
+ }
+ }
+
+ ///
+ /// Gets the time remaining until the circuit can be reset.
+ /// Returns TimeSpan.Zero if circuit is closed or already eligible for reset.
+ ///
+ public TimeSpan TimeUntilReset
+ {
+ get
+ {
+ if (_failureCount < _failureThreshold)
+ return TimeSpan.Zero;
+
+ var elapsed = DateTime.UtcNow - _lastFailureTime;
+ if (elapsed >= _resetTimeout)
+ return TimeSpan.Zero;
+
+ return _resetTimeout - elapsed;
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Services/MistralConnector.cs b/GermanApp/Infrastructure/Services/MistralConnector.cs
new file mode 100644
index 0000000..cbc2b2f
--- /dev/null
+++ b/GermanApp/Infrastructure/Services/MistralConnector.cs
@@ -0,0 +1,244 @@
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using GermanApp.Application.Models;
+using GermanApp.Domain.Interfaces;
+using GermanApp.Infrastructure.Configuration;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging;
+
+namespace GermanApp.Infrastructure.Services;
+
+///
+/// HTTP client implementation for Mistral API.
+/// Implements IMistralConnector from Domain layer.
+///
+public class MistralConnector : IMistralConnector
+{
+ private readonly HttpClient _httpClient;
+ private readonly MistralConfig _config;
+ private readonly ILogger _logger;
+ private readonly MistralRateLimiter _rateLimiter;
+ private readonly IMemoryCache _cache;
+ private readonly MistralCircuitBreaker _circuitBreaker;
+ private readonly JsonSerializerOptions _jsonOptions;
+
+ public MistralConnector(HttpClient httpClient, MistralConfig config,
+ ILogger logger, IMemoryCache cache)
+ {
+ _httpClient = httpClient;
+ _config = config;
+ _logger = logger;
+ _cache = cache;
+ _config.Validate();
+
+ _httpClient.BaseAddress = new Uri(_config.BaseUrl);
+ _httpClient.Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds);
+ _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_config.ApiKey}");
+ _httpClient.DefaultRequestHeaders.Add("Content-Type", "application/json");
+
+ _jsonOptions = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ _rateLimiter = new MistralRateLimiter(_config.RateLimitPerMinute);
+ _circuitBreaker = new MistralCircuitBreaker(
+ _config.CircuitBreakerFailureThreshold,
+ TimeSpan.FromMinutes(_config.CircuitBreakerResetMinutes));
+ }
+
+ public async Task CompleteAsync(MistralRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ return await ExecuteRequestAsync(
+ "completions", request, cancellationToken);
+ }
+
+ public async Task ChatAsync(MistralChatRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ return await ExecuteRequestAsync(
+ "chat/completions", request, cancellationToken);
+ }
+
+ public async Task> ListModelsAsync(
+ CancellationToken cancellationToken = default)
+ {
+ CheckCircuitAndRateLimit("models");
+ try
+ {
+ var response = await ExecuteWithRetryAsync(async () =>
+ {
+ var httpResponse = await _httpClient.GetAsync("models", cancellationToken);
+ return await HandleResponseAsync(httpResponse);
+ }, "ListModels");
+ _circuitBreaker.RecordSuccess();
+ return response.Data;
+ }
+ catch (Exception ex)
+ {
+ HandleRequestError(ex, "ListModels");
+ throw;
+ }
+ }
+
+ public async Task GetModelAsync(string modelId,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(modelId))
+ throw new ArgumentException("Model ID cannot be null or empty", nameof(modelId));
+ CheckCircuitAndRateLimit("models");
+ try
+ {
+ var response = await ExecuteWithRetryAsync(async () =>
+ {
+ var httpResponse = await _httpClient.GetAsync($"models/{modelId}", cancellationToken);
+ return await HandleResponseAsync(httpResponse);
+ }, "GetModel");
+ _circuitBreaker.RecordSuccess();
+ return response;
+ }
+ catch (Exception ex)
+ {
+ HandleRequestError(ex, "GetModel");
+ throw;
+ }
+ }
+
+ private async Task ExecuteRequestAsync(
+ string endpoint, TRequest request, CancellationToken cancellationToken)
+ where TResponse : class
+ {
+ CheckCircuitAndRateLimit(endpoint);
+
+ // Generate cache key for caching
+ var cacheKey = GenerateCacheKey(request);
+ if (_config.EnableCaching && _cache.TryGetValue(cacheKey, out TResponse cachedResponse))
+ {
+ _logger.LogDebug("Cache hit for {Endpoint}", endpoint);
+ return cachedResponse!;
+ }
+
+ try
+ {
+ var response = await ExecuteWithRetryAsync(async () =>
+ {
+ var json = JsonSerializer.Serialize(request, _jsonOptions);
+ var content = new StringContent(json, Encoding.UTF8, "application/json");
+ var httpResponse = await _httpClient.PostAsync(endpoint, content, cancellationToken);
+ return await HandleResponseAsync(httpResponse);
+ }, endpoint);
+
+ _circuitBreaker.RecordSuccess();
+
+ // Cache the response if caching is enabled
+ if (_config.EnableCaching && response != null)
+ {
+ _cache.Set(cacheKey, response, TimeSpan.FromMinutes(_config.CacheTTLMinutes));
+ }
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ HandleRequestError(ex, endpoint);
+ throw;
+ }
+ }
+
+ private void CheckCircuitAndRateLimit(string endpoint)
+ {
+ if (!_circuitBreaker.IsClosed)
+ throw new AiServiceException("Mistral API circuit breaker is open",
+ AiErrorCode.ServiceUnavailable);
+ if (!_rateLimiter.TryAcquire(endpoint))
+ throw new AiServiceException("Rate limit exceeded",
+ AiErrorCode.RateLimited);
+ }
+
+ private async Task ExecuteWithRetryAsync(Func> action, string operationName)
+ {
+ int retryCount = 0;
+ while (true)
+ {
+ try
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(_config.TimeoutSeconds));
+ return await action();
+ }
+ catch (Exception ex) when (retryCount < _config.MaxRetries && IsRetryable(ex))
+ {
+ retryCount++;
+ var delay = TimeSpan.FromSeconds(Math.Pow(2, retryCount));
+ _logger.LogWarning(ex, "{Operation} failed (attempt {RetryCount}), retrying in {Delay}s...",
+ operationName, retryCount, delay.TotalSeconds);
+ await Task.Delay(delay);
+ }
+ }
+ }
+
+ private bool IsRetryable(Exception ex) => ex switch
+ {
+ HttpRequestException or TimeoutException or TaskCanceledException => true,
+ _ => false
+ };
+
+ private async Task HandleResponseAsync(HttpResponseMessage httpResponse)
+ {
+ if (!httpResponse.IsSuccessStatusCode)
+ {
+ var errorContent = await httpResponse.Content.ReadAsStringAsync();
+ var errorResponse = JsonSerializer.Deserialize(errorContent, _jsonOptions);
+
+ var errorCode = errorResponse?.ErrorCode ?? MapStatusCodeToErrorCode(httpResponse.StatusCode);
+ var message = errorResponse?.Message ?? $"HTTP {(int)httpResponse.StatusCode}: {httpResponse.ReasonPhrase}";
+
+ _logger.LogError("Mistral API error: {StatusCode} - {Message}",
+ (int)httpResponse.StatusCode, message);
+
+ httpResponse.EnsureSuccessStatusCode(); // This will throw
+ }
+
+ var content = await httpResponse.Content.ReadAsStringAsync();
+ return JsonSerializer.Deserialize(content, _jsonOptions)
+ ?? throw new AiServiceException("Invalid response from Mistral API",
+ AiErrorCode.InvalidResponse);
+ }
+
+ private string GenerateCacheKey(T request)
+ {
+ var json = JsonSerializer.Serialize(request, _jsonOptions);
+ using var sha256 = System.Security.Cryptography.SHA256.Create();
+ var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(json));
+ return Convert.ToBase64String(hashBytes);
+ }
+
+ private AiErrorCode MapStatusCodeToErrorCode(HttpStatusCode statusCode)
+ {
+ return statusCode switch
+ {
+ HttpStatusCode.Unauthorized => AiErrorCode.AuthenticationError,
+ HttpStatusCode.TooManyRequests => AiErrorCode.RateLimited,
+ HttpStatusCode.RequestTimeout => AiErrorCode.Timeout,
+ HttpStatusCode.BadRequest => AiErrorCode.InvalidRequest,
+ HttpStatusCode.InternalServerError or
+ HttpStatusCode.ServiceUnavailable or
+ HttpStatusCode.BadGateway or
+ HttpStatusCode.GatewayTimeout => AiErrorCode.Temporary,
+ _ => AiErrorCode.Unknown
+ };
+ }
+
+ private void HandleRequestError(Exception ex, string operationName)
+ {
+ _circuitBreaker.RecordFailure();
+ _logger.LogError(ex, "Mistral API request failed: {Operation}", operationName);
+ if (ex is not AiServiceException)
+ {
+ throw new AiServiceException($"Mistral {operationName} failed: {ex.Message}",
+ ex, AiErrorCode.Temporary);
+ }
+ }
+}
diff --git a/GermanApp/Infrastructure/Services/MistralRateLimiter.cs b/GermanApp/Infrastructure/Services/MistralRateLimiter.cs
new file mode 100644
index 0000000..0e09e87
--- /dev/null
+++ b/GermanApp/Infrastructure/Services/MistralRateLimiter.cs
@@ -0,0 +1,97 @@
+using System.Collections.Concurrent;
+
+namespace GermanApp.Infrastructure.Services;
+
+///
+/// Simple in-memory rate limiter for Mistral API requests.
+/// This is part of the Infrastructure layer.
+///
+public class MistralRateLimiter
+{
+ private readonly int _maxRequests;
+ private readonly TimeSpan _window;
+ private readonly ConcurrentDictionary> _requests = new();
+
+ ///
+ /// Creates a new rate limiter.
+ ///
+ /// Maximum requests allowed per minute
+ public MistralRateLimiter(int maxRequestsPerMinute)
+ {
+ _maxRequests = maxRequestsPerMinute;
+ _window = TimeSpan.FromMinutes(1);
+ }
+
+ ///
+ /// Attempts to acquire a rate limit token for the specified endpoint.
+ ///
+ /// The API endpoint being called
+ /// True if request is allowed, false if rate limit exceeded
+ public bool TryAcquire(string endpoint)
+ {
+ var now = DateTime.UtcNow;
+ var requests = _requests.GetOrAdd(endpoint, _ => new List());
+
+ lock (requests)
+ {
+ // Remove old requests outside the window
+ requests.RemoveAll(r => now - r > _window);
+
+ // Check if limit exceeded
+ if (requests.Count >= _maxRequests)
+ return false;
+
+ // Add new request
+ requests.Add(now);
+ return true;
+ }
+ }
+
+ ///
+ /// Gets the current request count for an endpoint.
+ ///
+ /// The API endpoint
+ /// Number of requests in the current window
+ public int GetCurrentCount(string endpoint)
+ {
+ if (_requests.TryGetValue(endpoint, out var requests))
+ {
+ lock (requests)
+ {
+ var now = DateTime.UtcNow;
+ requests.RemoveAll(r => now - r > _window);
+ return requests.Count;
+ }
+ }
+ return 0;
+ }
+
+ ///
+ /// Resets the rate limiter for a specific endpoint.
+ ///
+ /// The API endpoint
+ public void Reset(string endpoint)
+ {
+ if (_requests.TryGetValue(endpoint, out var requests))
+ {
+ lock (requests)
+ {
+ requests.Clear();
+ }
+ }
+ }
+
+ ///
+ /// Resets all rate limiters.
+ ///
+ public void ResetAll()
+ {
+ foreach (var kvp in _requests)
+ {
+ lock (kvp.Value)
+ {
+ kvp.Value.Clear();
+ }
+ }
+ }
+}
diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs
index 8fa3d2c..9c3c3c8 100644
--- a/GermanApp/Program.cs
+++ b/GermanApp/Program.cs
@@ -10,6 +10,7 @@ using GermanApp.Infrastructure.Data.DbContext;
using GermanApp.Infrastructure.Data.Repositories;
using GermanApp.Infrastructure.Data.SeedData;
using GermanApp.Infrastructure.Services;
+using GermanApp.Infrastructure.Configuration;
using GermanApp.Presentation.Controllers;
using GermanApp.Presentation.Validators;
using GermanApp.Presentation.Endpoints;
@@ -17,6 +18,8 @@ using GermanApp.Shared.Middleware;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Serilog;
using System.Text;
@@ -132,6 +135,30 @@ try
builder.Services.AddScoped();
builder.Services.AddScoped();
+ // ============================================
+ // INFRASTRUCTURE LAYER - AI Services
+ // ============================================
+
+ // Add Memory Cache for caching (used by AI services)
+ builder.Services.AddMemoryCache();
+
+ // Add Mistral API configuration
+ builder.Services.Configure(builder.Configuration.GetSection("Mistral"));
+
+ // Add HttpClient for Mistral API
+ builder.Services.AddHttpClient("MistralClient");
+
+ // Register Mistral Connector
+ builder.Services.AddScoped(provider =>
+ {
+ var httpClient = provider.GetRequiredService().CreateClient("MistralClient");
+ var config = provider.GetRequiredService>().Value;
+ var logger = provider.GetRequiredService>();
+ var cache = provider.GetRequiredService();
+
+ return new MistralConnector(httpClient, config, logger, cache);
+ });
+
// ============================================
// APPLICATION LAYER - Use Cases & Services
// ============================================
diff --git a/docs/features/ai-services.md b/docs/features/ai-services.md
index 0004fea..953b208 100644
--- a/docs/features/ai-services.md
+++ b/docs/features/ai-services.md
@@ -129,14 +129,21 @@ As a learner, I want AI-powered features like generated stories, speech recognit
### Phase 0: Mistral API Connector (2-3 hours)
**Objective**: Create a reusable, testable connector for Mistral API that can be used by all Mistral-based services.
-- [ ] Create `Domain/Interfaces/IMistralConnector.cs` - Interface for Mistral API connector
-- [ ] Create `Infrastructure/Services/MistralConnector.cs` - HTTP client implementation
-- [ ] Implement request/response models (`MistralRequest.cs`, `MistralResponse.cs`)
-- [ ] Add HTTP client configuration with base URL and timeout
-- [ ] Implement retry logic with exponential backoff
-- [ ] Add rate limiting at the connector level
-- [ ] Add response caching mechanism
-- [ ] Implement circuit breaker pattern for failure handling
+- [x] Create `Domain/Interfaces/IMistralConnector.cs` - Interface for Mistral API connector
+- [x] Create `Infrastructure/Configuration/MistralConfig.cs` - Configuration class
+- [x] Create `Infrastructure/Services/MistralConnector.cs` - HTTP client implementation
+- [x] Create `Infrastructure/Services/MistralRateLimiter.cs` - Rate limiting implementation
+- [x] Create `Infrastructure/Services/MistralCircuitBreaker.cs` - Circuit breaker implementation
+- [x] Create `Application/Models/MistralRequest.cs` - Request models (MistralRequest, MistralChatRequest, MistralMessage)
+- [x] Create `Application/Models/MistralResponse.cs` - Response models (MistralResponse, MistralChoice, MistralChatMessage, MistralUsage)
+- [x] Create `Application/Models/MistralErrorResponse.cs` - Error response model
+- [x] Create `Domain/Interfaces/AiServiceException.cs` - Custom exception with AiErrorCode enum
+- [x] Add HTTP client configuration in Program.cs
+- [x] Register IMistralConnector in Program.cs
+- [x] Implement retry logic with exponential backoff
+- [x] Add rate limiting to connector
+- [x] Add response caching mechanism
+- [x] Implement circuit breaker pattern for failure handling
- [ ] Create unit tests for MistralConnector
**Deliverables:**
@@ -201,25 +208,31 @@ As a learner, I want AI-powered features like generated stories, speech recognit
## ✅ Tasks
### Backend - Mistral API Connector
-- [ ] Create `Domain/Interfaces/IMistralConnector.cs`
-- [ ] Create `Infrastructure/Services/MistralConnector.cs`
-- [ ] Create `Application/Models/MistralRequest.cs`
-- [ ] Create `Application/Models/MistralResponse.cs`
-- [ ] Create `Application/Models/MistralErrorResponse.cs`
-- [ ] Add HTTP client configuration in Program.cs
-- [ ] Implement retry logic with exponential backoff
-- [ ] Add rate limiting to connector
-- [ ] Add response caching mechanism
-- [ ] Implement circuit breaker pattern
+- [x] Create `Domain/Interfaces/IMistralConnector.cs`
+- [x] Create `Infrastructure/Configuration/MistralConfig.cs`
+- [x] Create `Infrastructure/Services/MistralConnector.cs`
+- [x] Create `Infrastructure/Services/MistralRateLimiter.cs`
+- [x] Create `Infrastructure/Services/MistralCircuitBreaker.cs`
+- [x] Create `Application/Models/MistralRequest.cs`
+- [x] Create `Application/Models/MistralResponse.cs`
+- [x] Create `Application/Models/MistralErrorResponse.cs`
+- [x] Create `Domain/Interfaces/AiServiceException.cs`
+- [x] Add HTTP client configuration in Program.cs
+- [x] Register IMistralConnector in Program.cs
+- [x] Implement retry logic with exponential backoff
+- [x] Add rate limiting to connector
+- [x] Add response caching mechanism
+- [x] Implement circuit breaker pattern
- [ ] Create unit tests for MistralConnector
### Backend - Configuration
+- [x] Create Configuration/MistralConfig.cs
- [ ] Add Mistral settings to appsettings.json
- [ ] Add Vosk settings to appsettings.json
- [ ] Add Coqui settings to appsettings.json
-- [ ] Create Configuration/MistralConfig.cs
- [ ] Create Configuration/VoskConfig.cs
- [ ] Create Configuration/CoquiConfig.cs
+- [x] Register Mistral Connector in Program.cs
- [ ] Register all AI services in Program.cs
- [ ] Add health checks for AI services
@@ -933,6 +946,7 @@ This follows Clean Architecture: interface (`IMistralConnector`) in Domain layer
|------|---------------|-------|
| May 31, 2025 | Created | Initial plan based on application-plan.md |
| June 9, 2025 | Updated | Added Phase 0: Mistral API Connector as first step |
+| June 10, 2025 | Phase 0 Complete | Mistral API Connector implemented (IMistralConnector, MistralConnector, MistralConfig, models, rate limiter, circuit breaker) and registered in Program.cs. Build successful, all 234 tests passing. Ready for unit tests for MistralConnector. |
---