using MarketAlly.AIPlugin; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Net.Http; namespace MarketAlly.AIPlugin.Refactoring.Plugins { public interface ISecureCredentialStore { Task GetApiKeyAsync(string service); Task StoreCredentialAsync(string service, string credential); } public class EnvironmentCredentialStore : ISecureCredentialStore { public Task GetApiKeyAsync(string service) { var envVarName = $"{service.ToUpper()}_API_KEY"; var apiKey = Environment.GetEnvironmentVariable(envVarName); if (string.IsNullOrEmpty(apiKey)) { throw new InvalidOperationException($"API key for {service} not found in environment variable {envVarName}"); } return Task.FromResult(apiKey); } public Task StoreCredentialAsync(string service, string credential) { // Environment variables are read-only at runtime throw new NotSupportedException("Cannot store credentials in environment variables at runtime"); } } [AIPlugin("IntelligentDescription", "Generates intelligent project descriptions by analyzing code content, dependencies, and patterns using AI")] public class IntelligentDescriptionPlugin : IAIPlugin { [AIParameter("Path to project directory or solution file", required: true)] public string ProjectPath { get; set; } [AIParameter("Project type (library, application, tool, maui)", required: true)] public string ProjectType { get; set; } [AIParameter("Target framework (e.g., net8.0, net9.0-android)", required: true)] public string TargetFramework { get; set; } [AIParameter("List of dependencies/packages", required: false)] public List Dependencies { get; set; } = new List(); [AIParameter("Sample of key code files content", required: false)] public string CodeSample { get; set; } = ""; [AIParameter("List of key class and method names", required: false)] public List KeyComponents { get; set; } = new List(); [AIParameter("Maximum number of files to analyze for content", required: false)] public int MaxFilesToSample { get; set; } = 5; [AIParameter("Claude API key for intelligent analysis", required: true)] public string ApiKey { get; set; } [AIParameter("Claude model to use", required: false)] public string Model { get; set; } = "claude-3-5-sonnet-20241022"; [AIParameter("Analysis temperature (0.0-1.0)", required: false)] public double Temperature { get; set; } = 0.3; public IReadOnlyDictionary SupportedParameters => new Dictionary { ["projectPath"] = typeof(string), ["projectpath"] = typeof(string), ["projectType"] = typeof(string), ["projecttype"] = typeof(string), ["targetFramework"] = typeof(string), ["targetframework"] = typeof(string), ["dependencies"] = typeof(List), ["codeSample"] = typeof(string), ["codesample"] = typeof(string), ["keyComponents"] = typeof(List), ["keycomponents"] = typeof(List), ["maxFilesToSample"] = typeof(int), ["maxfiletosample"] = typeof(int), ["apiKey"] = typeof(string), ["apikey"] = typeof(string), ["model"] = typeof(string), ["temperature"] = typeof(double) }; private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly ISecureCredentialStore _credentialStore; public IntelligentDescriptionPlugin() { _httpClient = new HttpClient(); _logger = null; // Logger not available in parameterless constructor _credentialStore = new EnvironmentCredentialStore(); } public IntelligentDescriptionPlugin(HttpClient httpClient, ILogger logger, ISecureCredentialStore credentialStore = null) { _httpClient = httpClient; _logger = logger; _credentialStore = credentialStore ?? new EnvironmentCredentialStore(); } public async Task ExecuteAsync(IReadOnlyDictionary parameters) { try { // Extract parameters string projectPath = GetParameterValue(parameters, "projectPath", "projectpath")?.ToString(); string projectType = GetParameterValue(parameters, "projectType", "projecttype")?.ToString(); string targetFramework = GetParameterValue(parameters, "targetFramework", "targetframework")?.ToString(); var dependencies = GetListParameter(parameters, "dependencies") ?? new List(); string codeSample = GetParameterValue(parameters, "codeSample", "codesample")?.ToString() ?? ""; var keyComponents = GetListParameter(parameters, "keyComponents", "keycomponents") ?? new List(); int maxFilesToSample = GetIntParameter(parameters, "maxFilesToSample", "maxfiletosample", 5); string model = GetParameterValue(parameters, "model")?.ToString() ?? "claude-3-5-sonnet-20241022"; double temperature = GetDoubleParameter(parameters, "temperature", 0.3); if (string.IsNullOrEmpty(projectPath)) return new AIPluginResult(new ArgumentException("Project path is required"), "Missing project path"); // Get API key securely from credential store string apiKey; try { apiKey = await _credentialStore.GetApiKeyAsync("CLAUDE"); } catch (Exception ex) { return new AIPluginResult(ex, "Failed to retrieve API key from secure store"); } // Collect additional content if code sample is empty if (string.IsNullOrEmpty(codeSample) && Directory.Exists(projectPath)) { codeSample = await CollectRepresentativeContent(projectPath, maxFilesToSample); } // Generate intelligent description using Claude var description = await GenerateIntelligentDescription( projectPath, projectType, targetFramework, dependencies, codeSample, keyComponents, apiKey, model, temperature); return new AIPluginResult(new { Description = description, ProjectPath = projectPath, ProjectType = projectType, TargetFramework = targetFramework, AnalyzedDependencies = dependencies.Count, CodeSampleLength = codeSample.Length, KeyComponents = keyComponents.Count, Timestamp = DateTime.UtcNow }, "Intelligent description generated successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate intelligent description"); return new AIPluginResult(ex, $"Intelligent description generation failed: {ex.Message}"); } } private async Task CollectRepresentativeContent(string projectPath, int maxFiles) { var content = new StringBuilder(); try { var csharpFiles = Directory.GetFiles(projectPath, "*.cs", SearchOption.AllDirectories) .Where(f => !ShouldExcludeFile(f)) .Take(maxFiles) .ToList(); // Prioritize important files var prioritizedFiles = PrioritizeFiles(csharpFiles); foreach (var file in prioritizedFiles.Take(maxFiles)) { try { var fileInfo = new FileInfo(file); if (fileInfo.Length > 10000) continue; // Skip very large files var fileContent = await File.ReadAllTextAsync(file); var relativePath = Path.GetRelativePath(projectPath, file); content.AppendLine($"// File: {relativePath}"); content.AppendLine(fileContent); content.AppendLine(); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to read file {File}", file); } } } catch (Exception ex) { _logger.LogWarning(ex, "Failed to collect content from {ProjectPath}", projectPath); } return content.ToString(); } private List PrioritizeFiles(List files) { var prioritized = new List(); // High priority patterns var highPriorityPatterns = new[] { "Program", "Main", "Startup", "App", "Plugin", "Service", "Controller", "Manager" }; foreach (var pattern in highPriorityPatterns) { prioritized.AddRange(files.Where(f => Path.GetFileName(f).Contains(pattern, StringComparison.OrdinalIgnoreCase))); } // Add remaining files prioritized.AddRange(files.Except(prioritized)); return prioritized.Distinct().ToList(); } private bool ShouldExcludeFile(string filePath) { var fileName = Path.GetFileName(filePath); var excludePatterns = new[] { ".Designer.cs", ".generated.cs", ".g.cs", "AssemblyInfo.cs", "GlobalAssemblyInfo.cs", "TemporaryGeneratedFile", ".AssemblyAttributes.cs" }; return excludePatterns.Any(pattern => fileName.Contains(pattern, StringComparison.OrdinalIgnoreCase)); } private async Task GenerateIntelligentDescription( string projectPath, string projectType, string targetFramework, List dependencies, string codeSample, List keyComponents, string apiKey, string model, double temperature) { var projectName = Path.GetFileNameWithoutExtension(projectPath); var dependenciesText = dependencies.Any() ? string.Join(", ", dependencies.Take(10)) : "None specified"; var componentsText = keyComponents.Any() ? string.Join(", ", keyComponents.Take(10)) : "Not provided"; var prompt = $@"Analyze this .NET project and provide a compelling 1-2 sentence description of what it does from a user's perspective. PROJECT METADATA: - Name: {projectName} - Type: {projectType} - Framework: {targetFramework} - Key Dependencies: {dependenciesText} - Key Components: {componentsText} CODE SAMPLE: {(string.IsNullOrEmpty(codeSample) ? "No code sample provided" : codeSample.Substring(0, Math.Min(codeSample.Length, 3000)))} ANALYSIS INSTRUCTIONS: 1. Focus on what the application/library DOES, not how it's built 2. Identify the primary purpose and value proposition 3. Use business/user language, not technical implementation details 4. Be specific about the domain (trading, productivity, gaming, etc.) if clear 5. Mention key capabilities that users would care about 6. Keep it concise but compelling EXAMPLES: - ""A financial trading application that provides real-time market data and portfolio management for retail investors"" - ""A cross-platform productivity tool that helps developers manage project documentation and code quality"" - ""A plugin-based refactoring toolkit that uses AI to automatically improve code quality and documentation"" Your response should be ONLY the description - no explanations or additional text."; return await CallClaudeAPI(prompt, apiKey, model, temperature); } private async Task CallClaudeAPI(string prompt, string apiKey, string model, double temperature) { var request = new { model = model, max_tokens = 150, temperature = temperature, messages = new[] { new { role = "user", content = prompt } } }; var requestJson = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var httpRequest = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages"); httpRequest.Headers.Add("X-API-Key", apiKey); httpRequest.Headers.Add("Anthropic-Version", "2023-06-01"); httpRequest.Content = new StringContent(requestJson, Encoding.UTF8, "application/json"); var response = await _httpClient.SendAsync(httpRequest); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); throw new HttpRequestException($"Claude API returned {response.StatusCode}: {errorContent}"); } var responseContent = await response.Content.ReadAsStringAsync(); using var document = JsonDocument.Parse(responseContent); var root = document.RootElement; if (root.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.Array) { var firstItem = content.EnumerateArray().FirstOrDefault(); if (firstItem.TryGetProperty("text", out var text)) { return text.GetString()?.Trim() ?? "Unable to generate description"; } } return "Unable to generate description"; } // Helper methods for parameter extraction private object GetParameterValue(IReadOnlyDictionary parameters, params string[] keys) { foreach (var key in keys) { if (parameters.TryGetValue(key, out var value)) return value; } return null; } private List GetListParameter(IReadOnlyDictionary parameters, params string[] keys) { var value = GetParameterValue(parameters, keys); return value switch { List list => list, string[] array => array.ToList(), string str when !string.IsNullOrEmpty(str) => str.Split(',').Select(s => s.Trim()).ToList(), _ => null }; } private int GetIntParameter(IReadOnlyDictionary parameters, string key1, string key2, int defaultValue = 0) { var value = GetParameterValue(parameters, key1, key2); return value != null ? Convert.ToInt32(value) : defaultValue; } private double GetDoubleParameter(IReadOnlyDictionary parameters, string key, double defaultValue = 0.0) { var value = GetParameterValue(parameters, key); return value != null ? Convert.ToDouble(value) : defaultValue; } } }