diff --git a/CodeBase/CodeBase.csproj b/CodeBase/CodeBase.csproj index dba28e0..b41a6e1 100644 --- a/CodeBase/CodeBase.csproj +++ b/CodeBase/CodeBase.csproj @@ -12,6 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/CodeBase/Controllers/CodeController.cs b/CodeBase/Controllers/CodeController.cs index deaf857..55fabb2 100644 --- a/CodeBase/Controllers/CodeController.cs +++ b/CodeBase/Controllers/CodeController.cs @@ -11,12 +11,6 @@ namespace CodeBase.Controllers public class CodeController(CodeService service, ScipProcessingService scipProcessingService) : ControllerBase { - [HttpPost("analyze")] - public async Task AnalyzeRepository(string path, string name) - { - var chunks = await service.GetCodeChunksAsync(path, name); - return chunks[0]; - } [HttpPost("answer")] public async Task GetAnswer(string question, string name) { @@ -25,9 +19,9 @@ namespace CodeBase.Controllers } [HttpPost("parser")] - public async Task ParserProject(string path, string name, string lang) + public async Task ParserProject(string path, string name, Language language) { - await scipProcessingService.ProcessAndSaveProjectAsync(path, lang, name); + await scipProcessingService.ProcessAndSaveProjectAsync(path, language.ToString(), name); return "Проект добавлен в базу"; } } diff --git a/CodeBase/Models/Enums.cs b/CodeBase/Models/Enums.cs new file mode 100644 index 0000000..72f111f --- /dev/null +++ b/CodeBase/Models/Enums.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace CodeBase.Models +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum Language + { + csharp, + python, + typescript, + go + } +} diff --git a/CodeBase/Models/GraphNodeContext.cs b/CodeBase/Models/GraphNodeContext.cs deleted file mode 100644 index 6c3fa76..0000000 --- a/CodeBase/Models/GraphNodeContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace CodeBase.Models -{ - public class GraphNodeContext - { - public string MethodName { get; set; } - public string FilePath { get; set; } - public string Content { get; set; } - - // Графовые связи, которые мы вытащим из Neo4j - - // Кого вызывает этот метод (и какие классы/енумы использует) - public List OutgoingDependencies { get; set; } = new(); - - // Кто вызывает этот метод (кто от него зависит) - public List IncomingDependencies { get; set; } = new(); - } -} diff --git a/CodeBase/Program.cs b/CodeBase/Program.cs index f4a37c4..3c9ca29 100644 --- a/CodeBase/Program.cs +++ b/CodeBase/Program.cs @@ -4,6 +4,7 @@ using CodeBase.Repositories; using CodeBase.Services; using Microsoft.Build.Locator; using Neo4j.Driver; +using System.Text.Json.Serialization; if (!MSBuildLocator.IsRegistered) { @@ -28,12 +29,14 @@ builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddScoped(); -builder.Services.AddTransient(); builder.Services.AddTransient(); -builder.Services.AddTransient(); builder.Services.AddTransient(); -builder.Services.AddControllers(); +builder.Services.AddControllers().AddJsonOptions(options => +{ + // Добавляем конвертер строковых енумов глобально + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); ; builder.Services.AddSwaggerGen(); var app = builder.Build(); @@ -58,12 +61,4 @@ app.UseAuthorization(); app.MapControllers(); -using (var scope = app.Services.CreateScope()) -{ - var graphRepo = scope.ServiceProvider.GetRequiredService(); - Console.WriteLine("Проверяем и создаем векторный индекс в Neo4j..."); - await graphRepo.InitializeDbAsync(); - Console.WriteLine("Индекс готов!"); -} - app.Run(); diff --git a/CodeBase/Repositories/GraphRepository.cs b/CodeBase/Repositories/GraphRepository.cs deleted file mode 100644 index b736855..0000000 --- a/CodeBase/Repositories/GraphRepository.cs +++ /dev/null @@ -1,139 +0,0 @@ -using CodeBase.Models; -using Neo4j.Driver; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -public class GraphRepository -{ - private readonly IDriver _driver; - - public GraphRepository(IDriver driver) - { - _driver = driver; - } - - // ========================================== - // 0. ИНИЦИАЛИЗАЦИЯ (Создаем пространство для векторов) - // ========================================== - public async Task InitializeDbAsync() - { - await using var session = _driver.AsyncSession(); - - await session.ExecuteWriteAsync(async tx => - { - // Говорим базе: "Создай индекс для поиска по сходству, если его еще нет. - // Размер вектора 768 (GraphCodeBERT), алгоритм - косинусное расстояние" - await tx.RunAsync(@" - CREATE VECTOR INDEX code_vectors IF NOT EXISTS - FOR (c:CodeChunk) ON (c.vector) - OPTIONS { indexConfig: { - `vector.dimensions`: 768, - `vector.similarity_function`: 'cosine' - }}" - ); - }); - } - - // ========================================== - // 1. СОХРАНЕНИЕ УЗЛОВ И ВЕКТОРОВ - // ========================================== - public async Task SaveChunksAsync(string projectName, List chunks) - { - await using var session = _driver.AsyncSession(); - - // 1. Делаем данные "безопасными" для базы - var parameters = chunks.Where(c => c.Embedding != null).Select(chunk => new - { - id = Guid.NewGuid().ToString(), - projectName = projectName, - filePath = chunk.FilePath, - methodName = chunk.EntityName, - content = chunk.Content, - vector = chunk.Embedding, - // Защита от null и пустых строк (база их не переварит в цикле FOREACH) - outgoingCalls = chunk.OutgoingCalls != null - ? chunk.OutgoingCalls.Where(x => !string.IsNullOrWhiteSpace(x)).ToList() - : new List() - }).ToList(); - - await session.ExecuteWriteAsync(async tx => - { - await tx.RunAsync(@" - UNWIND $batch AS chunk - - // Создаем или обновляем основной метод - MERGE (c:CodeChunk { methodName: chunk.methodName }) - SET c.id = chunk.id, - c.projectName = chunk.projectName, - c.filePath = chunk.filePath, - c.content = chunk.content, - c.vector = chunk.vector - - // Рисуем связи только для валидных вызовов - FOREACH (calledMethod IN chunk.outgoingCalls | - MERGE (target:CodeChunk { methodName: calledMethod }) - MERGE (c)-[:CALLS]->(target) - ) - ", new { batch = parameters }); - }); - } - - // ========================================== - // 2. ПОИСК (Пока только по вектору, связи добавим позже) - // ========================================== - public async Task> SearchAsync(string projectName, float[] queryVector, int topK = 15) - { - await using var session = _driver.AsyncSession(); - - return await session.ExecuteReadAsync(async tx => - { - var cursor = await tx.RunAsync(@" - // 1. Ищем самые подходящие узлы по векторному сходству - CALL db.index.vector.queryNodes('code_vectors', $topK, $queryVector) - YIELD node AS c, score - WHERE c.projectName = $projectName - - // 2. Ищем стрелочки ВНИЗ (Кого вызывает этот метод?) - // OPTIONAL MATCH гарантирует, что мы не потеряем узел, если связей нет - OPTIONAL MATCH (c)-[:CALLS]->(out:CodeChunk) - // Собираем имена вызываемых методов в массив - WITH c, score, collect(DISTINCT out.methodName) AS outgoingDependencies - - // 3. Ищем стрелочки ВВЕРХ (Кто вызывает этот метод?) - OPTIONAL MATCH (in:CodeChunk)-[:CALLS]->(c) - WITH c, score, outgoingDependencies, collect(DISTINCT in.methodName) AS incomingDependencies - - // 4. Возвращаем готовую структуру для C# - RETURN - c.methodName AS methodName, - c.filePath AS filePath, - c.content AS content, - outgoingDependencies, - incomingDependencies, - score - ORDER BY score DESC - ", new { topK, queryVector, projectName }); - - var results = new List(); - - while (await cursor.FetchAsync()) - { - var record = cursor.Current; - results.Add(new GraphNodeContext - { - MethodName = record["methodName"].As(), - FilePath = record["filePath"].As(), - Content = record["content"].As(), - - // Драйвер Neo4j возвращает массивы как IList, - // поэтому аккуратно кастуем их в наши списки строк - OutgoingDependencies = record["outgoingDependencies"].As>().ToList(), - IncomingDependencies = record["incomingDependencies"].As>().ToList() - }); - } - return results; - }); - } -} \ No newline at end of file diff --git a/CodeBase/Services/ChunkService.cs b/CodeBase/Services/ChunkService.cs deleted file mode 100644 index 52f0845..0000000 --- a/CodeBase/Services/ChunkService.cs +++ /dev/null @@ -1,190 +0,0 @@ -using CodeBase.Models; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.MSBuild; - -namespace CodeBase.Services -{ - public class ChunkService(VectorizationService vectorizationService) - { - public List ChunkCSharpFile(string filePath, string fileContent) - { - var chunks = new List(); - var syntaxTree = CSharpSyntaxTree.ParseText(fileContent); - var root = syntaxTree.GetRoot(); - - var compilation = CSharpCompilation.Create("MyAnalysis") - .AddSyntaxTrees(syntaxTree) - // Подкидываем базовые библиотеки .NET, чтобы он узнал System.Linq и прочее - .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)); - - var semanticModel = compilation.GetSemanticModel(syntaxTree); - - // 1. Собираем методы (как и раньше) - var methods = root.DescendantNodes().OfType(); - foreach (var method in methods) - { - var chunk = new CodeChunk - { - FilePath = filePath, - EntityName = method.Identifier.Text, - Content = method.ToFullString().Trim() - }; - - var invocations = method.DescendantNodes().OfType(); - - foreach (var invocation in invocations) - { - var symbolInfo = semanticModel.GetSymbolInfo(invocation); - - if (symbolInfo.Symbol is IMethodSymbol methodSymbol) - { - // Получаем полный путь пространства имен (например, "System.Linq") - string namespaceName = methodSymbol.ContainingNamespace.ToString(); - - // Пропускаем все системные вызовы .NET - if (namespaceName.StartsWith("System") || namespaceName.StartsWith("Microsoft")) - { - continue; - } - - // Если это наш метод (например, "Bas.Core.Services"), добавляем его - chunk.OutgoingCalls.Add(methodSymbol.Name); - Console.WriteLine($"[ПАРСЕР] Добавлена бизнес-связь -> {methodSymbol.Name}"); - } - } - - chunk.OutgoingCalls = chunk.OutgoingCalls.Distinct().ToList(); - - chunks.Add(chunk); - } - - // 2. ДОБАВЛЯЕМ СБОР ENUM (Перечислений) - var enums = root.DescendantNodes().OfType(); - foreach (var enumSyntax in enums) - { - chunks.Add(new CodeChunk - { - FilePath = filePath, - EntityName = enumSyntax.Identifier.Text, // Сохраняем имя енама - Content = enumSyntax.ToFullString().Trim() - }); - } - - // 3. ДОБАВЛЯЕМ СБОР КЛАССОВ-МОДЕЛЕЙ (без методов) - var classes = root.DescendantNodes().OfType(); - foreach (var classSyntax in classes) - { - // Берем только свойства, чтобы понимать структуру модели - var properties = classSyntax.Members.OfType(); - if (properties.Any()) - { - chunks.Add(new CodeChunk - { - FilePath = filePath, - EntityName = classSyntax.Identifier.Text, - // Сохраняем объявление класса и его свойства - Content = $"class {classSyntax.Identifier.Text} {{\n" + - string.Join("\n", properties.Select(p => p.ToFullString().Trim())) + - "\n}" - }); - } - } - - return chunks; - } - - public async Task> ParseProjectAsync(string csprojPath) - { - // 1. Создаем воркспейс и загружаем проект (это может занять пару секунд) - using var workspace = MSBuildWorkspace.Create(); - Console.WriteLine("Загружаем проект и строим семантическую модель..."); - var project = await workspace.OpenProjectAsync(csprojPath); - - var chunksList = new List(); - - // 2. Проходимся по всем C#-файлам в проекте - foreach (var document in project.Documents) - { - // Теперь у нас есть ГАРАНТИРОВАННАЯ семантическая модель для каждого файла - var semanticModel = await document.GetSemanticModelAsync(); - var syntaxTree = await document.GetSyntaxTreeAsync(); - var root = await syntaxTree.GetRootAsync(); - - var methods = root.DescendantNodes().OfType(); - - foreach (var method in methods) - { - var chunk = new CodeChunk - { - EntityName = method.Identifier.Text, - Content = method.ToFullString(), - FilePath = document.FilePath - }; - - var invocations = method.DescendantNodes().OfType(); - - foreach (var invocation in invocations) - { - // 3. Просим у модели 100% точную информацию о вызываемом методе - var symbolInfo = semanticModel.GetSymbolInfo(invocation); - - if (symbolInfo.Symbol is IMethodSymbol methodSymbol) - { - // Получаем пространство имен метода (например, "System.Linq" или "CodeBase.Services") - string namespaceName = methodSymbol.ContainingNamespace?.ToString() ?? ""; - - // Отсеиваем только системные вызовы, оставляя ВЕСЬ бизнес-код - if (!namespaceName.StartsWith("System") && !namespaceName.StartsWith("Microsoft")) - { - chunk.OutgoingCalls.Add(methodSymbol.Name); - Console.WriteLine($"[ПАРСЕР] Успех: {chunk.EntityName} -> {methodSymbol.Name} ({namespaceName})"); - } - } - } - - // 1. Ищем вообще ВСЕ слова (идентификаторы) внутри метода - var identifiers = method.DescendantNodes().OfType(); - - foreach (var identifier in identifiers) - { - // 2. Спрашиваем у семантической модели: "Что это за слово?" - var symbolInfo = semanticModel.GetSymbolInfo(identifier); - - // 3. Если это тип данных (класс, структура, интерфейс или ЕНУМ) - if (symbolInfo.Symbol is INamedTypeSymbol typeSymbol) - { - string namespaceName = typeSymbol.ContainingNamespace?.ToString() ?? ""; - - // Отсеиваем системные типы (string, int, List и т.д.) - if (!namespaceName.StartsWith("System") && !namespaceName.StartsWith("Microsoft")) - { - // Проверяем, что это именно то, что нам нужно - if (typeSymbol.TypeKind == TypeKind.Enum) - { - chunk.OutgoingCalls.Add(typeSymbol.Name); - Console.WriteLine($"[ПАРСЕР] Нашли использование енума -> {typeSymbol.Name}"); - } - else if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Interface) - { - chunk.OutgoingCalls.Add(typeSymbol.Name); - Console.WriteLine($"[ПАРСЕР] Нашли использование класса/интерфейса -> {typeSymbol.Name}"); - } - } - } - } - - // Убираем дубликаты - chunk.OutgoingCalls = chunk.OutgoingCalls.Distinct().ToList(); - chunksList.Add(chunk); - } - } - - return chunksList; - } - - - } -} diff --git a/CodeBase/Services/CodeService.cs b/CodeBase/Services/CodeService.cs index 51173f7..37b6939 100644 --- a/CodeBase/Services/CodeService.cs +++ b/CodeBase/Services/CodeService.cs @@ -3,27 +3,10 @@ using CodeBase.Repositories; namespace CodeBase.Services { - public class CodeService(ChunkService service, - ScipRepository repository, + public class CodeService(ScipRepository repository, VectorizationService vectorizationService, LlmService llmService) { - public async Task> GetCodeChunksAsync(string path, string name) - { - if (!Path.Exists(path)) - { - throw new Exception("Путь не найден"); - } - - var allChunks = await service.ParseProjectAsync(path); - - var chunks = await vectorizationService.EnrichChunksWithVectorsAsync(allChunks); - - //await repository.SaveChunksAsync(name, chunks); - - return chunks; - } - public async Task GetAnswerAsync(string name, string question) { var query = await vectorizationService.GetVectorAsync(question); @@ -45,29 +28,5 @@ namespace CodeBase.Services return results; } - - // Математика косинусного сходства - private float CalculateCosineSimilarity(float[] vectorA, float[] vectorB) - { - if (vectorA.Length != vectorB.Length) - throw new ArgumentException("Векторы должны быть одинаковой длины (например, 768)."); - - float dotProduct = 0; - float magnitudeA = 0; - float magnitudeB = 0; - - for (int i = 0; i < vectorA.Length; i++) - { - dotProduct += vectorA[i] * vectorB[i]; - magnitudeA += vectorA[i] * vectorA[i]; - magnitudeB += vectorB[i] * vectorB[i]; - } - - // Защита от деления на ноль - if (magnitudeA == 0 || magnitudeB == 0) - return 0; - - return (float)(dotProduct / (Math.Sqrt(magnitudeA) * Math.Sqrt(magnitudeB))); - } } } diff --git a/README.md b/README.md index 1372133..7889c8e 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,10 @@ Patch Notes: * Вся инфраструктура теперь в докере: база, питоновский скрипт для векторизации, llm * НУЖЕН РЕФАКТОРИНГ * НУЖЕН ПЕРЕСМОТР СКАЧИВАНИЯ LLM Внутри Ollama - долго - * Нужна проверка бага: парсинг сохраняется не сразу \ No newline at end of file + * Нужна проверка бага: парсинг сохраняется не сразу + * v2.1.1: + * Реализована попытка в мультипарсинг: шарп, с горем пополам питон, должен еще го и ts + * почищены лишние файлы, новая репа для работы с бд, отдельно вынесена иинициализация + * теперь ручка парсера принимает на вход путь к проекту, название и язык в виде enum + * расширение списка языков проиходит через enum и настройку appsetting посредством добавления строки вида язык:команда докера для сборки файла index через Scip + * установлен protobuf для работы с Scip \ No newline at end of file