274 lines
10 KiB
C#
Executable File
274 lines
10 KiB
C#
Executable File
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using System.Text.Json;
|
|
|
|
namespace MarketAlly.AIPlugin.ClaudeCode;
|
|
|
|
/// <summary>
|
|
/// Main implementation of Claude Code integration service
|
|
/// </summary>
|
|
public class ClaudeCodeService : IClaudeCodeService
|
|
{
|
|
private readonly IRateLimitAwareHttpClient _httpClient;
|
|
private readonly IContextClaudeService _contextService;
|
|
private readonly ILogger<ClaudeCodeService> _logger;
|
|
private readonly ClaudeCodeOptions _options;
|
|
|
|
public ClaudeCodeService(
|
|
IRateLimitAwareHttpClient httpClient,
|
|
IContextClaudeService contextService,
|
|
ILogger<ClaudeCodeService> logger,
|
|
IOptions<ClaudeCodeOptions> options)
|
|
{
|
|
_httpClient = httpClient;
|
|
_contextService = contextService;
|
|
_logger = logger;
|
|
_options = options.Value;
|
|
|
|
// Configure HTTP client
|
|
_httpClient.Configure(_options.BaseUrl, _options.ApiKey ?? string.Empty, _options.TenantId);
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<string>> SendChatMessageAsync(ChatRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Sending chat message: {Message}", request.Message);
|
|
|
|
// Get relevant context if requested
|
|
List<ContextEntry>? relevantContext = null;
|
|
if (request.IncludeProjectContext && !string.IsNullOrEmpty(request.ProjectPath))
|
|
{
|
|
var contextResponse = await _contextService.GetRelevantContextAsync(
|
|
request.Message,
|
|
request.ProjectPath,
|
|
5,
|
|
cancellationToken);
|
|
|
|
if (contextResponse.Success)
|
|
{
|
|
relevantContext = contextResponse.Data;
|
|
}
|
|
}
|
|
|
|
// Get conversation history if requested
|
|
List<ConversationMessage>? history = null;
|
|
if (request.IncludeHistory && !string.IsNullOrEmpty(request.SessionId))
|
|
{
|
|
var historyResponse = await _contextService.GetConversationHistoryAsync(
|
|
request.SessionId,
|
|
10,
|
|
cancellationToken);
|
|
|
|
if (historyResponse.Success)
|
|
{
|
|
history = historyResponse.Data;
|
|
}
|
|
}
|
|
|
|
// Prepare the API request
|
|
var apiRequest = new
|
|
{
|
|
message = request.Message,
|
|
sessionId = request.SessionId,
|
|
projectPath = request.ProjectPath,
|
|
context = request.Context,
|
|
relevantContext = relevantContext?.Select(c => new { c.Type, c.Content, c.Metadata }),
|
|
conversationHistory = history?.Select(h => new { h.Role, h.Content, h.Timestamp })
|
|
};
|
|
|
|
// Send request to Aizia API
|
|
var response = await _httpClient.PostAsJsonAsync("/api/chat/send", apiRequest, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Chat request failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<string>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<ChatApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Chat API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<string>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
// Store the conversation
|
|
if (!string.IsNullOrEmpty(request.SessionId) && !string.IsNullOrEmpty(responseData.Response))
|
|
{
|
|
await StoreConversationAsync(request.SessionId, request.Message, responseData.Response, request.ProjectPath);
|
|
}
|
|
|
|
_logger.LogDebug("Chat message sent successfully");
|
|
return ClaudeCodeResponse<string>.CreateSuccess(responseData.Response ?? string.Empty, response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<string>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error sending chat message");
|
|
return ClaudeCodeResponse<string>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<AnalysisResult>> AnalyzeCodeAsync(AnalysisRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Analyzing code: {FilePath}", request.FilePath ?? "inline code");
|
|
|
|
var apiRequest = new
|
|
{
|
|
filePath = request.FilePath,
|
|
code = request.Code,
|
|
analysisType = request.AnalysisType,
|
|
projectPath = request.ProjectPath,
|
|
options = request.Options
|
|
};
|
|
|
|
var response = await _httpClient.PostAsJsonAsync("/api/chat/analyze", apiRequest, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Analysis request failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<AnalysisResult>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<AnalysisApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Analysis API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<AnalysisResult>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Code analysis completed successfully");
|
|
return ClaudeCodeResponse<AnalysisResult>.CreateSuccess(responseData.Result!, response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<AnalysisResult>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error analyzing code");
|
|
return ClaudeCodeResponse<AnalysisResult>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<RateLimitInfo>> GetRateLimitStatusAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var rateLimitInfo = await _httpClient.GetRateLimitStatusAsync(cancellationToken);
|
|
return ClaudeCodeResponse<RateLimitInfo>.CreateSuccess(rateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting rate limit status");
|
|
return ClaudeCodeResponse<RateLimitInfo>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<List<ContextSearchResult>>> SearchContextAsync(string query, string? projectPath = null, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Searching context: {Query}", query);
|
|
|
|
var filters = new ContextSearchFilters
|
|
{
|
|
ProjectPath = projectPath,
|
|
MaxResults = 20
|
|
};
|
|
|
|
var response = await _contextService.SearchAllContextAsync(query, filters, cancellationToken);
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error searching context");
|
|
return ClaudeCodeResponse<List<ContextSearchResult>>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<bool>> StoreDecisionAsync(StoreDecisionRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Storing decision: {Decision}", request.Decision);
|
|
|
|
var insight = new ProjectInsight
|
|
{
|
|
ProjectPath = request.ProjectPath ?? string.Empty,
|
|
Type = "decision",
|
|
Category = request.Category,
|
|
Title = "User Decision",
|
|
Description = request.Decision,
|
|
Data = request.Metadata ?? new Dictionary<string, object>()
|
|
};
|
|
|
|
var response = await _contextService.StoreProjectInsightAsync(insight, cancellationToken);
|
|
return ClaudeCodeResponse<bool>.CreateSuccess(response.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error storing decision");
|
|
return ClaudeCodeResponse<bool>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
private async Task StoreConversationAsync(string sessionId, string userMessage, string assistantResponse, string? projectPath)
|
|
{
|
|
try
|
|
{
|
|
// Store user message
|
|
var userMsg = new ConversationMessage
|
|
{
|
|
SessionId = sessionId,
|
|
Role = "user",
|
|
Content = userMessage,
|
|
ProjectPath = projectPath
|
|
};
|
|
await _contextService.StoreConversationMessageAsync(userMsg);
|
|
|
|
// Store assistant response
|
|
var assistantMsg = new ConversationMessage
|
|
{
|
|
SessionId = sessionId,
|
|
Role = "assistant",
|
|
Content = assistantResponse,
|
|
ProjectPath = projectPath
|
|
};
|
|
await _contextService.StoreConversationMessageAsync(assistantMsg);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to store conversation messages");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// API response models
|
|
/// </summary>
|
|
internal class ChatApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Response { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
internal class AnalysisApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public AnalysisResult? Result { get; set; }
|
|
public string? Error { get; set; }
|
|
} |