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}"; } }