using Anthropic.SDK; using Anthropic.SDK.Messaging; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using PowderCoating.Application.Interfaces; namespace PowderCoating.Infrastructure.Services; public class AiHelpService : IAiHelpService { private readonly ILogger _logger; private readonly AnthropicClient? _client; private const int MaxHistoryTurns = 10; // keep last 10 exchanges to limit tokens private const string Model = "claude-sonnet-4-6"; /// /// Initializes and eagerly creates the /// if the API key is configured. The client is stored as a /// nullable field rather than throwing at construction time so that the rest of the /// application continues to function when the AI key is absent — the null is checked in /// and returns a user-friendly message instead of a 500 /// error. The client is constructed once here (rather than per call) because /// is thread-safe and reuse avoids the overhead of /// recreating the HTTP client on each chat message. /// public AiHelpService(IConfiguration config, ILogger logger) { _logger = logger; var apiKey = config["AI:Anthropic:ApiKey"]; if (!string.IsNullOrWhiteSpace(apiKey)) _client = new AnthropicClient(apiKey); } /// /// Sends a user message to Claude with the full conversation history and the /// HelpKnowledgeBase-sourced system prompt, then returns the assistant's plain-text /// response. The conversation history is trimmed to the last /// × 2 messages (10 exchanges = 20 turns) before being /// sent to keep prompt tokens within budget — older context is dropped because users /// asking help questions rarely need to reference what they said more than 10 turns ago, /// and the system prompt already contains the full knowledge base as grounding. A 60-second /// cancellation token is applied to prevent slow AI responses from holding an ASP.NET /// request thread indefinitely; the controller's [EnableRateLimiting] attribute /// provides the outer rate-limit layer so this method does not need to enforce it. When /// the API key is not configured, a friendly "not configured" message is returned so /// that users see a clear explanation rather than an unhandled exception or empty response. /// public async Task SendMessageAsync( List conversationHistory, string userMessage, string systemPrompt) { if (_client == null) { _logger.LogWarning("AI Help: Anthropic API key not configured."); return "The AI Help Assistant is not configured yet. Please contact your system administrator."; } try { var trimmedHistory = conversationHistory .TakeLast(MaxHistoryTurns * 2) .ToList(); var messages = trimmedHistory .Select(m => new Message { Role = m.Role == "user" ? RoleType.User : RoleType.Assistant, Content = new List { new TextContent { Text = m.Content } } }) .ToList(); messages.Add(new Message { Role = RoleType.User, Content = new List { new TextContent { Text = userMessage } } }); var request = new MessageParameters { Model = Model, MaxTokens = 1024, SystemMessage = systemPrompt, Messages = messages }; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var response = await _client.Messages.GetClaudeMessageAsync(request, cts.Token); return response.FirstMessage?.Text ?? response.Content?.OfType().FirstOrDefault()?.Text ?? "I wasn't able to generate a response. Please try again."; } catch (OperationCanceledException) { _logger.LogWarning("AI Help: Request timed out."); return "The request took too long to process. Please try again."; } catch (Exception ex) { _logger.LogError(ex, "AI Help: Error calling Anthropic API."); return "I encountered an error processing your request. Please try again in a moment."; } } }