using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace MarketAlly.AIPlugin.ClaudeCode; /// /// Implementation of chat service for Claude Code sessions /// public class ChatService : IChatService { private readonly IRateLimitAwareHttpClient _httpClient; private readonly ILogger _logger; private readonly ClaudeCodeOptions _options; public ChatService( IRateLimitAwareHttpClient httpClient, ILogger logger, IOptions options) { _httpClient = httpClient; _logger = logger; _options = options.Value; } public async Task> StartSessionAsync(StartSessionRequest request, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Starting new chat session: {SessionName}", request.SessionName ?? "Unnamed"); var response = await _httpClient.PostAsJsonAsync("/api/chat/start-session", request, cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"Start session failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("Start session API returned error: {Error}", error); return ClaudeCodeResponse.CreateError(error, "API_ERROR", response.RateLimitInfo); } _logger.LogDebug("Chat session started with ID: {SessionId}", responseData.Session?.Id); return ClaudeCodeResponse.CreateSuccess(responseData.Session!, response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error starting chat session"); return ClaudeCodeResponse.CreateError(ex.Message, "EXCEPTION"); } } public async Task> SendMessageAsync(string sessionId, string message, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Sending message to session: {SessionId}", sessionId); var request = new { sessionId = sessionId, message = message }; var response = await _httpClient.PostAsJsonAsync("/api/chat/send-message", request, cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"Send message failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("Send message API returned error: {Error}", error); return ClaudeCodeResponse.CreateError(error, "API_ERROR", response.RateLimitInfo); } _logger.LogDebug("Message sent successfully"); return ClaudeCodeResponse.CreateSuccess(responseData.Message!, response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error sending message"); return ClaudeCodeResponse.CreateError(ex.Message, "EXCEPTION"); } } public async Task>> GetSessionHistoryAsync(string sessionId, int maxMessages = 50, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Getting session history: {SessionId}", sessionId); var response = await _httpClient.GetAsync($"/api/chat/sessions/{sessionId}/history?maxMessages={maxMessages}", cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"Get session history failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("Session history API returned error: {Error}", error); return ClaudeCodeResponse>.CreateError(error, "API_ERROR", response.RateLimitInfo); } _logger.LogDebug("Retrieved {Count} messages from session history", responseData.Messages?.Count ?? 0); return ClaudeCodeResponse>.CreateSuccess(responseData.Messages ?? new List(), response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error getting session history"); return ClaudeCodeResponse>.CreateError(ex.Message, "EXCEPTION"); } } public async Task>> GetActiveSessionsAsync(CancellationToken cancellationToken = default) { try { _logger.LogDebug("Getting active sessions"); var response = await _httpClient.GetAsync("/api/chat/sessions", cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"Get active sessions failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("Active sessions API returned error: {Error}", error); return ClaudeCodeResponse>.CreateError(error, "API_ERROR", response.RateLimitInfo); } _logger.LogDebug("Retrieved {Count} active sessions", responseData.Sessions?.Count ?? 0); return ClaudeCodeResponse>.CreateSuccess(responseData.Sessions ?? new List(), response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error getting active sessions"); return ClaudeCodeResponse>.CreateError(ex.Message, "EXCEPTION"); } } public async Task> EndSessionAsync(string sessionId, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Ending session: {SessionId}", sessionId); var response = await _httpClient.PostAsync($"/api/chat/sessions/{sessionId}/end", null, cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"End session failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("End session API returned error: {Error}", error); return ClaudeCodeResponse.CreateError(error, "API_ERROR", response.RateLimitInfo); } _logger.LogDebug("Session ended successfully"); return ClaudeCodeResponse.CreateSuccess(true, response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error ending session"); return ClaudeCodeResponse.CreateError(ex.Message, "EXCEPTION"); } } public async Task> UpdateSessionContextAsync(string sessionId, SessionContextUpdate updates, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Updating session context: {SessionId}", sessionId); var response = await _httpClient.PutAsync($"/api/chat/sessions/{sessionId}/context", new StringContent(System.Text.Json.JsonSerializer.Serialize(updates), System.Text.Encoding.UTF8, "application/json"), cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"Update session context failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("Update session context API returned error: {Error}", error); return ClaudeCodeResponse.CreateError(error, "API_ERROR", response.RateLimitInfo); } _logger.LogDebug("Session context updated successfully"); return ClaudeCodeResponse.CreateSuccess(responseData.Session!, response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error updating session context"); return ClaudeCodeResponse.CreateError(ex.Message, "EXCEPTION"); } } public async Task> SetTypingIndicatorAsync(string sessionId, bool isTyping, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Setting typing indicator for session: {SessionId} - {IsTyping}", sessionId, isTyping); var request = new { isTyping = isTyping }; var response = await _httpClient.PostAsJsonAsync($"/api/chat/sessions/{sessionId}/typing", request, cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"Set typing indicator failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("Typing indicator API returned error: {Error}", error); return ClaudeCodeResponse.CreateError(error, "API_ERROR", response.RateLimitInfo); } return ClaudeCodeResponse.CreateSuccess(true, response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error setting typing indicator"); return ClaudeCodeResponse.CreateError(ex.Message, "EXCEPTION"); } } public async Task> GetSessionAnalyticsAsync(string sessionId, CancellationToken cancellationToken = default) { try { _logger.LogDebug("Getting session analytics: {SessionId}", sessionId); var response = await _httpClient.GetAsync($"/api/chat/sessions/{sessionId}/analytics", cancellationToken); if (!response.IsSuccessStatusCode) { var error = $"Get session analytics failed: {response.ErrorMessage}"; _logger.LogError(error); return ClaudeCodeResponse.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo); } var responseData = response.DeserializeJson(); if (responseData?.Success != true) { var error = responseData?.Error ?? "Unknown error occurred"; _logger.LogError("Session analytics API returned error: {Error}", error); return ClaudeCodeResponse.CreateError(error, "API_ERROR", response.RateLimitInfo); } _logger.LogDebug("Retrieved session analytics successfully"); return ClaudeCodeResponse.CreateSuccess(responseData.Analytics!, response.RateLimitInfo); } catch (RateLimitExceededException ex) { _logger.LogWarning("Rate limit exceeded: {Message}", ex.Message); return ClaudeCodeResponse.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo); } catch (Exception ex) { _logger.LogError(ex, "Error getting session analytics"); return ClaudeCodeResponse.CreateError(ex.Message, "EXCEPTION"); } } } /// /// API response models for chat operations /// internal class StartSessionApiResponse { public bool Success { get; set; } public ChatSession? Session { get; set; } public string? Error { get; set; } } internal class SendMessageApiResponse { public bool Success { get; set; } public ChatMessage? Message { get; set; } public string? Error { get; set; } } internal class SessionHistoryApiResponse { public bool Success { get; set; } public List? Messages { get; set; } public string? Error { get; set; } } internal class ActiveSessionsApiResponse { public bool Success { get; set; } public List? Sessions { get; set; } public string? Error { get; set; } } internal class UpdateSessionApiResponse { public bool Success { get; set; } public ChatSession? Session { get; set; } public string? Error { get; set; } } internal class SessionAnalyticsApiResponse { public bool Success { get; set; } public SessionAnalytics? Analytics { get; set; } public string? Error { get; set; } }