From 4f78606b2c0a406e9d29b53927497a8b5a19e589 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Tue, 14 Apr 2026 16:05:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=20Qdrant=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D0=B2=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LazyBear.MCP/Program.cs | 3 + .../Services/Qdrant/QdrantClientProvider.cs | 29 ++ .../Services/Qdrant/QdrantKnowledgeTools.cs | 333 ++++++++++++++++++ .../Services/Qdrant/QdrantToolModule.cs | 18 + LazyBear.MCP/appsettings.json | 5 + memory-bank/activeContext.md | 6 + memory-bank/productContext.md | 19 +- memory-bank/progress.md | 11 +- memory-bank/systemPatterns.md | 44 ++- memory-bank/techContext.md | 12 + 10 files changed, 471 insertions(+), 9 deletions(-) create mode 100644 LazyBear.MCP/Services/Qdrant/QdrantClientProvider.cs create mode 100644 LazyBear.MCP/Services/Qdrant/QdrantKnowledgeTools.cs create mode 100644 LazyBear.MCP/Services/Qdrant/QdrantToolModule.cs diff --git a/LazyBear.MCP/Program.cs b/LazyBear.MCP/Program.cs index acc8d01..e153e04 100644 --- a/LazyBear.MCP/Program.cs +++ b/LazyBear.MCP/Program.cs @@ -4,6 +4,7 @@ using LazyBear.MCP.Services.Jira; using LazyBear.MCP.Services.Kubernetes; using LazyBear.MCP.Services.Logging; using LazyBear.MCP.Services.Mcp; +using LazyBear.MCP.Services.Qdrant; using LazyBear.MCP.Services.ToolRegistry; using LazyBear.MCP.TUI; using LazyBear.MCP.TUI.Components; @@ -27,12 +28,14 @@ var host = Host.CreateDefaultBuilder(args) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Модули инструментов (добавь новый IToolModule — он появится в TUI) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // HTTP MCP endpoint запускаем в фоне, чтобы TUI оставался владельцем консоли services.AddHostedService(); diff --git a/LazyBear.MCP/Services/Qdrant/QdrantClientProvider.cs b/LazyBear.MCP/Services/Qdrant/QdrantClientProvider.cs new file mode 100644 index 0000000..1d53056 --- /dev/null +++ b/LazyBear.MCP/Services/Qdrant/QdrantClientProvider.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Configuration; +using RestSharp; + +namespace LazyBear.MCP.Services.Qdrant; + +public sealed class QdrantClientProvider +{ + public RestClient? Client { get; } + + public string? InitializationError { get; } + + public QdrantClientProvider(IConfiguration configuration) + { + try + { + var url = configuration["Qdrant:Url"]; + if (string.IsNullOrWhiteSpace(url)) + { + throw new InvalidOperationException("Qdrant:Url не настроен в конфигурации."); + } + + Client = new RestClient(url); + } + catch (Exception ex) + { + InitializationError = $"{ex.GetType().Name}: {ex.Message}"; + } + } +} diff --git a/LazyBear.MCP/Services/Qdrant/QdrantKnowledgeTools.cs b/LazyBear.MCP/Services/Qdrant/QdrantKnowledgeTools.cs new file mode 100644 index 0000000..6532b7b --- /dev/null +++ b/LazyBear.MCP/Services/Qdrant/QdrantKnowledgeTools.cs @@ -0,0 +1,333 @@ +using System.ComponentModel; +using System.Text.Json; +using LazyBear.MCP.Services.ToolRegistry; +using ModelContextProtocol.Server; +using RestSharp; + +namespace LazyBear.MCP.Services.Qdrant; + +[McpServerToolType] +public sealed class QdrantKnowledgeTools( + QdrantClientProvider provider, + IConfiguration configuration, + ToolRegistryService registry) +{ + private readonly RestClient? _client = provider.Client; + private readonly string? _clientInitializationError = provider.InitializationError; + private readonly string _apiKey = configuration["Qdrant:ApiKey"] ?? string.Empty; + private readonly string _defaultCollection = configuration["Qdrant:DefaultCollection"] ?? "knowledge"; + + private const string ModuleName = "Qdrant"; + + private bool TryCheckEnabled(string toolName, out string error) + { + if (!registry.IsToolEnabled(ModuleName, toolName)) + { + error = $"Инструмент '{toolName}' модуля Qdrant отключён в TUI."; + return false; + } + + error = string.Empty; + return true; + } + + [McpServerTool, Description("Получить список коллекций Qdrant")] + public async Task ListCollections(CancellationToken cancellationToken = default) + { + if (!TryCheckEnabled("ListCollections", out var enabledError)) return enabledError; + if (!TryGetClient(out var client, out var error)) return error; + + try + { + var request = CreateRequest("/collections"); + var response = await client.ExecuteAsync(request, cancellationToken); + + if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) + { + return FormatResponseError("list_collections", response); + } + + using var document = JsonDocument.Parse(response.Content); + if (!document.RootElement.TryGetProperty("result", out var resultElement) || + !resultElement.TryGetProperty("collections", out var collectionsElement) || + collectionsElement.GetArrayLength() == 0) + { + return "Коллекции Qdrant не найдены."; + } + + var names = collectionsElement + .EnumerateArray() + .Select(item => GetNestedString(item, "name") ?? "unknown") + .ToArray(); + + return $"Коллекции Qdrant ({names.Length} шт.): {string.Join(", ", names)}"; + } + catch (Exception ex) + { + return FormatException("list_collections", ex); + } + } + + [McpServerTool, Description("Создать коллекцию Qdrant для базы знаний")] + public async Task CreateCollection( + [Description("Имя коллекции. Если пусто — используется Qdrant:DefaultCollection")] string? collection = null, + [Description("Размер вектора") ] int vectorSize = 1536, + [Description("Метрика расстояния: Cosine, Euclid, Dot или Manhattan")] string distance = "Cosine", + CancellationToken cancellationToken = default) + { + if (!TryCheckEnabled("CreateCollection", out var enabledError)) return enabledError; + if (!TryGetClient(out var client, out var error)) return error; + + var resolvedCollection = ResolveCollection(collection); + if (vectorSize <= 0) + return "vectorSize должен быть больше 0."; + + if (distance is not ("Cosine" or "Euclid" or "Dot" or "Manhattan")) + return $"distance должен быть одним из: Cosine, Euclid, Dot, Manhattan. Получено: '{distance}'."; + + try + { + var request = CreateRequest($"/collections/{resolvedCollection}", Method.Put); + request.AddJsonBody(new + { + vectors = new + { + size = vectorSize, + distance + } + }); + + var response = await client.ExecuteAsync(request, cancellationToken); + if (!response.IsSuccessful) + { + return FormatResponseError("create_collection", response, resolvedCollection); + } + + return $"Коллекция Qdrant '{resolvedCollection}' создана (vectorSize={vectorSize}, distance={distance})."; + } + catch (Exception ex) + { + return FormatException("create_collection", ex, resolvedCollection); + } + } + + [McpServerTool, Description("Добавить или обновить документ в базе знаний Qdrant")] + public async Task UpsertKnowledgeDocument( + [Description("ID документа/точки") ] string id, + [Description("Вектор embedding") ] float[] vector, + [Description("Текст/контент документа") ] string content, + [Description("Имя коллекции. Если пусто — используется Qdrant:DefaultCollection")] string? collection = null, + [Description("Дополнительные метаданные JSON-объектом") ] Dictionary? metadata = null, + CancellationToken cancellationToken = default) + { + if (!TryCheckEnabled("UpsertKnowledgeDocument", out var enabledError)) return enabledError; + if (!TryGetClient(out var client, out var error)) return error; + + if (string.IsNullOrWhiteSpace(id)) return "id документа не задан."; + if (!Guid.TryParse(id, out _) && !ulong.TryParse(id, out _)) + return $"id должен быть UUID (например, 550e8400-e29b-41d4-a716-446655440000) или uint64. Получено: '{id}'."; + if (vector is null || vector.Length == 0) return "vector не задан."; + + var resolvedCollection = ResolveCollection(collection); + + try + { + var payload = new Dictionary + { + ["content"] = content, + ["updatedAt"] = DateTimeOffset.UtcNow + }; + + if (metadata is not null) + { + foreach (var (key, value) in metadata) + { + payload[key] = value; + } + } + + var request = CreateRequest($"/collections/{resolvedCollection}/points", Method.Put); + request.AddJsonBody(new + { + points = new object[] + { + new + { + id, + vector, + payload + } + } + }); + + var response = await client.ExecuteAsync(request, cancellationToken); + if (!response.IsSuccessful) + { + return FormatResponseError("upsert_knowledge_document", response, resolvedCollection); + } + + return $"Документ '{id}' сохранён в коллекции '{resolvedCollection}'."; + } + catch (Exception ex) + { + return FormatException("upsert_knowledge_document", ex, resolvedCollection); + } + } + + [McpServerTool, Description("Векторный поиск по базе знаний Qdrant")] + public async Task SearchKnowledge( + [Description("Вектор запроса") ] float[] queryVector, + [Description("Имя коллекции. Если пусто — используется Qdrant:DefaultCollection")] string? collection = null, + [Description("Количество результатов") ] int limit = 5, + CancellationToken cancellationToken = default) + { + if (!TryCheckEnabled("SearchKnowledge", out var enabledError)) return enabledError; + if (!TryGetClient(out var client, out var error)) return error; + + if (queryVector is null || queryVector.Length == 0) return "queryVector не задан."; + + var resolvedCollection = ResolveCollection(collection); + var resolvedLimit = Math.Max(1, limit); + + try + { + var request = CreateRequest($"/collections/{resolvedCollection}/points/query", Method.Post); + request.AddJsonBody(new + { + query = queryVector, + limit = resolvedLimit, + with_payload = true + }); + + var response = await client.ExecuteAsync(request, cancellationToken); + if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content)) + { + return FormatResponseError("search_knowledge", response, resolvedCollection); + } + + using var document = JsonDocument.Parse(response.Content); + if (!document.RootElement.TryGetProperty("result", out var resultsElement) || + resultsElement.GetArrayLength() == 0) + { + return "Поиск по базе знаний не дал результатов."; + } + + var lines = new List(); + foreach (var item in resultsElement.EnumerateArray()) + { + var pointId = item.TryGetProperty("id", out var idElement) ? idElement.ToString() : "-"; + var score = item.TryGetProperty("score", out var scoreElement) + ? scoreElement.GetDouble().ToString("0.####") + : "-"; + var content = (GetNestedString(item, "payload", "content") ?? "(без контента)") + .ReplaceLineEndings(" "); + lines.Add($"id={pointId}; score={score}; content={content}"); + } + + return $"Результаты поиска Qdrant ({lines.Count}):\n{string.Join('\n', lines)}"; + } + catch (Exception ex) + { + return FormatException("search_knowledge", ex, resolvedCollection); + } + } + + [McpServerTool, Description("Удалить документ из базы знаний Qdrant по ID")] + public async Task DeleteKnowledgeDocument( + [Description("ID документа/точки") ] string id, + [Description("Имя коллекции. Если пусто — используется Qdrant:DefaultCollection")] string? collection = null, + CancellationToken cancellationToken = default) + { + if (!TryCheckEnabled("DeleteKnowledgeDocument", out var enabledError)) return enabledError; + if (!TryGetClient(out var client, out var error)) return error; + + if (string.IsNullOrWhiteSpace(id)) return "id документа не задан."; + if (!Guid.TryParse(id, out _) && !ulong.TryParse(id, out _)) + return $"id должен быть UUID (например, 550e8400-e29b-41d4-a716-446655440000) или uint64. Получено: '{id}'."; + + var resolvedCollection = ResolveCollection(collection); + + try + { + var request = CreateRequest($"/collections/{resolvedCollection}/points/delete", Method.Post); + request.AddJsonBody(new + { + points = new[] { id } + }); + + var response = await client.ExecuteAsync(request, cancellationToken); + if (!response.IsSuccessful) + { + return FormatResponseError("delete_knowledge_document", response, resolvedCollection); + } + + return $"Документ '{id}' удалён из коллекции '{resolvedCollection}'."; + } + catch (Exception ex) + { + return FormatException("delete_knowledge_document", ex, resolvedCollection); + } + } + + private string ResolveCollection(string? collection) + { + return string.IsNullOrWhiteSpace(collection) ? _defaultCollection : collection; + } + + private RestRequest CreateRequest(string resource, Method method = Method.Get) + { + var request = new RestRequest(resource, method); + request.AddHeader("Accept", "application/json"); + + if (!string.IsNullOrWhiteSpace(_apiKey)) + { + request.AddHeader("api-key", _apiKey); + } + + return request; + } + + private bool TryGetClient(out RestClient client, out string error) + { + if (_client is null) + { + client = null!; + var details = string.IsNullOrWhiteSpace(_clientInitializationError) + ? string.Empty + : $" Детали: {_clientInitializationError}"; + error = "Qdrant клиент не инициализирован. Проверьте Qdrant:Url." + details; + return false; + } + + client = _client; + error = string.Empty; + return true; + } + + private static string? GetNestedString(JsonElement element, params string[] path) + { + var current = element; + foreach (var segment in path) + { + if (!current.TryGetProperty(segment, out current)) + { + return null; + } + } + + return current.ValueKind == JsonValueKind.String ? current.GetString() : current.ToString(); + } + + private static string FormatResponseError(string toolName, RestResponse response, string? resource = null) + { + var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'"; + var body = string.IsNullOrWhiteSpace(response.Content) ? "-" : response.Content; + return $"Ошибка Qdrant в tool '{toolName}'{resourcePart}: status={(int)response.StatusCode}, details={body}"; + } + + private static string FormatException(string toolName, Exception exception, string? resource = null) + { + var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'"; + return $"Ошибка Qdrant в tool '{toolName}'{resourcePart}: {exception.GetType().Name}: {exception.Message}"; + } +} diff --git a/LazyBear.MCP/Services/Qdrant/QdrantToolModule.cs b/LazyBear.MCP/Services/Qdrant/QdrantToolModule.cs new file mode 100644 index 0000000..04254e1 --- /dev/null +++ b/LazyBear.MCP/Services/Qdrant/QdrantToolModule.cs @@ -0,0 +1,18 @@ +using LazyBear.MCP.Services.ToolRegistry; + +namespace LazyBear.MCP.Services.Qdrant; + +public sealed class QdrantToolModule : IToolModule +{ + public string ModuleName => "Qdrant"; + public string Description => "Qdrant: база знаний (коллекции, документы, векторный поиск)"; + + public IReadOnlyList ToolNames => + [ + "ListCollections", + "CreateCollection", + "UpsertKnowledgeDocument", + "SearchKnowledge", + "DeleteKnowledgeDocument" + ]; +} diff --git a/LazyBear.MCP/appsettings.json b/LazyBear.MCP/appsettings.json index 8fb02b1..c968790 100644 --- a/LazyBear.MCP/appsettings.json +++ b/LazyBear.MCP/appsettings.json @@ -19,6 +19,11 @@ "Token": "", "Project": "" }, + "Qdrant": { + "Url": "", + "ApiKey": "", + "DefaultCollection": "knowledge" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index dcd477a..525803d 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -22,6 +22,7 @@ 1. ✅ Создать Memory Bank структуру 2. 🔄 Продолжить документирование архитектуры и паттернов 3. ⏳ Обновлять Memory Bank после значимых изменений +4. ⏳ Добавить Qdrant в Memory Bank ## 🔍 Важные решения @@ -61,6 +62,11 @@ "Token": "", "Username": "", "SpaceKey": "" + }, + "Qdrant": { + "Url": "", // URL Qdrant сервера + "ApiKey": "", // Опционально для авторизации + "DefaultCollection": "knowledge" // Default коллекция для векторного поиска } } ``` diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md index ba2aab6..018e3fa 100644 --- a/memory-bank/productContext.md +++ b/memory-bank/productContext.md @@ -125,6 +125,22 @@ TUI показывает: - Статус подов - Последние events - Кнопки действий +- Векторные индексы Qdrant (если используется) +``` + +### Сценарий 4: Поиск знаний через Qdrant + +``` +User (via AI): "Как настроить деплой nginx?" +↓ +AI вызывает: qdrantKnowledgeTools/search({ + query: "nginx деплой настройка", + vector_embedding: [0.1, 0.2, ...] +}) +↓ +Qdrant возвращает релевантные документы +↓ +AI предоставляет из Confluence/документации ``` ## Метрики успеха @@ -133,6 +149,7 @@ TUI показывает: |---------|------| | Время на задачу (Jira) | -50% после интеграции | | Время на задачу (K8s) | -70% после интеграции | +| Время на поиск знаний | -60% с Qdrant | | Довольство пользователей | >4.5/5 | | Количество инцидентов | Снизить на -30% | @@ -163,4 +180,4 @@ TUI показывает: --- -*Документ описывает почему проект существует и как должен работать пользователь.* \ No newline at end of file +*Документ описывает почему проект существует и как должен работать пользователь.* diff --git a/memory-bank/progress.md b/memory-bank/progress.md index da6a87a..add76e5 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -50,6 +50,15 @@ - `create_branch` — создать ветку - `delete_branch` — удалить ветку +### Qdrant Integration (Vector DB) + +- ✅ CRUD коллекции (создание, удаление) +- ✅ Добавление/обновление документов (upsert) +- ✅ Векторный поиск по коллекции +- ✅ Удаление документов по ID +- ✅ Поддержка метрик (Cosine, Euclid, Dot) +- ✅ Поддержка API ключа (опционально) + ### MCP Server - ✅ HTTP Transport MCP 1.2.0 @@ -168,7 +177,7 @@ **Состояние**: Development -**Последний commit**: `e96bab114ea1a58f3ea7bd5ab40d4645d456cd8f` +**Последний commit**: `b5fe2623b3d14333a7138c22456862bff3781b82` **Что работает**: Все основные функциональности готовы diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md index a2b5b50..22099db 100644 --- a/memory-bank/systemPatterns.md +++ b/memory-bank/systemPatterns.md @@ -18,16 +18,17 @@ ↓ ┌─────────────────────────────────────────────────────────┐ │ Services Layer (IToolModule) │ -│ ┌──────────┐ ┌───────────────┐ ┌────────────────────┐ │ -│ │JiraTools │ │ConfluenceTools│ │KubernetesTools │ │ -│ └──────────┘ └───────────────┘ └────────────────────┘ │ +│ ┌──────────┐ ┌───────────────┐ ┌────────────────────┐ ┌─────┐│ +│ │JiraTools │ │ConfluenceTools│ │KubernetesTools │ │Qdrant││ +│ └──────────┘ └───────────────┘ └────────────────────┘ └─────┘│ +│ └────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ External API Layer │ -│ ┌──────────┐ ┌───────────────┐ ┌────────────────────┐ │ -│ │ Jira API │ │Confluence API │ │ K8s API │ │ -│ └──────────┘ └───────────────┘ └────────────────────┘ │ +│ ┌──────────┐ ┌───────────────┐ ┌────────────────────┐ ┌────┐│ +│ │ Jira API │ │Confluence API │ │ K8s API │ │Qdr│ │ +│ └──────────┘ └───────────────┘ └────────────────────┘ └───┘ │ └─────────────────────────────────────────────────────────┘ ``` @@ -289,4 +290,33 @@ Console.WriteLine --- -*Файл описывает систему архитектуры, ключевые компоненты и потоки данных. Обновлять при введении новых архитектурных решений.* \ No newline at end of file +### Qdrant Client Provider Pattern + +**File**: `LazyBear.MCP/Services/Qdrant/QdrantClientProvider.cs` + +```csharp +public class QdrantClientProvider +{ + private readonly IConfiguration _config; + + public QdrantClient GetClient() + { + // Конфиг из appsettings.json + var url = _config["Qdrant:Url"]; + var apiKey = _config["Qdrant:ApiKey"] ?? string.Empty; + + return new QdrantClient(url, apiKey); + } +} +``` + +**Fallback порядок**: +1. Explicit URL из конфига +2. Environment variable QDRANT_URL +3. Localhost default + +**Ответственность**: Создание клиентов Qdrant с поддержкой API ключа (опционально) + +--- + +*Файл описывает систему архитектуры, ключевые компоненты и потоки данных. Обновлять при введении новых архитектурных решений.* diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md index 6c1b6c3..f998393 100644 --- a/memory-bank/techContext.md +++ b/memory-bank/techContext.md @@ -13,6 +13,7 @@ | **Model Context Protocol** | 1.2.0 | MCP стандарт | | **Kubernetes Client** | 13+ | .NET SDK для K8s | | **RazorConsole** | Latest | TUI framework | +| **Qdrant.Client** | Latest | Векторный поиск,知识库 | ### Файлы конфигурации @@ -101,6 +102,10 @@ LazyBear.MCP/ │ │ └── JiraClientProvider.cs │ ├── Confluence/ │ │ └── ConfluencePagesTools.cs +│ ├── Qdrant/ +│ │ ├── QdrantClientProvider.cs +│ │ ├── QdrantKnowledgeTools.cs +│ │ └── QdrantToolModule.cs │ └── Kubernetes/ │ ├── K8sConfigTools.cs │ ├── K8sDeploymentTools.cs @@ -215,6 +220,13 @@ echo '{"jsonrpc":"2.0","id":1,"method":"k8sPodsTools/getPodStatus","params":{"na - `Jira:Url` обязателен, иначе инициализация провайдера может упасть - `Kubernetes:KubeconfigPath` может быть пустым — используется fallback +- `Qdrant:Url` обязателен для векторного поиска, если используется +- `Qdrant:ApiKey` опционален, но рекомендуется для безопасного доступа + +### Qdrant Gotchas + +- Косинусная метрика — default для векторного поиска +- Размер вектора должен быть фиксированным при создании коллекции ### RazorConsole Gotchas