381 lines
16 KiB
C#
Executable File
381 lines
16 KiB
C#
Executable File
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace MarketAlly.AIPlugin.ClaudeCode;
|
|
|
|
/// <summary>
|
|
/// Implementation of chat service for Claude Code sessions
|
|
/// </summary>
|
|
public class ChatService : IChatService
|
|
{
|
|
private readonly IRateLimitAwareHttpClient _httpClient;
|
|
private readonly ILogger<ChatService> _logger;
|
|
private readonly ClaudeCodeOptions _options;
|
|
|
|
public ChatService(
|
|
IRateLimitAwareHttpClient httpClient,
|
|
ILogger<ChatService> logger,
|
|
IOptions<ClaudeCodeOptions> options)
|
|
{
|
|
_httpClient = httpClient;
|
|
_logger = logger;
|
|
_options = options.Value;
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<ChatSession>> 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<ChatSession>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<StartSessionApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Start session API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<ChatSession>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Chat session started with ID: {SessionId}", responseData.Session?.Id);
|
|
return ClaudeCodeResponse<ChatSession>.CreateSuccess(responseData.Session!, response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<ChatSession>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error starting chat session");
|
|
return ClaudeCodeResponse<ChatSession>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<ChatMessage>> 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<ChatMessage>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<SendMessageApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Send message API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<ChatMessage>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Message sent successfully");
|
|
return ClaudeCodeResponse<ChatMessage>.CreateSuccess(responseData.Message!, response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<ChatMessage>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error sending message");
|
|
return ClaudeCodeResponse<ChatMessage>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<List<ChatMessage>>> 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<List<ChatMessage>>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<SessionHistoryApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Session history API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<List<ChatMessage>>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Retrieved {Count} messages from session history", responseData.Messages?.Count ?? 0);
|
|
return ClaudeCodeResponse<List<ChatMessage>>.CreateSuccess(responseData.Messages ?? new List<ChatMessage>(), response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<List<ChatMessage>>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting session history");
|
|
return ClaudeCodeResponse<List<ChatMessage>>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<List<ChatSession>>> 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<List<ChatSession>>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<ActiveSessionsApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Active sessions API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<List<ChatSession>>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Retrieved {Count} active sessions", responseData.Sessions?.Count ?? 0);
|
|
return ClaudeCodeResponse<List<ChatSession>>.CreateSuccess(responseData.Sessions ?? new List<ChatSession>(), response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<List<ChatSession>>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting active sessions");
|
|
return ClaudeCodeResponse<List<ChatSession>>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<bool>> 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<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("End session API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<bool>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Session ended 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 ending session");
|
|
return ClaudeCodeResponse<bool>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<ChatSession>> 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<ChatSession>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<UpdateSessionApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Update session context API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<ChatSession>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Session context updated successfully");
|
|
return ClaudeCodeResponse<ChatSession>.CreateSuccess(responseData.Session!, response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<ChatSession>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error updating session context");
|
|
return ClaudeCodeResponse<ChatSession>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<bool>> 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<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("Typing indicator API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<bool>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
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 setting typing indicator");
|
|
return ClaudeCodeResponse<bool>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
|
|
public async Task<ClaudeCodeResponse<SessionAnalytics>> 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<SessionAnalytics>.CreateError(error, response.StatusCode.ToString(), response.RateLimitInfo);
|
|
}
|
|
|
|
var responseData = response.DeserializeJson<SessionAnalyticsApiResponse>();
|
|
if (responseData?.Success != true)
|
|
{
|
|
var error = responseData?.Error ?? "Unknown error occurred";
|
|
_logger.LogError("Session analytics API returned error: {Error}", error);
|
|
return ClaudeCodeResponse<SessionAnalytics>.CreateError(error, "API_ERROR", response.RateLimitInfo);
|
|
}
|
|
|
|
_logger.LogDebug("Retrieved session analytics successfully");
|
|
return ClaudeCodeResponse<SessionAnalytics>.CreateSuccess(responseData.Analytics!, response.RateLimitInfo);
|
|
}
|
|
catch (RateLimitExceededException ex)
|
|
{
|
|
_logger.LogWarning("Rate limit exceeded: {Message}", ex.Message);
|
|
return ClaudeCodeResponse<SessionAnalytics>.CreateError(ex.Message, "RATE_LIMITED", ex.RateLimitInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting session analytics");
|
|
return ClaudeCodeResponse<SessionAnalytics>.CreateError(ex.Message, "EXCEPTION");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// API response models for chat operations
|
|
/// </summary>
|
|
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<ChatMessage>? Messages { get; set; }
|
|
public string? Error { get; set; }
|
|
}
|
|
|
|
internal class ActiveSessionsApiResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public List<ChatSession>? 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; }
|
|
} |