Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Services/AiHelpService.cs
T
2026-04-23 21:38:24 -04:00

107 lines
4.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.";
}
}
}