Переехали на графовую модель, засунули все в докер, поменяли парсинг
This commit is contained in:
@@ -8,9 +8,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.6" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Locator" Version="1.11.2" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.6.0" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.6.0" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Framework" Version="17.11.48" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Neo4j.Driver" Version="6.3.0" />
|
||||||
<PackageReference Include="OpenAI" Version="2.12.0" />
|
<PackageReference Include="OpenAI" Version="2.12.0" />
|
||||||
<PackageReference Include="Qdrant.Client" Version="1.18.1" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,6 @@
|
|||||||
public string Documentation { get; set; }
|
public string Documentation { get; set; }
|
||||||
public string Content { get; set; }
|
public string Content { get; set; }
|
||||||
public float[] Vector { get; set; }
|
public float[] Vector { get; set; }
|
||||||
|
public List<string> OutgoingCalls { get; set; } = new List<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
CodeBase/Models/GraphNodeContext.cs
Normal file
17
CodeBase/Models/GraphNodeContext.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CodeBase.Models
|
||||||
|
{
|
||||||
|
public class GraphNodeContext
|
||||||
|
{
|
||||||
|
public string MethodName { get; set; }
|
||||||
|
public string FilePath { get; set; }
|
||||||
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
// Графовые связи, которые мы вытащим из Neo4j
|
||||||
|
|
||||||
|
// Кого вызывает этот метод (и какие классы/енумы использует)
|
||||||
|
public List<string> OutgoingDependencies { get; set; } = new();
|
||||||
|
|
||||||
|
// Кто вызывает этот метод (кто от него зависит)
|
||||||
|
public List<string> IncomingDependencies { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,28 @@
|
|||||||
using CodeBase.Services;
|
using CodeBase.Services;
|
||||||
using CodeBase.Warehouse;
|
using Microsoft.Build.Locator;
|
||||||
using Qdrant.Client;
|
using Neo4j.Driver;
|
||||||
using Qdrant.Client.Grpc;
|
|
||||||
|
if (!MSBuildLocator.IsRegistered)
|
||||||
|
{
|
||||||
|
MSBuildLocator.RegisterDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddSingleton(new QdrantClient("localhost", 6334));
|
builder.Services.AddSingleton<IDriver>(sp =>
|
||||||
|
GraphDatabase.Driver(
|
||||||
|
"bolt://localhost:7687",
|
||||||
|
AuthTokens.Basic("neo4j", "password123")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
builder.Services.AddSingleton<ChunkWarehouse>();
|
|
||||||
builder.Services.AddTransient<VectorizationService>();
|
builder.Services.AddTransient<VectorizationService>();
|
||||||
builder.Services.AddTransient<LlmService>();
|
builder.Services.AddTransient<LlmService>();
|
||||||
builder.Services.AddScoped<LlmPromptBuilder>();
|
builder.Services.AddScoped<LlmPromptBuilder>();
|
||||||
builder.Services.AddTransient<ChunkService>();
|
builder.Services.AddTransient<ChunkService>();
|
||||||
builder.Services.AddTransient<CodeService>();
|
builder.Services.AddTransient<CodeService>();
|
||||||
builder.Services.AddTransient<QdrantRepository>();
|
builder.Services.AddTransient<GraphRepository>();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
@@ -34,27 +42,12 @@ app.UseAuthorization();
|
|||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var qdrantClient = scope.ServiceProvider.GetRequiredService<QdrantClient>();
|
var graphRepo = scope.ServiceProvider.GetRequiredService<GraphRepository>();
|
||||||
|
Console.WriteLine("Проверяем и создаем векторный индекс в Neo4j...");
|
||||||
// Получаем список существующих коллекций
|
await graphRepo.InitializeDbAsync();
|
||||||
var collections = await qdrantClient.ListCollectionsAsync();
|
Console.WriteLine("Индекс готов!");
|
||||||
|
|
||||||
if (!collections.Contains("CodeBaseCollection"))
|
|
||||||
{
|
|
||||||
Console.WriteLine("Создаем новую коллекцию в Qdrant...");
|
|
||||||
await qdrantClient.CreateCollectionAsync(
|
|
||||||
collectionName: "CodeBaseCollection",
|
|
||||||
vectorsConfig: new VectorParams
|
|
||||||
{
|
|
||||||
Size = 768, // Размер вектора GraphCodeBERT
|
|
||||||
Distance = Distance.Cosine // Сразу задаем алгоритм поиска (косинусное сходство)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
139
CodeBase/Repositories/GraphRepository.cs
Normal file
139
CodeBase/Repositories/GraphRepository.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
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<CodeChunk> chunks)
|
||||||
|
{
|
||||||
|
await using var session = _driver.AsyncSession();
|
||||||
|
|
||||||
|
// 1. Делаем данные "безопасными" для базы
|
||||||
|
var parameters = chunks.Where(c => c.Vector != null).Select(chunk => new
|
||||||
|
{
|
||||||
|
id = Guid.NewGuid().ToString(),
|
||||||
|
projectName = projectName,
|
||||||
|
filePath = chunk.FilePath,
|
||||||
|
methodName = chunk.MethodName,
|
||||||
|
content = chunk.Content,
|
||||||
|
vector = chunk.Vector,
|
||||||
|
// Защита от null и пустых строк (база их не переварит в цикле FOREACH)
|
||||||
|
outgoingCalls = chunk.OutgoingCalls != null
|
||||||
|
? chunk.OutgoingCalls.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()
|
||||||
|
: new List<string>()
|
||||||
|
}).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<List<GraphNodeContext>> 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<GraphNodeContext>();
|
||||||
|
|
||||||
|
while (await cursor.FetchAsync())
|
||||||
|
{
|
||||||
|
var record = cursor.Current;
|
||||||
|
results.Add(new GraphNodeContext
|
||||||
|
{
|
||||||
|
MethodName = record["methodName"].As<string>(),
|
||||||
|
FilePath = record["filePath"].As<string>(),
|
||||||
|
Content = record["content"].As<string>(),
|
||||||
|
|
||||||
|
// Драйвер Neo4j возвращает массивы как IList<object>,
|
||||||
|
// поэтому аккуратно кастуем их в наши списки строк
|
||||||
|
OutgoingDependencies = record["outgoingDependencies"].As<IList<string>>().ToList(),
|
||||||
|
IncomingDependencies = record["incomingDependencies"].As<IList<string>>().ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using CodeBase.Models;
|
|
||||||
using Qdrant.Client;
|
|
||||||
using Qdrant.Client.Grpc;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
public class QdrantRepository
|
|
||||||
{
|
|
||||||
private readonly QdrantClient _client;
|
|
||||||
private const string CollectionName = "CodeBaseCollection";
|
|
||||||
|
|
||||||
public QdrantRepository(QdrantClient client)
|
|
||||||
{
|
|
||||||
_client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 1. СОХРАНЕНИЕ ВЕКТОРОВ С ПРИВЯЗКОЙ К ПРОЕКТУ
|
|
||||||
// ==========================================
|
|
||||||
public async Task SaveChunksAsync(string projectName, List<CodeChunk> chunks)
|
|
||||||
{
|
|
||||||
var points = new List<PointStruct>();
|
|
||||||
|
|
||||||
foreach (var chunk in chunks)
|
|
||||||
{
|
|
||||||
if (chunk.Vector == null) continue;
|
|
||||||
|
|
||||||
// Формируем точку для БД
|
|
||||||
var point = new PointStruct
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(), // Уникальный ID чанка
|
|
||||||
Vectors = chunk.Vector, // Тот самый массив float[]
|
|
||||||
|
|
||||||
// Вот здесь мы навешиваем все ярлыки!
|
|
||||||
Payload =
|
|
||||||
{
|
|
||||||
["ProjectName"] = projectName,
|
|
||||||
["FilePath"] = chunk.FilePath,
|
|
||||||
["MethodName"] = chunk.MethodName,
|
|
||||||
["Content"] = chunk.Content // Сам текст кода тоже храним прямо в БД
|
|
||||||
}
|
|
||||||
};
|
|
||||||
points.Add(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем пачку векторов в базу
|
|
||||||
await _client.UpsertAsync(CollectionName, points);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 2. ПОИСК ТОЛЬКО В РАМКАХ ОДНОГО ПРОЕКТА
|
|
||||||
// ==========================================
|
|
||||||
public async Task<List<CodeChunk>> SearchAsync(string projectName, float[] queryVector, int topK = 15)
|
|
||||||
{
|
|
||||||
// Создаем фильтр: искать только там, где ProjectName совпадает с запрошенным
|
|
||||||
var projectFilter = new Filter
|
|
||||||
{
|
|
||||||
Must = {
|
|
||||||
new Condition
|
|
||||||
{
|
|
||||||
Field = new FieldCondition
|
|
||||||
{
|
|
||||||
Key = "ProjectName",
|
|
||||||
Match = new Match { Keyword = projectName }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// База делает всю тяжелую математику за нас!
|
|
||||||
var searchResults = await _client.SearchAsync(
|
|
||||||
collectionName: CollectionName,
|
|
||||||
vector: queryVector,
|
|
||||||
limit: (ulong)topK,
|
|
||||||
filter: projectFilter, // Передаем наш фильтр
|
|
||||||
payloadSelector: true // Просим БД вернуть Payload вместе с результатами
|
|
||||||
);
|
|
||||||
|
|
||||||
// Превращаем ответы базы обратно в наши C#-объекты CodeChunk
|
|
||||||
return searchResults.Select(hit => new CodeChunk
|
|
||||||
{
|
|
||||||
FilePath = hit.Payload["FilePath"].StringValue,
|
|
||||||
MethodName = hit.Payload["MethodName"].StringValue,
|
|
||||||
Content = hit.Payload["Content"].StringValue
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using CodeBase.Models;
|
using CodeBase.Models;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.MSBuild;
|
||||||
|
|
||||||
namespace CodeBase.Services
|
namespace CodeBase.Services
|
||||||
{
|
{
|
||||||
@@ -12,16 +14,51 @@ namespace CodeBase.Services
|
|||||||
var syntaxTree = CSharpSyntaxTree.ParseText(fileContent);
|
var syntaxTree = CSharpSyntaxTree.ParseText(fileContent);
|
||||||
var root = syntaxTree.GetRoot();
|
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. Собираем методы (как и раньше)
|
// 1. Собираем методы (как и раньше)
|
||||||
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
|
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
|
||||||
foreach (var method in methods)
|
foreach (var method in methods)
|
||||||
{
|
{
|
||||||
chunks.Add(new CodeChunk
|
var chunk = new CodeChunk
|
||||||
{
|
{
|
||||||
FilePath = filePath,
|
FilePath = filePath,
|
||||||
MethodName = method.Identifier.Text,
|
MethodName = method.Identifier.Text,
|
||||||
Content = method.ToFullString().Trim()
|
Content = method.ToFullString().Trim()
|
||||||
});
|
};
|
||||||
|
|
||||||
|
var invocations = method.DescendantNodes().OfType<InvocationExpressionSyntax>();
|
||||||
|
|
||||||
|
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 (Перечислений)
|
// 2. ДОБАВЛЯЕМ СБОР ENUM (Перечислений)
|
||||||
@@ -58,5 +95,94 @@ namespace CodeBase.Services
|
|||||||
|
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<CodeChunk>> ParseProjectAsync(string csprojPath)
|
||||||
|
{
|
||||||
|
// 1. Создаем воркспейс и загружаем проект (это может занять пару секунд)
|
||||||
|
using var workspace = MSBuildWorkspace.Create();
|
||||||
|
Console.WriteLine("Загружаем проект и строим семантическую модель...");
|
||||||
|
var project = await workspace.OpenProjectAsync(csprojPath);
|
||||||
|
|
||||||
|
var chunksList = new List<CodeChunk>();
|
||||||
|
|
||||||
|
// 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<MethodDeclarationSyntax>();
|
||||||
|
|
||||||
|
foreach (var method in methods)
|
||||||
|
{
|
||||||
|
var chunk = new CodeChunk
|
||||||
|
{
|
||||||
|
MethodName = method.Identifier.Text,
|
||||||
|
Content = method.ToFullString(),
|
||||||
|
FilePath = document.FilePath
|
||||||
|
};
|
||||||
|
|
||||||
|
var invocations = method.DescendantNodes().OfType<InvocationExpressionSyntax>();
|
||||||
|
|
||||||
|
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.MethodName} -> {methodSymbol.Name} ({namespaceName})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Ищем вообще ВСЕ слова (идентификаторы) внутри метода
|
||||||
|
var identifiers = method.DescendantNodes().OfType<IdentifierNameSyntax>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using CodeBase.Models;
|
using CodeBase.Models;
|
||||||
using CodeBase.Warehouse;
|
|
||||||
|
|
||||||
namespace CodeBase.Services
|
namespace CodeBase.Services
|
||||||
{
|
{
|
||||||
public class CodeService(ChunkService service,
|
public class CodeService(ChunkService service,
|
||||||
QdrantRepository repository,
|
GraphRepository repository,
|
||||||
VectorizationService vectorizationService,
|
VectorizationService vectorizationService,
|
||||||
LlmService llmService)
|
LlmService llmService)
|
||||||
{
|
{
|
||||||
@@ -15,20 +14,7 @@ namespace CodeBase.Services
|
|||||||
throw new Exception("Путь не найден");
|
throw new Exception("Путь не найден");
|
||||||
}
|
}
|
||||||
|
|
||||||
var allChunks = new List<CodeChunk>();
|
var allChunks = await service.ParseProjectAsync(path);
|
||||||
|
|
||||||
// Находим все .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);
|
var chunks = await vectorizationService.EnrichChunksWithVectorsAsync(allChunks);
|
||||||
|
|
||||||
@@ -49,7 +35,7 @@ namespace CodeBase.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Главный метод поиска
|
// Главный метод поиска
|
||||||
public async Task<List<CodeChunk>> SearchAsync(
|
public async Task<List<GraphNodeContext>> SearchAsync(
|
||||||
float[] queryVector,
|
float[] queryVector,
|
||||||
string name,
|
string name,
|
||||||
int topK = 3) // Возвращаем топ-3 результата
|
int topK = 3) // Возвращаем топ-3 результата
|
||||||
|
|||||||
@@ -4,38 +4,47 @@ using System.Text;
|
|||||||
|
|
||||||
public class LlmPromptBuilder
|
public class LlmPromptBuilder
|
||||||
{
|
{
|
||||||
public string BuildPrompt(string userQuestion, List<CodeChunk> foundChunks)
|
public string BuildPrompt(string userQuestion, List<GraphNodeContext> graphContexts)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var promptBuilder = new StringBuilder();
|
||||||
|
|
||||||
// 1. Задаем жесткую роль и ограничения
|
promptBuilder.AppendLine("Ты — опытный C#-архитектор. Твоя задача — ответить на вопрос пользователя, опираясь ИСКЛЮЧИТЕЛЬНО на предоставленный граф вызовов и зависимостей кода. Не придумывай методы или классы, которых нет в контексте.");
|
||||||
sb.AppendLine("Ты — опытный разработчик и архитектор. Твоя задача — ответить на вопрос пользователя.");
|
promptBuilder.AppendLine("\nКонтекст из кодовой базы (Граф зависимостей):");
|
||||||
sb.AppendLine("ОТВЕЧАЙ СТРОГО НА ОСНОВЕ ПРЕДОСТАВЛЕННОГО КОДА НИЖЕ.");
|
|
||||||
sb.AppendLine("Если в коде нет ответа на вопрос, честно скажи: «В данном фрагменте кода нет этой информации». Не придумывай функции, которых нет в тексте.");
|
|
||||||
|
|
||||||
sb.AppendLine("\n================ ПРЕДОСТАВЛЕННЫЙ КОД ================");
|
foreach (var node in graphContexts)
|
||||||
|
|
||||||
// 2. Вклеиваем найденные чанки с контекстом
|
|
||||||
foreach (var chunk in foundChunks)
|
|
||||||
{
|
{
|
||||||
sb.AppendLine($"Файл: {chunk.FilePath}");
|
promptBuilder.AppendLine("--------------------------------------------------");
|
||||||
sb.AppendLine($"Класс: {chunk.ClassName}");
|
promptBuilder.AppendLine($"[ГЛАВНЫЙ УЗЕЛ]");
|
||||||
sb.AppendLine($"Метод: {chunk.MethodName}");
|
promptBuilder.AppendLine($"Имя: {node.MethodName}");
|
||||||
if (!string.IsNullOrEmpty(chunk.Documentation))
|
promptBuilder.AppendLine($"Файл: {node.FilePath}");
|
||||||
|
|
||||||
|
// Добавляем зависимости ВНИЗ (что использует метод)
|
||||||
|
if (node.OutgoingDependencies.Any())
|
||||||
{
|
{
|
||||||
sb.AppendLine($"Документация: {chunk.Documentation}");
|
promptBuilder.AppendLine("\n[ИСПОЛЬЗУЕТ ВНУТРИ СЕБЯ]:");
|
||||||
|
foreach (var dep in node.OutgoingDependencies.Distinct())
|
||||||
|
{
|
||||||
|
promptBuilder.AppendLine($"- {dep}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sb.AppendLine("Код:");
|
|
||||||
sb.AppendLine("```csharp");
|
// Добавляем зависимости ВВЕРХ (кто зависит от метода)
|
||||||
sb.AppendLine(chunk.Content);
|
if (node.IncomingDependencies.Any())
|
||||||
sb.AppendLine("```");
|
{
|
||||||
sb.AppendLine("--------------------------------------------------");
|
promptBuilder.AppendLine("\n[ВЫЗЫВАЕТСЯ ИЗ]:");
|
||||||
|
foreach (var caller in node.IncomingDependencies.Distinct())
|
||||||
|
{
|
||||||
|
promptBuilder.AppendLine($"- {caller}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promptBuilder.AppendLine("\nКод:");
|
||||||
|
promptBuilder.AppendLine(node.Content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Добавляем сам вопрос
|
promptBuilder.AppendLine("--------------------------------------------------");
|
||||||
sb.AppendLine("\n================ ВОПРОС ПОЛЬЗОВАТЕЛЯ ================");
|
promptBuilder.AppendLine($"Вопрос пользователя: {userQuestion}");
|
||||||
sb.AppendLine(userQuestion);
|
|
||||||
|
|
||||||
return sb.ToString();
|
return promptBuilder.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,11 +30,11 @@ public class LlmService
|
|||||||
_builder = llmPromptBuilder;
|
_builder = llmPromptBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<String> AskQuestionAsync(string userQuestion, List<CodeChunk> foundChunks)
|
public async Task<String> AskQuestionAsync(string userQuestion, List<GraphNodeContext> grafContext)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var finalPrompt = _builder.BuildPrompt(userQuestion, foundChunks);
|
var finalPrompt = _builder.BuildPrompt(userQuestion, grafContext);
|
||||||
// Формируем сообщение. Поскольку мы уже зашили роль и инструкции
|
// Формируем сообщение. Поскольку мы уже зашили роль и инструкции
|
||||||
// внутрь finalPrompt с помощью LlmPromptBuilder,
|
// внутрь finalPrompt с помощью LlmPromptBuilder,
|
||||||
// передаем все это как UserMessage.
|
// передаем все это как UserMessage.
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
using CodeBase.Models;
|
|
||||||
|
|
||||||
namespace CodeBase.Warehouse
|
|
||||||
{
|
|
||||||
public class ChunkWarehouse
|
|
||||||
{
|
|
||||||
public Dictionary<string, List<CodeChunk>> dictionary;
|
|
||||||
|
|
||||||
public ChunkWarehouse()
|
|
||||||
{
|
|
||||||
this.dictionary = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpsertDictionary(string name, List<CodeChunk> chunks)
|
|
||||||
{
|
|
||||||
if (dictionary.ContainsKey(name))
|
|
||||||
{
|
|
||||||
dictionary[name] = chunks;
|
|
||||||
}
|
|
||||||
else dictionary.TryAdd(name, chunks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteElement(string name)
|
|
||||||
{
|
|
||||||
dictionary.Remove(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<CodeChunk> GetChunks(string name)
|
|
||||||
{
|
|
||||||
if(dictionary.TryGetValue(name, out var chunks))
|
|
||||||
{
|
|
||||||
return chunks;
|
|
||||||
}
|
|
||||||
throw new Exception("Такого проекта нет");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearDictionary()
|
|
||||||
{
|
|
||||||
dictionary.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
PythonProject/Dockerfile
Normal file
13
PythonProject/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копирование кода приложения
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Команда запуска (замени main:app или script.py на свой стартовый файл)
|
||||||
|
CMD ["python", "main.py"]
|
||||||
17
README.md
17
README.md
@@ -1,5 +1,18 @@
|
|||||||
|
|
||||||
# CodeBase
|
# CodeBase
|
||||||
Прототип базы знаний по коду проектов. Парсит проект на чанки согласно синтаксическому дереву, векторизует через GrafCodeBert, потом может ответить на вопрос по коду, сравнивая вектора вопроса и чанков, ответ формируется с помощью LLM.
|
|
||||||
|
Прототип базы знаний по коду проектов. На вход получает полный адрес .cproj файла, парсит код для составления графа с векторным представлением каждого метода. Способен ответить на вопрос по проекту, ища векторное совпадение по косинусному сходству и по взаимосвязям в графе
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Patch Notes:
|
Patch Notes:
|
||||||
v1: реализовано MVP, работаем только с шарпом, храним все в оперативе. В качестве LLM - qwen2.5-coder:7
|
|
||||||
|
* v1: реализовано MVP, работаем только с шарпом, храним все в оперативе. В качестве LLM - qwen2.5-coder:7
|
||||||
|
* v2:
|
||||||
|
* Переезд на графовую базу данных Neo4j с автоматической инициализацией.
|
||||||
|
* С помощью Microsoft.CodeAnalysis.MSBuild парсит все зависимости, исключая попадание системных вызовов в дерево вызовов кода.
|
||||||
|
* Изменен промт: теперь анализируем не просто чанки, а дерево зависимостей
|
||||||
|
* Вся инфраструктура теперь в докере: база, питоновский скрипт для векторизации, llm
|
||||||
|
* НУЖЕН РЕФАКТОРИНГ
|
||||||
|
* НУЖЕН ПЕРЕСМОТР СКАЧИВАНИЯ LLM Внутри Ollama - долго
|
||||||
|
* Нужна проверка бага: парсинг сохраняется не сразу
|
||||||
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
neo4j:
|
||||||
|
image: neo4j:5.15.0
|
||||||
|
container_name: graphrag_neo4j
|
||||||
|
ports:
|
||||||
|
- "7474:7474"
|
||||||
|
- "7687:7687"
|
||||||
|
environment:
|
||||||
|
- NEO4J_AUTH=neo4j/password123
|
||||||
|
|
||||||
|
python-service:
|
||||||
|
build: ./PythonProject
|
||||||
|
container_name: graphrag_python
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
|
||||||
|
ollama:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: graphrag_ollama
|
||||||
|
ports:
|
||||||
|
- "11434:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama_data:/root/.ollama
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
|
||||||
|
ollama-init:
|
||||||
|
image: ollama/ollama:latest
|
||||||
|
container_name: graphrag_ollama_init
|
||||||
|
depends_on:
|
||||||
|
- ollama # Ждем, пока запустится основной сервер
|
||||||
|
environment:
|
||||||
|
# Указываем этому контейнеру стучаться в основной сервер
|
||||||
|
- OLLAMA_HOST=ollama:11434
|
||||||
|
# Команда на скачивание (замени qwen2.5-coder на нужную, если передумаешь)
|
||||||
|
command: pull qwen2.5-coder:7b
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ollama_data:
|
||||||
Reference in New Issue
Block a user