607 lines
17 KiB
C#
Executable File
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;
|
|
} |