From 17a64809cbeab69ec04ebaaf5df59cd72395fde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BE=D0=B7=D0=BE=D1=80=D0=BE=D0=B2=D0=B0=20=D0=90?= =?UTF-8?q?=D0=BB=D1=91=D0=BD=D0=B0?= Date: Wed, 22 Jul 2026 20:47:51 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=D1=82?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeBase/CodeBase.csproj | 16 +++ CodeBase/CodeBase.http | 6 ++ CodeBase/CodeBase.slnx | 3 + CodeBase/Controllers/CodeController.cs | 26 +++++ CodeBase/Models/CodeChunk.cs | 12 +++ CodeBase/Program.cs | 33 ++++++ CodeBase/Properties/launchSettings.json | 23 ++++ CodeBase/Services/ChunkService.cs | 62 +++++++++++ CodeBase/Services/CodeService.cs | 102 ++++++++++++++++++ CodeBase/Services/LlmPromptBuilder.cs | 41 +++++++ CodeBase/Services/LlmService.cs | 62 +++++++++++ CodeBase/Services/VectorizationService.cs | 92 ++++++++++++++++ CodeBase/Warehouse/ChunkWarehouse.cs | 42 ++++++++ CodeBase/appsettings.Development.json | 8 ++ CodeBase/appsettings.json | 9 ++ PythonProject/.idea/.gitignore | 3 + PythonProject/.idea/PythonProject.iml | 10 ++ .../inspectionProfiles/profiles_settings.xml | 6 ++ PythonProject/.idea/misc.xml | 7 ++ PythonProject/.idea/modules.xml | 8 ++ PythonProject/main.py | 30 ++++++ PythonProject/requirements.txt | 5 + 22 files changed, 606 insertions(+) create mode 100644 CodeBase/CodeBase.csproj create mode 100644 CodeBase/CodeBase.http create mode 100644 CodeBase/CodeBase.slnx create mode 100644 CodeBase/Controllers/CodeController.cs create mode 100644 CodeBase/Models/CodeChunk.cs create mode 100644 CodeBase/Program.cs create mode 100644 CodeBase/Properties/launchSettings.json create mode 100644 CodeBase/Services/ChunkService.cs create mode 100644 CodeBase/Services/CodeService.cs create mode 100644 CodeBase/Services/LlmPromptBuilder.cs create mode 100644 CodeBase/Services/LlmService.cs create mode 100644 CodeBase/Services/VectorizationService.cs create mode 100644 CodeBase/Warehouse/ChunkWarehouse.cs create mode 100644 CodeBase/appsettings.Development.json create mode 100644 CodeBase/appsettings.json create mode 100644 PythonProject/.idea/.gitignore create mode 100644 PythonProject/.idea/PythonProject.iml create mode 100644 PythonProject/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 PythonProject/.idea/misc.xml create mode 100644 PythonProject/.idea/modules.xml create mode 100644 PythonProject/main.py create mode 100644 PythonProject/requirements.txt diff --git a/CodeBase/CodeBase.csproj b/CodeBase/CodeBase.csproj new file mode 100644 index 0000000..8718518 --- /dev/null +++ b/CodeBase/CodeBase.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/CodeBase/CodeBase.http b/CodeBase/CodeBase.http new file mode 100644 index 0000000..0feaa0d --- /dev/null +++ b/CodeBase/CodeBase.http @@ -0,0 +1,6 @@ +@CodeBase_HostAddress = http://localhost:5236 + +GET {{CodeBase_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/CodeBase/CodeBase.slnx b/CodeBase/CodeBase.slnx new file mode 100644 index 0000000..540a350 --- /dev/null +++ b/CodeBase/CodeBase.slnx @@ -0,0 +1,3 @@ + + + diff --git a/CodeBase/Controllers/CodeController.cs b/CodeBase/Controllers/CodeController.cs new file mode 100644 index 0000000..73b3e71 --- /dev/null +++ b/CodeBase/Controllers/CodeController.cs @@ -0,0 +1,26 @@ +using CodeBase.Models; +using CodeBase.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace CodeBase.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class CodeController(CodeService service) : 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) + { + var answer = await service.GetAnswerAsync(name, question); + return answer; + } + } +} diff --git a/CodeBase/Models/CodeChunk.cs b/CodeBase/Models/CodeChunk.cs new file mode 100644 index 0000000..3c65039 --- /dev/null +++ b/CodeBase/Models/CodeChunk.cs @@ -0,0 +1,12 @@ +namespace CodeBase.Models +{ + public class CodeChunk + { + public string FilePath { get; set; } + public string ClassName { get; set; } + public string MethodName { get; set; } + public string Documentation { get; set; } + public string Content { get; set; } + public float[] Vector { get; set; } + } +} diff --git a/CodeBase/Program.cs b/CodeBase/Program.cs new file mode 100644 index 0000000..cb2d15a --- /dev/null +++ b/CodeBase/Program.cs @@ -0,0 +1,33 @@ +using CodeBase.Services; +using CodeBase.Warehouse; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddSingleton(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddScoped(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +builder.Services.AddControllers(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/CodeBase/Properties/launchSettings.json b/CodeBase/Properties/launchSettings.json new file mode 100644 index 0000000..f0052b3 --- /dev/null +++ b/CodeBase/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5236", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7218;http://localhost:5236", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CodeBase/Services/ChunkService.cs b/CodeBase/Services/ChunkService.cs new file mode 100644 index 0000000..896c8d2 --- /dev/null +++ b/CodeBase/Services/ChunkService.cs @@ -0,0 +1,62 @@ +using CodeBase.Models; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +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(); + + // 1. Собираем методы (как и раньше) + var methods = root.DescendantNodes().OfType(); + foreach (var method in methods) + { + chunks.Add(new CodeChunk + { + FilePath = filePath, + MethodName = method.Identifier.Text, + Content = method.ToFullString().Trim() + }); + } + + // 2. ДОБАВЛЯЕМ СБОР ENUM (Перечислений) + var enums = root.DescendantNodes().OfType(); + foreach (var enumSyntax in enums) + { + chunks.Add(new CodeChunk + { + FilePath = filePath, + MethodName = 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, + MethodName = classSyntax.Identifier.Text, + // Сохраняем объявление класса и его свойства + Content = $"class {classSyntax.Identifier.Text} {{\n" + + string.Join("\n", properties.Select(p => p.ToFullString().Trim())) + + "\n}" + }); + } + } + + return chunks; + } + } +} diff --git a/CodeBase/Services/CodeService.cs b/CodeBase/Services/CodeService.cs new file mode 100644 index 0000000..c381a59 --- /dev/null +++ b/CodeBase/Services/CodeService.cs @@ -0,0 +1,102 @@ +using CodeBase.Models; +using CodeBase.Warehouse; + +namespace CodeBase.Services +{ + public class CodeService(ChunkService service, + ChunkWarehouse warehouse, + VectorizationService vectorizationService, + LlmService llmService) + { + public async Task> GetCodeChunksAsync(string path, string name) + { + if (!Path.Exists(path)) + { + throw new Exception("Путь не найден"); + } + + var allChunks = new List(); + + // Находим все .cs файлы во всех вложенных папках + var csFiles = Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories); + + foreach (var filePath in csFiles) + { + var content = File.ReadAllText(filePath); + + // Парсим каждый файл отдельно, сохраняя его оригинальный путь + var fileChunks = service.ChunkCSharpFile(filePath, content); + + allChunks.AddRange(fileChunks); + } + + var chunks = await vectorizationService.EnrichChunksWithVectorsAsync(allChunks); + + warehouse.UpsertDictionary(name, allChunks); + + return chunks; + } + + public async Task GetAnswerAsync(string name, string question) + { + var query = await vectorizationService.GetVectorAsync(question); + + var answerVectors = Search(query, name, 15); + + var answer = await llmService.AskQuestionAsync(question, answerVectors.Select(s => s.Chunk).ToList()); + + return answer; + } + + // Главный метод поиска + public List<(CodeChunk Chunk, float Score)> Search( + float[] queryVector, + string name, + int topK = 3) // Возвращаем топ-3 результата + { + var memoryBase = warehouse.GetChunks(name); + + var results = new List<(CodeChunk, float)>(); + + foreach (var chunk in memoryBase) + { + if (chunk.Vector == null || chunk.Vector.Length == 0) + continue; + + // Считаем близость вектора вопроса к вектору кода + float similarity = CalculateCosineSimilarity(queryVector, chunk.Vector); + results.Add((chunk, similarity)); + } + + // Сортируем по убыванию сходства (чем ближе к 1, тем лучше) + return results + .OrderByDescending(x => x.Item2) + .Take(topK) + .ToList(); + } + + // Математика косинусного сходства + 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/CodeBase/Services/LlmPromptBuilder.cs b/CodeBase/Services/LlmPromptBuilder.cs new file mode 100644 index 0000000..2321352 --- /dev/null +++ b/CodeBase/Services/LlmPromptBuilder.cs @@ -0,0 +1,41 @@ +using CodeBase.Models; +using System.Collections.Generic; +using System.Text; + +public class LlmPromptBuilder +{ + public string BuildPrompt(string userQuestion, List foundChunks) + { + var sb = new StringBuilder(); + + // 1. Задаем жесткую роль и ограничения + sb.AppendLine("Ты — опытный разработчик и архитектор. Твоя задача — ответить на вопрос пользователя."); + sb.AppendLine("ОТВЕЧАЙ СТРОГО НА ОСНОВЕ ПРЕДОСТАВЛЕННОГО КОДА НИЖЕ."); + sb.AppendLine("Если в коде нет ответа на вопрос, честно скажи: «В данном фрагменте кода нет этой информации». Не придумывай функции, которых нет в тексте."); + + sb.AppendLine("\n================ ПРЕДОСТАВЛЕННЫЙ КОД ================"); + + // 2. Вклеиваем найденные чанки с контекстом + foreach (var chunk in foundChunks) + { + sb.AppendLine($"Файл: {chunk.FilePath}"); + sb.AppendLine($"Класс: {chunk.ClassName}"); + sb.AppendLine($"Метод: {chunk.MethodName}"); + if (!string.IsNullOrEmpty(chunk.Documentation)) + { + sb.AppendLine($"Документация: {chunk.Documentation}"); + } + sb.AppendLine("Код:"); + sb.AppendLine("```csharp"); + sb.AppendLine(chunk.Content); + sb.AppendLine("```"); + sb.AppendLine("--------------------------------------------------"); + } + + // 3. Добавляем сам вопрос + sb.AppendLine("\n================ ВОПРОС ПОЛЬЗОВАТЕЛЯ ================"); + sb.AppendLine(userQuestion); + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/CodeBase/Services/LlmService.cs b/CodeBase/Services/LlmService.cs new file mode 100644 index 0000000..ec3e797 --- /dev/null +++ b/CodeBase/Services/LlmService.cs @@ -0,0 +1,62 @@ +using CodeBase.Models; +using OpenAI; +using OpenAI.Chat; +using System; +using System.ClientModel; +using System.Threading.Tasks; + +public class LlmService +{ + private readonly ChatClient _chatClient; + private readonly LlmPromptBuilder _builder; + + public LlmService(LlmPromptBuilder llmPromptBuilder) + { + // 1. Указываем адрес Ollama (обязательно с /v1 на конце для совместимости с OpenAI) + var options = new OpenAIClientOptions + { + Endpoint = new Uri("http://localhost:11434/v1") + }; + + // 2. Ключ не нужен, но пакет просит хоть какую-то строку + var credential = new ApiKeyCredential("ollama"); + + // 3. ВАЖНО: Имя модели должно ТОЧНО совпадать с тем, что ты скачала в Ollama. + // Например, "llama3", "llama3.1", "qwen2.5-coder" или "phi3". + string modelName = "qwen2.5-coder:7b"; + + _chatClient = new ChatClient(modelName, credential, options); + + _builder = llmPromptBuilder; + } + + public async Task AskQuestionAsync(string userQuestion, List foundChunks) + { + try + { + var finalPrompt = _builder.BuildPrompt(userQuestion, foundChunks); + // Формируем сообщение. Поскольку мы уже зашили роль и инструкции + // внутрь finalPrompt с помощью LlmPromptBuilder, + // передаем все это как UserMessage. + var messages = new ChatMessage[] + { + new UserChatMessage(finalPrompt) + }; + + // Отправляем запрос (настройки опциональны) + var completionOptions = new ChatCompletionOptions + { + Temperature = 0.2f, // Делаем ответы менее креативными и более точными + }; + + ChatCompletion completion = await _chatClient.CompleteChatAsync(messages, completionOptions); + + // Возвращаем сгенерированный текст + return completion.Content[0].Text; + } + catch (Exception ex) + { + return $"[-] Ошибка при обращении к LLM: {ex.Message}"; + } + } +} \ No newline at end of file diff --git a/CodeBase/Services/VectorizationService.cs b/CodeBase/Services/VectorizationService.cs new file mode 100644 index 0000000..3bb40a2 --- /dev/null +++ b/CodeBase/Services/VectorizationService.cs @@ -0,0 +1,92 @@ +using CodeBase.Models; + +namespace CodeBase.Services +{ + public class VectorizeRequest + { + public string text { get; set; } + } + + public class VectorizeResponse + { + public float[] vector { get; set; } + } + + public class VectorizationService + { + private readonly HttpClient _httpClient; + + public VectorizationService() + { + _httpClient = new HttpClient(); + // Адрес нашего локального Python-сервиса + _httpClient.BaseAddress = new Uri("http://localhost:8000/"); + } + + + public async Task GetVectorAsync(string text) + { + var requestBody = new VectorizeRequest { text = text }; + + try + { + // Стучимся на наш Python-сервер (порт 8000) + var response = await _httpClient.PostAsJsonAsync("vectorize", requestBody); + + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync(); + return result?.vector; + } + + Console.WriteLine($"[-] Ошибка API при векторизации вопроса: {response.StatusCode}"); + return null; + } + catch (Exception ex) + { + Console.WriteLine($"[-] Ошибка подключения к Python-сервису: {ex.Message}"); + return null; + } + } + + public async Task> EnrichChunksWithVectorsAsync(List chunks) + { + foreach (var chunk in chunks) + { + // 1. Склеиваем контекст + // Мы даем нейросети подсказку о том, где именно лежит этот код + string contextText = $"File: {chunk.FilePath}\nClass: {chunk.ClassName}\nMethod: {chunk.MethodName}\nCode:\n{chunk.Content}"; + + var requestBody = new VectorizeRequest { text = contextText }; + + try + { + // 2. Стучимся в Python + var response = await _httpClient.PostAsJsonAsync("vectorize", requestBody); + + if (response.IsSuccessStatusCode) + { + // 3. Достаем массив чисел (обычно 768 элементов для GraphCodeBERT) + var result = await response.Content.ReadFromJsonAsync(); + if (result != null && result.vector != null) + { + // 4. Сохраняем вектор прямо в наш объект в памяти + chunk.Vector = result.vector; + Console.WriteLine($"[+] Векторизован метод: {chunk.MethodName}"); + } + } + else + { + Console.WriteLine($"[-] Ошибка API для {chunk.MethodName}: {response.StatusCode}"); + } + + } + catch (Exception ex) + { + Console.WriteLine($"[-] Ошибка подключения к Python-сервису: {ex.Message}"); + } + } + return chunks; + } + } +} diff --git a/CodeBase/Warehouse/ChunkWarehouse.cs b/CodeBase/Warehouse/ChunkWarehouse.cs new file mode 100644 index 0000000..9d1e150 --- /dev/null +++ b/CodeBase/Warehouse/ChunkWarehouse.cs @@ -0,0 +1,42 @@ +using CodeBase.Models; + +namespace CodeBase.Warehouse +{ + public class ChunkWarehouse + { + public Dictionary> dictionary; + + public ChunkWarehouse() + { + this.dictionary = new(); + } + + public void UpsertDictionary(string name, List chunks) + { + if (dictionary.ContainsKey(name)) + { + dictionary[name] = chunks; + } + else dictionary.TryAdd(name, chunks); + } + + public void DeleteElement(string name) + { + dictionary.Remove(name); + } + + public List GetChunks(string name) + { + if(dictionary.TryGetValue(name, out var chunks)) + { + return chunks; + } + throw new Exception("Такого проекта нет"); + } + + public void ClearDictionary() + { + dictionary.Clear(); + } + } +} diff --git a/CodeBase/appsettings.Development.json b/CodeBase/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/CodeBase/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CodeBase/appsettings.json b/CodeBase/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/CodeBase/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/PythonProject/.idea/.gitignore b/PythonProject/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/PythonProject/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/PythonProject/.idea/PythonProject.iml b/PythonProject/.idea/PythonProject.iml new file mode 100644 index 0000000..74d515a --- /dev/null +++ b/PythonProject/.idea/PythonProject.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/PythonProject/.idea/inspectionProfiles/profiles_settings.xml b/PythonProject/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/PythonProject/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/PythonProject/.idea/misc.xml b/PythonProject/.idea/misc.xml new file mode 100644 index 0000000..ad62bdc --- /dev/null +++ b/PythonProject/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/PythonProject/.idea/modules.xml b/PythonProject/.idea/modules.xml new file mode 100644 index 0000000..5f43230 --- /dev/null +++ b/PythonProject/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/PythonProject/main.py b/PythonProject/main.py new file mode 100644 index 0000000..090ab4f --- /dev/null +++ b/PythonProject/main.py @@ -0,0 +1,30 @@ +from fastapi import FastAPI +from pydantic import BaseModel +from transformers import AutoTokenizer, AutoModel +import torch +import uvicorn # <-- Добавили импорт + +app = FastAPI(title="GraphCodeBERT Vectorizer") + +# Загружаем модель глобально при старте приложения +model_name = "microsoft/graphcodebert-base" +tokenizer = AutoTokenizer.from_pretrained(model_name) +model = AutoModel.from_pretrained(model_name) + +class ChunkRequest(BaseModel): + text: str + +@app.post("/vectorize") +def vectorize(request: ChunkRequest): + inputs = tokenizer(request.text, return_tensors="pt", truncation=True, max_length=512) + + with torch.no_grad(): + outputs = model(**inputs) + + vector = outputs.last_hidden_state[:, 0, :].squeeze().tolist() + return {"vector": vector} + +# <-- Добавили блок запуска +if __name__ == "__main__": + print("Запускаем сервер на порту 8000...") + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/PythonProject/requirements.txt b/PythonProject/requirements.txt new file mode 100644 index 0000000..4df094f --- /dev/null +++ b/PythonProject/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +transformers +torch +pydantic \ No newline at end of file