107 lines
4.6 KiB
C#
107 lines
4.6 KiB
C#
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.";
|
||
}
|
||
}
|
||
}
|