MarketAlly.AIPlugin.Extensions/Test.Context/ContextClaudeService.cs

607 lines
17 KiB
C#
Executable File

using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Context;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Test.Context;
/// <summary>
/// Simplified ContextClaudeService that uses AIPluginHelper properly
/// </summary>
public class ContextClaudeService
{
private readonly ILogger<ContextClaudeService> _logger;
private readonly AIPluginRegistry _registry;
private readonly Claude4Settings _settings;
private readonly HttpClient _httpClient;
private string _currentSessionId = "";
private string _currentTopic = "";
private string _currentProject = "";
public ContextClaudeService(
ILogger<ContextClaudeService> logger,
AIPluginRegistry registry,
IOptions<Claude4Settings> settings,
HttpClient httpClient)
{
_logger = logger;
_registry = registry;
_settings = settings.Value;
_httpClient = httpClient;
}
#region Web API Methods
/// <summary>
/// Process a single message for web API - returns Claude's response
/// </summary>
public async Task<string> ProcessSingleMessageAsync(string message, string topic, string projectPath, List<object>? previousMessages = null)
{
try
{
_logger.LogInformation("Processing single message for topic: {Topic}", topic);
_currentTopic = topic;
_currentProject = projectPath;
_currentSessionId = Guid.NewGuid().ToString();
// Build conversation history
var messages = new List<object>();
// Add system message with context
var systemPrompt = await BuildSystemPromptWithContextAsync(topic, projectPath);
messages.Add(new { role = "system", content = systemPrompt });
// Add previous messages if provided
if (previousMessages != null && previousMessages.Any())
{
messages.AddRange(previousMessages.TakeLast(10));
}
// Add current message
messages.Add(new { role = "user", content = message });
// Call Claude using AIPluginHelper
var claudeResponse = await CallClaudeWithAIPluginHelperAsync(messages);
// Auto-store important information
await AutoStoreImportantInformationAsync(message, claudeResponse);
return claudeResponse;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error processing message");
throw new InvalidOperationException("Claude integration temporarily unavailable. Please try again later.", ex);
}
}
/// <summary>
/// Process a command for web API
/// </summary>
public async Task<string> ProcessCommandAsync(string command, List<string> arguments, string projectPath)
{
try
{
_currentProject = projectPath;
var cmd = command.ToLower();
return cmd switch
{
"/search" => await HandleSearchCommandAsync(arguments, projectPath),
"/context" => await HandleContextCommandAsync(projectPath),
"/store" => await HandleStoreCommandAsync(arguments, projectPath),
"/project" => await HandleProjectCommandAsync(projectPath),
"/help" => HandleHelpCommand(),
_ => $"Unknown command: {command}. Available commands: /search, /context, /store, /project, /help"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process command: {Command}", command);
return $"Failed to execute command '{command}'. Please try again.";
}
}
/// <summary>
/// Initialize a session and return initialization data
/// </summary>
public async Task<object> InitializeSessionAsync(string topic, string projectPath)
{
try
{
_currentTopic = topic;
_currentProject = projectPath;
_currentSessionId = Guid.NewGuid().ToString();
var result = await _registry.CallFunctionAsync("ConversationContinuity", new Dictionary<string, object>
{
["action"] = "initialize",
["topic"] = topic,
["projectPath"] = projectPath
});
return new
{
SessionId = _currentSessionId,
Topic = topic,
ProjectPath = projectPath,
StartTime = DateTime.UtcNow,
Success = result.Success,
RecentContext = result.Success ? result.Data : null,
Message = result.Success ? "Session initialized with context" : "Session initialized without context"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize session");
return new
{
SessionId = Guid.NewGuid().ToString(),
Topic = topic,
ProjectPath = projectPath,
StartTime = DateTime.UtcNow,
Success = false,
Error = "Session initialization failed"
};
}
}
#endregion
#region Interactive Chat Methods
/// <summary>
/// Interactive Claude chat mode with context management
/// </summary>
public async Task<string> InteractiveContextChatAsync(string topic, string projectPath)
{
try
{
Console.WriteLine("=".PadRight(60, '='));
Console.WriteLine("CLAUDE INTERACTIVE CHAT WITH CONTEXT MANAGEMENT");
Console.WriteLine("=".PadRight(60, '='));
Console.WriteLine($"Topic: {topic}");
Console.WriteLine($"Project: {projectPath}");
Console.WriteLine();
// Initialize session
await InitializeSessionAsync(topic, projectPath);
var messages = new List<object>();
var systemPrompt = await BuildSystemPromptWithContextAsync(topic, projectPath);
messages.Add(new { role = "system", content = systemPrompt });
Console.WriteLine("Chat started! Type '/end' to finish.");
Console.WriteLine();
while (true)
{
Console.Write("You: ");
var userInput = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(userInput)) continue;
if (userInput == "/end") break;
// Handle commands
if (userInput.StartsWith("/"))
{
var parts = userInput.Split(' ', 2);
var cmd = parts[0];
var args = parts.Length > 1 ? parts[1].Split(' ').ToList() : new List<string>();
var cmdResult = await ProcessCommandAsync(cmd, args, projectPath);
Console.WriteLine($"[COMMAND] {cmdResult}");
Console.WriteLine();
continue;
}
// Add user message
messages.Add(new { role = "user", content = userInput });
try
{
var claudeResponse = await CallClaudeWithAIPluginHelperAsync(messages);
Console.WriteLine($"Claude: {claudeResponse}");
Console.WriteLine();
// Add Claude's response
messages.Add(new { role = "assistant", content = claudeResponse });
// Auto-store important information
await AutoStoreImportantInformationAsync(userInput, claudeResponse);
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] {ex.Message}");
Console.WriteLine();
}
}
return "Interactive chat session completed";
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Interactive chat failed: {ex.Message}");
return $"Chat session failed: {ex.Message}";
}
}
#endregion
#region Core Claude Integration Using AIPluginHelper
/// <summary>
/// Call Claude using the existing AIPluginHelper framework
/// </summary>
private async Task<string> CallClaudeWithAIPluginHelperAsync(List<object> messages)
{
try
{
// Get function definitions from registry
var functionDefinitions = _registry.GetAllPluginSchemas();
// Convert messages to format expected by AIPluginHelper
var dynamicMessages = messages.Select(msg =>
{
var msgJson = JsonSerializer.Serialize(msg);
var msgDict = JsonSerializer.Deserialize<Dictionary<string, object>>(msgJson);
return new
{
Role = msgDict?["role"]?.ToString() ?? "user",
Content = msgDict?["content"]?.ToString() ?? ""
};
});
// Use AIPluginHelper to build request
var requestJson = AIPluginHelper.SerializeRequestWithTools(
AIPluginHelper.AIModel.Claude,
dynamicMessages,
functionDefinitions,
_settings.DefaultModel,
_settings.DefaultTemperature,
_settings.DefaultMaxTokens,
"auto"
);
// Send request
var responseJson = await SendClaudeRequestAsync(requestJson);
// Process response
var claudeResponse = JsonSerializer.Deserialize<ClaudeApiResponse>(responseJson);
return await ProcessClaudeResponseAsync(claudeResponse, messages);
}
catch (Exception ex)
{
_logger.LogError(ex, "Claude API call failed");
throw;
}
}
/// <summary>
/// Send request to Claude API
/// </summary>
private async Task<string> SendClaudeRequestAsync(string requestJson)
{
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("x-api-key", _settings.ApiKey);
_httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("https://api.anthropic.com/v1/messages", content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"Claude API error: {response.StatusCode} - {errorContent}");
}
return await response.Content.ReadAsStringAsync();
}
/// <summary>
/// Process Claude's response and handle tool calls
/// </summary>
private async Task<string> ProcessClaudeResponseAsync(ClaudeApiResponse claudeResponse, List<object> messages)
{
if (claudeResponse?.Content == null || !claudeResponse.Content.Any())
{
return "I apologize, but I didn't receive a proper response. Please try again.";
}
var textResponse = "";
var hasToolCalls = false;
// Process each content block
foreach (var block in claudeResponse.Content)
{
if (block.Type == "text")
{
textResponse += block.Text;
}
else if (block.Type == "tool_use")
{
hasToolCalls = true;
// Execute the tool call
var toolResult = await ExecutePluginAsync(block.Name, block.Input ?? new Dictionary<string, object>());
// For now, just append tool results to text response
textResponse += $"\n\n[Tool Result: {toolResult}]";
}
}
return textResponse.Trim();
}
/// <summary>
/// Execute a plugin using the registry
/// </summary>
private async Task<string> ExecutePluginAsync(string? pluginName, Dictionary<string, object> parameters)
{
try
{
if (string.IsNullOrEmpty(pluginName))
return "Invalid plugin name";
// Convert JsonElement parameters to proper types
var convertedParameters = ConvertJsonElementParameters(parameters);
var result = await _registry.CallFunctionAsync(pluginName, convertedParameters);
if (result.Success)
{
return JsonSerializer.Serialize(result.Data, new JsonSerializerOptions { WriteIndented = true });
}
else
{
return $"Plugin execution failed: {result.Message}";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Plugin execution failed: {PluginName}", pluginName);
return $"Error executing {pluginName}: {ex.Message}";
}
}
/// <summary>
/// Convert JsonElement parameters to proper .NET types
/// </summary>
private Dictionary<string, object> ConvertJsonElementParameters(Dictionary<string, object> parameters)
{
var converted = new Dictionary<string, object>();
foreach (var param in parameters)
{
if (param.Value is JsonElement jsonElement)
{
converted[param.Key] = ConvertJsonElementToObject(jsonElement);
}
else
{
converted[param.Key] = param.Value;
}
}
return converted;
}
private object ConvertJsonElementToObject(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.String:
return element.GetString() ?? "";
case JsonValueKind.Number:
if (element.TryGetInt32(out int intValue))
return intValue;
return element.GetDouble();
case JsonValueKind.True:
return true;
case JsonValueKind.False:
return false;
case JsonValueKind.Null:
return null;
case JsonValueKind.Array:
var array = new List<object>();
foreach (var item in element.EnumerateArray())
{
array.Add(ConvertJsonElementToObject(item));
}
return array;
case JsonValueKind.Object:
var dict = new Dictionary<string, object>();
foreach (var property in element.EnumerateObject())
{
dict[property.Name] = ConvertJsonElementToObject(property.Value);
}
return dict;
default:
return element.GetRawText();
}
}
#endregion
#region Helper Methods
private async Task<string> BuildSystemPromptWithContextAsync(string topic, string projectPath)
{
var prompt = new StringBuilder();
prompt.AppendLine($"You are Claude, an AI assistant helping with: {topic}");
prompt.AppendLine();
prompt.AppendLine("You have access to context management tools for storing and retrieving information.");
prompt.AppendLine("Use these tools to maintain conversation continuity and reference previous discussions.");
return prompt.ToString();
}
private async Task AutoStoreImportantInformationAsync(string userInput, string claudeResponse)
{
var importantKeywords = new[] { "decision", "choose", "implement", "recommend", "solution", "approach", "decided" };
if (importantKeywords.Any(keyword => claudeResponse.ToLower().Contains(keyword)))
{
try
{
await _registry.CallFunctionAsync("ContextStorage", new Dictionary<string, object>
{
["contextType"] = "insight",
["summary"] = $"Important discussion point ({DateTime.Now:HH:mm})",
["content"] = $"User: {userInput}\n\nClaude: {claudeResponse}",
["priority"] = "medium",
["tags"] = $"{_currentTopic},auto-stored,important",
["projectPath"] = _currentProject
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to auto-store important information");
}
}
}
#endregion
#region Command Handlers
private async Task<string> HandleSearchCommandAsync(List<string> arguments, string projectPath)
{
if (!arguments.Any())
return "Usage: /search <query>";
var query = string.Join(" ", arguments);
var result = await _registry.CallFunctionAsync("ContextSearch", new Dictionary<string, object>
{
["query"] = query,
["maxResults"] = 5,
["includeContent"] = true,
["projectPath"] = projectPath
});
return result.Success ? FormatSearchResults(query, result.Data) : $"Search failed: {result.Message}";
}
private async Task<string> HandleContextCommandAsync(string projectPath)
{
var result = await _registry.CallFunctionAsync("ContextRetrieval", new Dictionary<string, object>
{
["contextType"] = "all",
["projectPath"] = projectPath,
["conversationLimit"] = 5
});
return result.Success ? "Context retrieved successfully" : $"Failed: {result.Message}";
}
private async Task<string> HandleStoreCommandAsync(List<string> arguments, string projectPath)
{
if (!arguments.Any())
return "Usage: /store <summary>";
var summary = string.Join(" ", arguments);
var result = await _registry.CallFunctionAsync("ContextStorage", new Dictionary<string, object>
{
["contextType"] = "conversation",
["summary"] = summary,
["content"] = $"Manual storage: {summary}",
["priority"] = "medium",
["tags"] = $"{_currentTopic},manual-store",
["projectPath"] = projectPath
});
return result.Success ? $"✅ Stored: {summary}" : $"❌ Storage failed: {result.Message}";
}
private async Task<string> HandleProjectCommandAsync(string projectPath)
{
var result = await _registry.CallFunctionAsync("ConversationContinuity", new Dictionary<string, object>
{
["action"] = "get_project_context",
["projectPath"] = projectPath
});
return result.Success ? "Project context retrieved" : $"Failed: {result.Message}";
}
private string HandleHelpCommand()
{
return @"Available Commands:
/context - Show recent context
/search <query> - Search stored context
/store <summary> - Store discussion point
/project - Show project context
/help - Show this help";
}
private string FormatSearchResults(string query, object? data)
{
return $"Search results for '{query}' - {(data != null ? "Found results" : "No results")}";
}
#endregion
#region Claude API Response Models
public class ClaudeApiResponse
{
[JsonPropertyName("content")]
public List<ClaudeContentBlock> Content { get; set; } = new();
}
public class ClaudeContentBlock
{
[JsonPropertyName("type")]
public string Type { get; set; } = "";
[JsonPropertyName("text")]
public string? Text { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("input")]
public Dictionary<string, object>? Input { get; set; }
}
#endregion
#region Public Convenience Methods
public async Task<bool> StoreImportantDecisionAsync(string sessionId, string userMessage, string assistantResponse, string? projectPath = null)
{
try
{
var result = await _registry.CallFunctionAsync("ContextStorage", new Dictionary<string, object>
{
["contextType"] = "conversation",
["summary"] = $"Chat decision: {userMessage.Substring(0, Math.Min(50, userMessage.Length))}...",
["content"] = $"**User:** {userMessage}\n\n**Claude:** {assistantResponse}",
["priority"] = "medium",
["tags"] = "chat-session,auto-stored,important-decision",
["projectPath"] = projectPath ?? _currentProject
});
return result.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to store important decision");
return false;
}
}
#endregion
}
// Add Claude4Settings for your Test.Context project
public class Claude4Settings
{
public string ApiKey { get; set; } = "";
public string DefaultModel { get; set; } = "claude-3-5-sonnet-20241022";
public double DefaultTemperature { get; set; } = 0.3;
public int DefaultMaxTokens { get; set; } = 8000;
}