366 lines
16 KiB
C#
Executable File
366 lines
16 KiB
C#
Executable File
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace MarketAlly.AIPlugin.ClaudeCode;
|
|
|
|
/// <summary>
|
|
/// Implementation of context management service for Claude Code
|
|
/// </summary>
|
|
public class ContextClaudeService : IContextClaudeService
|
|
{
|
|
private readonly IRateLimitAwareHttpClient _httpClient;
|
|
private readonly ILogger<ContextClaudeService> _logger;
|
|
private readonly ClaudeCodeOptions _options;
|
|
|
|
public ContextClaudeService(
|
|
IRateLimitAwareHttpClient httpClient,
|
|
ILogger<ContextClaudeService> logger,
|
|
IOptions<ClaudeCodeOptions> options)
|
|
{
|
|
_httpClient = httpClient;
|
|
_logger = logger;
|
|
_options = options.Value;
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<List<ContextEntry>>> GetRelevantContextAsync(
|
|
string query,
|
|
string? projectPath = null,
|
|
int maxResults = 10,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Getting relevant context for query: {Query}", query);
|
|
|
|
var request = new
|
|
{
|
|
query = query,
|
|
projectPath = projectPath,
|
|
maxResults = maxResults
|
|
};
|
|
|
|
var response = await _httpClient.PostAsJsonAsync("/api/context/search", request, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Context search failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<List<ContextEntry>>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<ContextSearchApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Context API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<List<ContextEntry>>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Found {Count} relevant context entries", responseData.Results?.Count ?? 0);
|
|
return ClaudeCodeResponse<List<ContextEntry>>.CreateSuccess(responseData.Results ?? new List<ContextEntry>(), response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<List<ContextEntry>>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting relevant context");
|
|
return ClaudeCodeResponse<List<ContextEntry>>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<string>> StoreContextAsync(ContextEntry entry, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Storing context entry: {Type}", entry.Type);
|
|
|
|
var response = await _httpClient.PostAsJsonAsync("/api/context/store", entry, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Context storage failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<string>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<ContextStoreApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Context storage API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<string>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Context entry stored with ID: {Id}", responseData.Id);
|
|
return ClaudeCodeResponse<string>.CreateSuccess(responseData.Id ?? 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 storing context");
|
|
return ClaudeCodeResponse<string>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<List<ConversationMessage>>> GetConversationHistoryAsync(
|
|
string sessionId,
|
|
int maxMessages = 50,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Getting conversation history for session: {SessionId}", sessionId);
|
|
|
|
var response = await _httpClient.GetAsync($"/api/conversations/{sessionId}/history?maxMessages={maxMessages}", cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Conversation history request failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<List<ConversationMessage>>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<ConversationHistoryApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Conversation history API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<List<ConversationMessage>>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Retrieved {Count} conversation messages", responseData.Messages?.Count ?? 0);
|
|
return ClaudeCodeResponse<List<ConversationMessage>>.CreateSuccess(responseData.Messages ?? new List<ConversationMessage>(), response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<List<ConversationMessage>>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting conversation history");
|
|
return ClaudeCodeResponse<List<ConversationMessage>>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<bool>> StoreConversationMessageAsync(ConversationMessage message, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Storing conversation message for session: {SessionId}", message.SessionId);
|
|
|
|
var response = await _httpClient.PostAsJsonAsync("/api/conversations/store-message", message, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Message storage failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<bool>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<SimpleApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Message storage API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<bool>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Conversation message stored successfully");
|
|
return ClaudeCodeResponse<bool>.CreateSuccess(true, response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<bool>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error storing conversation message");
|
|
return ClaudeCodeResponse<bool>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<List<ProjectInsight>>> GetProjectInsightsAsync(
|
|
string projectPath,
|
|
string? category = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Getting project insights for: {ProjectPath}", projectPath);
|
|
|
|
var queryParams = $"projectPath={Uri.EscapeDataString(projectPath)}";
|
|
if (!string.IsNullOrEmpty(category))
|
|
{
|
|
queryParams += $"&category={Uri.EscapeDataString(category)}";
|
|
}
|
|
|
|
var response = await _httpClient.GetAsync($"/api/learning/insights?{queryParams}", cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Project insights request failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<List<ProjectInsight>>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<ProjectInsightsApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Project insights API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<List<ProjectInsight>>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Retrieved {Count} project insights", responseData.Insights?.Count ?? 0);
|
|
return ClaudeCodeResponse<List<ProjectInsight>>.CreateSuccess(responseData.Insights ?? new List<ProjectInsight>(), response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<List<ProjectInsight>>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting project insights");
|
|
return ClaudeCodeResponse<List<ProjectInsight>>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<string>> StoreProjectInsightAsync(ProjectInsight insight, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Storing project insight: {Type} - {Title}", insight.Type, insight.Title);
|
|
|
|
var response = await _httpClient.PostAsJsonAsync("/api/learning/store-insight", insight, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Project insight storage failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<string>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<ContextStoreApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Project insight storage API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<string>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Project insight stored with ID: {Id}", responseData.Id);
|
|
return ClaudeCodeResponse<string>.CreateSuccess(responseData.Id ?? 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 storing project insight");
|
|
return ClaudeCodeResponse<string>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<List<ContextSearchResult>>> SearchAllContextAsync(
|
|
string query,
|
|
ContextSearchFilters? filters = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogDebug("Searching all context with query: {Query}", query);
|
|
|
|
var request = new
|
|
{
|
|
query = query,
|
|
filters = filters
|
|
};
|
|
|
|
var response = await _httpClient.PostAsJsonAsync("/api/context/search-all", request, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var error = $"Context search failed: {response.ErrorMessage}";
|
|
_logger.LogError(error);
|
|
return ClaudeCodeResponse<List<ContextSearchResult>>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<AllContextSearchApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Context search API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<List<ContextSearchResult>>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Found {Count} context search results", responseData.Results?.Count ?? 0);
|
|
return ClaudeCodeResponse<List<ContextSearchResult>>.CreateSuccess(responseData.Results ?? new List<ContextSearchResult>(), response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<List<ContextSearchResult>>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error searching context");
|
|
return ClaudeCodeResponse<List<ContextSearchResult>>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// API response models for context operations
|
|
/// </summary>
|
|
internal class ContextSearchApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public List<ContextEntry>? Results { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
internal class ContextStoreApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Id { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
internal class ConversationHistoryApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public List<ConversationMessage>? Messages { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
internal class ProjectInsightsApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public List<ProjectInsight>? Insights { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
internal class AllContextSearchApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public List<ContextSearchResult>? Results { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
internal class SimpleApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
} |