Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,106 @@
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<AiHelpService> _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";
/// <summary>
/// Initializes <see cref="AiHelpService"/> and eagerly creates the
/// <see cref="AnthropicClient"/> 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
/// <see cref="SendMessageAsync"/> and returns a user-friendly message instead of a 500
/// error. The client is constructed once here (rather than per call) because
/// <see cref="AnthropicClient"/> is thread-safe and reuse avoids the overhead of
/// recreating the HTTP client on each chat message.
/// </summary>
public AiHelpService(IConfiguration config, ILogger<AiHelpService> logger)
{
_logger = logger;
var apiKey = config["AI:Anthropic:ApiKey"];
if (!string.IsNullOrWhiteSpace(apiKey))
_client = new AnthropicClient(apiKey);
}
/// <summary>
/// Sends a user message to Claude with the full conversation history and the
/// <c>HelpKnowledgeBase</c>-sourced system prompt, then returns the assistant's plain-text
/// response. The conversation history is trimmed to the last
/// <see cref="MaxHistoryTurns"/> × 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 <c>[EnableRateLimiting]</c> 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.
/// </summary>
public async Task<string> SendMessageAsync(
List<AiHelpMessage> 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<ContentBase> { new TextContent { Text = m.Content } }
})
.ToList();
messages.Add(new Message
{
Role = RoleType.User,
Content = new List<ContentBase> { 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<TextContent>().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.";
}
}
}