Initial commit
This commit is contained in:
@@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user