366 lines
13 KiB
C#
Executable File
366 lines
13 KiB
C#
Executable File
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<string> GetApiKeyAsync(string service);
|
|
Task StoreCredentialAsync(string service, string credential);
|
|
}
|
|
|
|
public class EnvironmentCredentialStore : ISecureCredentialStore
|
|
{
|
|
public Task<string> 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<string> Dependencies { get; set; } = new List<string>();
|
|
|
|
[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<string> KeyComponents { get; set; } = new List<string>();
|
|
|
|
[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<string, Type> SupportedParameters => new Dictionary<string, Type>
|
|
{
|
|
["projectPath"] = typeof(string),
|
|
["projectpath"] = typeof(string),
|
|
["projectType"] = typeof(string),
|
|
["projecttype"] = typeof(string),
|
|
["targetFramework"] = typeof(string),
|
|
["targetframework"] = typeof(string),
|
|
["dependencies"] = typeof(List<string>),
|
|
["codeSample"] = typeof(string),
|
|
["codesample"] = typeof(string),
|
|
["keyComponents"] = typeof(List<string>),
|
|
["keycomponents"] = typeof(List<string>),
|
|
["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<IAIPlugin> _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<IntelligentDescriptionPlugin> logger, ISecureCredentialStore credentialStore = null)
|
|
{
|
|
_httpClient = httpClient;
|
|
_logger = logger;
|
|
_credentialStore = credentialStore ?? new EnvironmentCredentialStore();
|
|
}
|
|
|
|
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> 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>();
|
|
string codeSample = GetParameterValue(parameters, "codeSample", "codesample")?.ToString() ?? "";
|
|
var keyComponents = GetListParameter(parameters, "keyComponents", "keycomponents") ?? new List<string>();
|
|
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<string> 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<string> PrioritizeFiles(List<string> files)
|
|
{
|
|
var prioritized = new List<string>();
|
|
|
|
// 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<string> GenerateIntelligentDescription(
|
|
string projectPath, string projectType, string targetFramework,
|
|
List<string> dependencies, string codeSample, List<string> 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<string> 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<string, object> parameters, params string[] keys)
|
|
{
|
|
foreach (var key in keys)
|
|
{
|
|
if (parameters.TryGetValue(key, out var value))
|
|
return value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private List<string> GetListParameter(IReadOnlyDictionary<string, object> parameters, params string[] keys)
|
|
{
|
|
var value = GetParameterValue(parameters, keys);
|
|
return value switch
|
|
{
|
|
List<string> list => list,
|
|
string[] array => array.ToList(),
|
|
string str when !string.IsNullOrEmpty(str) => str.Split(',').Select(s => s.Trim()).ToList(),
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private int GetIntParameter(IReadOnlyDictionary<string, object> 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<string, object> parameters, string key, double defaultValue = 0.0)
|
|
{
|
|
var value = GetParameterValue(parameters, key);
|
|
return value != null ? Convert.ToDouble(value) : defaultValue;
|
|
}
|
|
}
|
|
} |