From c72e4aa80e4511a62dab0c9e7ab3b797054f0d4e Mon Sep 17 00:00:00 2001 From: Victor Buzin Date: Tue, 16 Jun 2026 10:31:45 +0300 Subject: [PATCH 1/5] feat: add Operator role with MCP integration and upgrade to .NET 10 / C# 15 (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Operator (MCP tools) micro-agent role Introduce a new Operator role — a lightweight micro-agent that connects the council to MCP (Model Context Protocol) servers and exposes their tools (web search, filesystem, Notion, PostgreSQL, debate history, etc.) to debate participants. - Participants delegate tasks at any moment via the [[OPERATOR: task]] marker - Operator uses its own cheaper LLM to plan tool calls, execute MCP tools and interpret results, optionally reusing the council's compression strategy - Wired through all debate strategies (Standard/Critique/Consensus), CouncilExecutor, CouncilBuilder and DI options (Delibera:Operator) - DI-initializable; supports stdio and HTTP MCP transports - Per-round Operator interactions rendered in the final Markdown report - Adds ModelContextProtocol package, console --operator flag, example & docs * feat: upgrade to .NET 10 / C# 15, high-perf optimizations & MCP tools example - Bump LangVersion to preview (C# 15) in both projects - Update Microsoft.SourceLink.GitHub to 10.0.300 (other deps already at latest .NET 10 versions) - Perf: SIMD-vectorized CosineSimilarity (ReadOnlySpan + Vector) - Perf: TokenCounter ReadOnlySpan overload (alloc-free word counting) - Perf: CompressionCache.ComputeKey uses ArrayPool + stackalloc SHA-256 - Add OperatorMcpToolsExample (browser via @playwright/mcp, Marp presentations) + --operator-mcp flag - Fix nullable warning in Program.cs - Add Russian docs: docs/NET10-Upgrade-RU.md * docs: Add English version of .NET 10 upgrade documentation * docs: Update README and QuickStart with Operator role documentation --------- Co-authored-by: techbuzzz Co-authored-by: Delibera Agent --- README-RU.md | 689 ++++++++++++++++++ README.md | 176 ++++- docs/NET10-Upgrade-RU.docx | Bin 0 -> 20175 bytes docs/NET10-Upgrade-RU.md | 301 ++++++++ docs/NET10-Upgrade-RU.pdf | Bin 0 -> 112771 bytes docs/NET10-Upgrade.docx | Bin 0 -> 18738 bytes docs/NET10-Upgrade.md | 306 ++++++++ docs/NET10-Upgrade.pdf | Bin 0 -> 104010 bytes docs/QuickStart-RU.md | 451 ++++++++++++ docs/QuickStart.md | 172 ++++- .../Delibera.ConsoleApp.csproj | 2 +- .../Examples/OperatorExample.cs | 111 +++ .../Examples/OperatorMcpToolsExample.cs | 237 ++++++ src/Delibera.ConsoleApp/Program.cs | 23 +- .../Compression/CompressionCache.cs | 31 +- .../Compression/SemanticCompressor.cs | 36 +- src/Delibera.Core/Compression/TokenCounter.cs | 15 +- src/Delibera.Core/Council/CouncilBuilder.cs | 52 +- src/Delibera.Core/Council/CouncilExecutor.cs | 38 +- src/Delibera.Core/Council/Operator.cs | 354 +++++++++ src/Delibera.Core/Debate/ConsensusDebate.cs | 26 +- src/Delibera.Core/Debate/CritiqueDebate.cs | 26 +- .../Debate/DebateResultBuilder.cs | 4 +- src/Delibera.Core/Debate/DebateScenario.cs | 106 ++- src/Delibera.Core/Debate/StandardDebate.cs | 29 +- src/Delibera.Core/Delibera.Core.csproj | 5 +- .../DependencyInjection/CouncilOptions.cs | 78 ++ .../Interfaces/ICouncilBuilder.cs | 24 + .../Interfaces/ICouncilExecutor.cs | 3 + .../Interfaces/IDebateStrategy.cs | 2 + src/Delibera.Core/Interfaces/IMcpClient.cs | 41 ++ src/Delibera.Core/Interfaces/IOperator.cs | 51 ++ src/Delibera.Core/Models/DebateResult.cs | 22 + src/Delibera.Core/Models/DebateRound.cs | 3 + src/Delibera.Core/Models/OperatorModels.cs | 176 +++++ .../Providers/Mcp/McpClientAdapter.cs | 162 ++++ src/Delibera.Core/README.md | 112 ++- 37 files changed, 3814 insertions(+), 50 deletions(-) create mode 100644 README-RU.md create mode 100644 docs/NET10-Upgrade-RU.docx create mode 100644 docs/NET10-Upgrade-RU.md create mode 100644 docs/NET10-Upgrade-RU.pdf create mode 100644 docs/NET10-Upgrade.docx create mode 100644 docs/NET10-Upgrade.md create mode 100644 docs/NET10-Upgrade.pdf create mode 100644 docs/QuickStart-RU.md create mode 100644 src/Delibera.ConsoleApp/Examples/OperatorExample.cs create mode 100644 src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs create mode 100644 src/Delibera.Core/Council/Operator.cs create mode 100644 src/Delibera.Core/Interfaces/IMcpClient.cs create mode 100644 src/Delibera.Core/Interfaces/IOperator.cs create mode 100644 src/Delibera.Core/Models/OperatorModels.cs create mode 100644 src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs diff --git a/README-RU.md b/README-RU.md new file mode 100644 index 0000000..d4811dd --- /dev/null +++ b/README-RU.md @@ -0,0 +1,689 @@ +
+ +Delibera + +# Delibera + +### ⚖️ Продуманные решения с помощью ИИ + +**Коллективное принятие решений через структурированное обсуждение ИИ — с RAG, pgvector, Knowledge Keeper, 🛠️ Operator (MCP-инструменты), Chairman, 🔥 сжатием контекста, 💉 Dependency Injection и 📋 журналированием выполнения** + +[![NuGet](https://img.shields.io/nuget/v/Delibera.Core.svg)](https://www.nuget.org/packages/Delibera.Core) +[![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) +[![.NET 10](https://img.shields.io/badge/.NET-10.0-1F2937.svg)](https://dotnet.microsoft.com) +[![C# 15](https://img.shields.io/badge/C%23-15.0--preview-239120.svg)](https://learn.microsoft.com/dotnet/csharp/) + +🇬🇧 [English version (README.md)](README.md) + +
+ +--- + +## 📖 Обзор + +**Delibera** — это фреймворк на C# / .NET 10, который оркеструет **многомодельные обсуждения** +между LLM. Несколько моделей ИИ рассуждают над вопросом в течение структурированных раундов, +критикуют ответы друг друга, а **Chairman** (председатель) взвешивает аргументы, чтобы +синтезировать сбалансированный финальный вердикт — обогащённый **Knowledge Keeper** на базе +**Qdrant** или **PostgreSQL/pgvector** (RAG), с **интеллектуальным сжатием контекста** для +минимизации расхода токенов. + +Название происходит от слова *deliberation* — тщательного взвешивания доказательств и точек зрения +перед принятием решения. Delibera привносит эту дисциплину в ИИ, помогая командам приходить к +**продуманным, хорошо обоснованным результатам**, а не к догадкам одной модели. + +--- + +## ✨ Ключевые возможности + +| Возможность | Описание | +| --------------------------------- | --------------------------------------------------------------------------------- | +| **🏛️ Многомодельные советы** | Оркестрация любого числа LLM-участников по структурированным раундам дебатов | +| **⚖️ Синтез Chairman** | Выделенный модератор открывает, регулирует и синтезирует финальный вердикт | +| **📚 Knowledge Keeper (RAG)** | Семантический поиск по раундам со структурированными ответами и цитированием | +| **🛠️ Operator (MCP-инструменты)** | Микроагент, делегирующий задачи MCP-серверам (веб, файлы, Marp, Notion, …) по запросу в ходе дебатов | +| **🐘 Qdrant + pgvector** | Подключаемые векторные хранилища — отдельная БД или ваш существующий PostgreSQL | +| **🗜️ Сжатие контекста** | 4 стратегии (Semantic, Deduplication, Summarization, Hybrid) экономят 30–70% токенов | +| **💉 Dependency Injection** | Расширение `AddDelibera()` для `IServiceCollection` с полной привязкой опций | +| **📋 Журналирование выполнения** | Модель `ExecutionLog` с `LogLevel` — события Chairman, KK, сжатия и участников | +| **📁 Раздельный вывод файлов** | Экспорт `result.md`, `statistics.md` и `logs.md` по отдельности | +| **🔌 Interface-First** | Чистые абстракции для провайдеров, фабрик, билдеров и исполнителей | +| **🧱 Современный C# 15 (preview)** | Построено на .NET 10 с `LangVersion=preview`, file-scoped namespaces, records, span/SIMD горячие пути | + +--- + +## 📑 Содержание + +- [Быстрый старт](#-быстрый-старт) + - [Требования и модели](#требования-и-модели) + - [Установка](#установка) + - [Минимальный пример](#минимальный-пример) + - [Запуск](#запуск) +- [Dependency Injection](#-dependency-injection) +- [Operator (MCP-инструменты)](#️-operator-mcp-инструменты) +- [Сжатие контекста](#️-сжатие-контекста) +- [Интеграция RAG](#-интеграция-rag) +- [Стратегии дебатов](#️-стратегии-дебатов) +- [Структура выходных файлов](#-структура-выходных-файлов) +- [Примеры ConsoleApp](#-примеры-consoleapp) +- [Установка и сборка](#️-установка-и-сборка) +- [Архитектура](#️-архитектура) +- [Участие в разработке](#-участие-в-разработке) +- [Лицензия](#-лицензия) + +--- + +## 🚀 Быстрый старт + +### Требования и модели + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (≥ 10.0.301) — проект нацелен на + `net10.0` и собирается с `LangVersion=preview` для включения возможностей **C# 15**. См. + [docs/NET10-Upgrade-RU.md](docs/NET10-Upgrade-RU.md) для полных заметок о миграции. +- Запущенный экземпляр [Ollama](https://ollama.com) — локально (`ollama serve`) или + [Ollama Cloud](https://ollama.com/cloud) (только API-ключ, без установки). +- Минимальный набор моделей, перечисленный ниже. +- **Опционально — для роли Operator:** [Node.js + npx](https://nodejs.org) для запуска MCP-серверов + (например, `@playwright/mcp`, `@marp-team/marp-cli`). См. [Operator (MCP-инструменты)](#️-operator-mcp-инструменты). + +#### 🟢 Минимальный — для небольших дебатов (≈ 2 ГБ всего) + +Подходит для smoke-тестов, слабого железа и быстрых запусков из CLI. + +| Назначение | Модель | Размер | Команда загрузки | +| ------------------- | ------------------- | ------- | ---------------------------------- | +| Участник совета | `llama3.2:1b` | 1.3 ГБ | `ollama pull llama3.2:1b` | +| Участник совета | `qwen2.5:1.5b` | 1.1 ГБ | `ollama pull qwen2.5:1.5b` | +| Эмбеддинги (RAG) | `nomic-embed-text` | 274 МБ | `ollama pull nomic-embed-text` | + +#### 🟡 Стандартный — рекомендуется для большинства случаев (≈ 7 ГБ всего) + +Хорошее качество рассуждений при низкой задержке. **Это набор по умолчанию, используемый по всему README.** + +| Назначение | Модель | Размер | Команда загрузки | +| ------------------- | -------------------- | ------- | ----------------------------------- | +| Участник совета | `llama3.2:3b` | 2.0 ГБ | `ollama pull llama3.2:3b` | +| Участник совета | `qwen2.5:7b` | 4.7 ГБ | `ollama pull qwen2.5:7b` | +| Эмбеддинги (RAG) | `nomic-embed-text` | 274 МБ | `ollama pull nomic-embed-text` | + +#### 🔴 Высокопроизводительный — для продакшн-уровня дебатов (≈ 30+ ГБ) + +Более тяжёлые локальные модели, рекомендуются на GPU с ≥ 24 ГБ VRAM или на Ollama Cloud. + +| Назначение | Модель | Размер | Команда загрузки | +| ------------------- | ---------------------------- | --------- | --------------------------------------------- | +| Участник совета | `llama3.1:8b` | 4.9 ГБ | `ollama pull llama3.1:8b` | +| Участник совета | `qwen2.5:14b` | 9.0 ГБ | `ollama pull qwen2.5:14b` | +| Участник совета | `mistral:7b` | 4.4 ГБ | `ollama pull mistral:7b` | +| Chairman | `qwen2.5:14b` *(или больше)* | 9.0 ГБ | `ollama pull qwen2.5:14b` | +| Эмбеддинги (RAG) | `nomic-embed-text` | 274 МБ | `ollama pull nomic-embed-text` | + +> 💡 **Ollama Cloud** использует те же имена моделей, но не требует места на локальном диске — нужен +> только API-ключ. См. [раздел конфигурации](#-dependency-injection) для его настройки. + +### Установка + +```bash +dotnet add package Delibera.Core +``` + +### Минимальный пример + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Providers; + +using var factory = new ProviderFactory(); +var ollama = factory.CreateOllama("http://localhost:11434"); + +var result = await new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Analyst") + .AddMember("qwen2.5:7b", ollama, "Strategist") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + .WithStandardDebate() + .WithSystemPrompt("You are a software architecture expert.") + .WithUserPrompt("Microservices vs Monolith for a 5-person startup?") + .WithMaxRounds(4) + .SaveResultTo("./deliberation.md") + .Build() + .ExecuteAsync(); + +Console.WriteLine(result.FinalVerdict); +``` + +> 📄 См. [docs/QuickStart-RU.md](docs/QuickStart-RU.md) для пошагового руководства. + +### Запуск + +```bash +dotnet run +``` + +Delibera проведёт структурированные многораундовые дебаты и запишет полную стенограмму и вердикт +председателя в файл `deliberation.md`. + +--- + +## 💉 Dependency Injection + +Зарегистрируйте все сервисы Delibera одной строкой: + +```csharp +using Delibera.Core.DependencyInjection; + +// Вариант A: С привязкой конфигурации (привязывает секцию "Delibera") +services.AddDelibera(configuration, "Delibera"); + +// Вариант B: С делегатом опций +services.AddDelibera(options => +{ + options.Strategy = "Standard"; + options.MaxRounds = 4; + options.Temperature = 0.7f; + options.Compression.Enabled = true; + options.Compression.Strategy = "Hybrid"; + options.Compression.TargetRatio = 0.5; +}); + +// Вариант C: Только значения по умолчанию +services.AddDelibera(); +``` + +Резолвит из DI следующие интерфейсы: + +| Интерфейс | Реализация | Время жизни | +| --------------------- | -------------------- | ----------- | +| `ILLMProviderFactory` | `ProviderFactory` | Singleton | +| `IRagProviderFactory` | `RagProviderFactory` | Singleton | +| `ICompressionFactory` | `CompressionService` | Singleton | +| `ICouncilBuilder` | `CouncilBuilder` | Transient | + +### Конфигурация (`appsettings.json`) + +```json +{ + "Delibera": { + "Strategy": "Standard", + "MaxRounds": 4, + "Temperature": 0.7, + "SystemPrompt": "You are a knowledgeable AI expert participating in a council debate.", + "Providers": { + "DefaultType": "Ollama", + "DefaultEndpoint": "http://localhost:11434", + "ApiKey": "", + "EmbeddingModel": "nomic-embed-text" + }, + "Compression": { + "Enabled": true, + "Strategy": "Hybrid", + "TargetRatio": 0.5, + "EnableCache": true, + "MaxCacheEntries": 256 + }, + "Rag": { + "Enabled": false, + "ProviderType": "Qdrant", + "Host": "localhost", + "Port": 6334, + "CollectionName": "council_knowledge", + "ConnectionString": null + }, + "Output": { + "Directory": "./debate_results", + "SeparateFiles": true, + "FilePrefix": null + } + } +} +``` + +Чтобы использовать **Ollama Cloud**, задайте `Providers:DefaultEndpoint` равным +`https://api.ollama.com` и поместите ключ в `Providers:ApiKey` (или `OllamaCloud:ApiKey` в секции +`DeliberaApp`, используемой консольным приложением — см. [Примеры ConsoleApp](#-примеры-consoleapp)). + +--- + +## 🛠️ Operator (MCP-инструменты) + +**Operator** — это лёгкий микроагент, который соединяет совет с внешним миром через серверы +[**MCP (Model Context Protocol)**](https://modelcontextprotocol.io). Он предоставляет участникам +дебатов любые инструменты, которые дают эти серверы — веб-навигацию, доступ к файловой системе, +генерацию Marp-презентаций, Notion, PostgreSQL и т. д. + +**Как это работает** + +1. Operator подключается к одному или нескольким MCP-серверам и обнаруживает их инструменты при + `InitializeAsync`. +2. Участникам сообщается (в их системном промпте), что умеет Operator, и они могут делегировать + задачу в **любой момент** дебатов, написав маркер в своём сообщении: + + ``` + [[OPERATOR: открой https://modelcontextprotocol.io и кратко перескажи, что такое MCP]] + ``` + +3. Operator интерпретирует запрос своей **собственной (более дешёвой) LLM-моделью**, выбирает и + вызывает нужные MCP-инструменты, интерпретирует результаты и возвращает краткий ответ, который + внедряется в следующий раунд. +4. Если совет использует **сжатие контекста**, Operator может переиспользовать ту же стратегию для + сжатия больших выводов инструментов перед их возвратом в дебаты. Его `DisposeAsync` — + это `ValueTask` (паттерн `IAsyncDisposable` из .NET 10), поэтому MCP-клиенты освобождаются без + аллокации `Task`. + +Все взаимодействия с Operator записываются по раундам и отображаются в финальном Markdown-отчёте в +блоке **🛠️ Operator Interactions**. + +### Настройка MCP-серверов + +```csharp +using Delibera.Core.Models; + +var servers = new[] +{ + // stdio-транспорт — запускает локальный процесс MCP-сервера + McpServerConfig.Stdio( + name: "browser", + command: "npx", + arguments: new[] { "-y", "@playwright/mcp@latest", "--headless" }), + + McpServerConfig.Stdio( + name: "marp", + command: "npx", + arguments: new[] { "-y", "@marp-team/marp-cli", "--server", "./out" }), + + // …или HTTP/SSE-транспорт для удалённого MCP-сервера + // McpServerConfig.Http( + // name: "remote", + // endpoint: "https://my-mcp-host.example.com/mcp", + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }), +}; +``` + +| Сервер | Транспорт | Команда запуска | Что даёт | +| ------------ | --------- | ----------------------------------------------- | ---------------------------------------------------- | +| 🌐 `browser` | stdio | `npx -y @playwright/mcp@latest --headless` | Навигация по сайтам, чтение страниц, клики, скриншоты | +| 🎯 `marp` | stdio | `npx -y @marp-team/marp-cli --server ` | Генерация презентаций (HTML/PDF/PPTX) из Markdown | + +### Быстрое использование (внутри совета) + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Providers.LLM; + +var ollama = new OllamaProvider("http://localhost:11434"); + +var council = new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Optimist") + .AddMember("qwen2.5:7b", ollama, "Skeptic") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + // Operator использует свою более дешёвую модель; reuseCompression разделяет компрессор совета + .WithOperator("llama3.2:3b", ollama, servers, reuseCompression: true) + .WithStandardDebate() + .WithUserPrompt("Research the latest .NET 10 features and prepare a short summary.") + .WithMaxRounds(4) + .Build() + .ExecuteAsync(); +``` + +Предпочитаете создать `Operator` самостоятельно? Передайте готовый экземпляр: + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Interfaces; +using Delibera.Core.Models; +using Delibera.Core.Providers.Mcp; + +var @operator = new Operator( + new CouncilMember("llama3.2:3b", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }, + compressor: null, // необязательный IContextCompressor + compressionOptions: null); // необязательные CompressionOptions + +var council = new CouncilBuilder() + /* …участники… */ + .WithOperator(@operator) + .Build(); +``` + +### Прямое использование (без совета) + +```csharp +await using var @operator = new Operator( + new CouncilMember("llama3.2:3b", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }); + +await @operator.InitializeAsync(); + +var result = await @operator.ExecuteTaskAsync( + "Open https://modelcontextprotocol.io and briefly summarize what MCP is."); + +Console.WriteLine(result.FinalAnswer); +``` + +### Dependency Injection + +Настройте Operator декларативно в `appsettings.json` в секции `Delibera:Operator`: + +```json +{ + "Delibera": { + "Operator": { + "Enabled": true, + "ModelName": "llama3.2:3b", + "ReuseCompression": true, + "McpServers": [ + { + "Name": "browser", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@playwright/mcp@latest", "--headless" ] + }, + { + "Name": "remote", + "Transport": "Http", + "Endpoint": "https://my-mcp-host.example.com/mcp", + "AdditionalHeaders": { "Authorization": "Bearer " } + } + ] + } + } +} +``` + +> ▶️ Полный рабочий пример находится в +> [`OperatorMcpToolsExample.cs`](src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs). +> Запустите его командой `dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp`. +> Полное техническое описание см. в [docs/NET10-Upgrade-RU.md](docs/NET10-Upgrade-RU.md). + +--- + +## 🗜️ Сжатие контекста + +Автоматически сжимайте контекст между раундами обсуждения — экономьте **30–70% токенов** без потери смысла. + +| Стратегия | Как работает | Лучше всего для | +| ----------------- | -------------------------------------------------------- | -------------------------------- | +| **Semantic** | Эмбеддинг предложений, ранжирование по релевантности, топ-N | Большие контексты знаний | +| **Deduplication** | Удаляет семантически похожие предложения между участниками | Многомодельные дебаты с пересечениями | +| **Summarization** | LLM создаёт краткое резюме, сохраняя ключевые факты | Максимальная степень сжатия | +| **Hybrid** | Конвейер Dedup → Semantic → Summarize | Лучшее общее качество | +| **None** | Без изменений (когда отключено) | Отладка | + +```csharp +using Delibera.Core.Compression; +using Delibera.Core.Providers.LLM; + +var ollama = new OllamaProvider("http://localhost:11434"); +var embeddings = new OllamaEmbeddingProvider(ollama, "nomic-embed-text"); + +var result = await new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Analyst") + .AddMember("qwen2.5:7b", ollama, "Strategist") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + .WithCompression(CompressionStrategy.Hybrid, + llmProvider: ollama, + modelName: "llama3.2:3b", + embeddingProvider: embeddings) + .WithCompressionOptions(new CompressionOptions { TargetRatio = 0.5 }) + .WithCompressionCache() + .WithUserPrompt("Analyze our architecture options...") + .WithMaxRounds(4) + .Build() + .ExecuteAsync(); + +Console.WriteLine(result.TokenStats?.ToSummary()); +``` + +### Конвейер сжатия + +``` +IContextCompressor +├── SemanticCompressor ← Ранжирование предложений на основе эмбеддингов +├── DeduplicationCompressor ← Удаление дубликатов по схожести +├── SummarizationCompressor ← Резюмирование с помощью LLM +├── HybridCompressor ← Многоэтапный конвейер (Dedup → Semantic → Summarize) +└── PassThroughCompressor ← Без операций (когда отключено) + +CompressionFactory ← Статическая фабрика (Create по enum или строке) +CompressionService ← DI-дружественная обёртка над CompressionFactory +CompressionCache ← LRU-кэш с ключами SHA-256 +TokenCounter ← Эвристическая оценка токенов +``` + +--- + +## 📚 Интеграция RAG + +Используйте выделенный экземпляр **Qdrant** или вашу существующую базу **PostgreSQL/pgvector** в +качестве векторного хранилища. + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Models; +using Delibera.Core.Providers.LLM; +using Delibera.Core.Providers.RAG; + +var ollama = new OllamaProvider("http://localhost:11434"); +var embeddings = new OllamaEmbeddingProvider(ollama, "nomic-embed-text"); + +// pgvector — просто добавьте строку подключения +var ragFactory = new RagProviderFactory(); +var rag = ragFactory.CreatePgVector( + embeddings, + "Host=localhost;Database=council_vectors;Username=postgres;Password=postgres"); + +await rag.IndexDocumentAsync("my_collection", documentText); +var results = await rag.SearchAsync("my_collection", "query", limit: 5); + +// Подключаем к Knowledge Keeper +var kkMember = new CouncilMember("llama3.2:3b", ollama, "Knowledge Keeper"); +var keeper = new KnowledgeKeeper(rag, kkMember, "my_knowledge"); +await keeper.IndexFileAsync("./docs/architecture.md"); +``` + +``` +IRagProvider +├── QdrantRagProvider +│ └── QdrantVectorStore ← Qdrant gRPC +└── PgVectorRagProvider + └── PgVectorStore ← PostgreSQL/pgvector + +IEmbeddingProvider +└── OllamaEmbeddingProvider +``` + +Затем Knowledge Keeper присоединяется к совету через `WithKnowledgeKeeper(...)` — см. +[пример RAG](src/Delibera.ConsoleApp/Examples/RagExample.cs) для полного рабочего демо. + +--- + +## 🗣️ Стратегии дебатов + +| Стратегия | Поток | Сценарий использования | +| --------------------- | ------------------------------------------------------- | ----------------------- | +| **StandardDebate** | Initial → Critique → Improved → Verdict | Общий анализ | +| **CritiqueDebate** | Position → Attack → Defence → Judge | Проверка гипотез | +| **ConsensusDebate** | Perspectives → Common Ground → Consensus → Facilitator | Поиск оптимального решения | + +Каждая стратегия реализована как `IDebateStrategy` — см. +[`src/Delibera.Core/Debate/`](src/Delibera.Core/Debate/) для полного исходного кода. + +### Паттерны проектирования + +| Паттерн | Использование | +| ------------------- | ------------------------------------------------------------------------- | +| **Factory** | `ProviderFactory`, `RagProviderFactory`, `CompressionFactory`, `Chairman` | +| **Strategy** | `IDebateStrategy`, `IContextCompressor` | +| **Builder** | Fluent API `CouncilBuilder` | +| **Template Method** | Абстрактный базовый класс `DebateScenario` | +| **Cache** | `CompressionCache` с ключами SHA-256 | +| **Observer** | Событие `OnRoundCompleted` у `CouncilExecutor` | + +--- + +## 📁 Структура выходных файлов + +Каждое обсуждение можно экспортировать как один файл или как три отдельных Markdown-документа: + +```csharp +var result = await executor.ExecuteAsync(); + +// Сохранить в 3 отдельных файла +var (resultPath, statsPath, logsPath) = await result.SaveAllAsync("./output"); +// Создаёт: debate_20260604_120000_result.md +// debate_20260604_120000_statistics.md +// debate_20260604_120000_logs.md + +// Или сохранить по отдельности +await result.SaveToMarkdownAsync("result.md"); +await result.SaveStatisticsAsync("statistics.md"); +await result.SaveLogsAsync("logs.md"); +``` + +| Файл | Содержимое | +| ----------------- | ----------------------------------------------------------------------------- | +| `*_result.md` | Полная стенограмма обсуждения, раунды и финальный вердикт председателя | +| `*_statistics.md` | Статистика использования токенов с разбивкой по раундам | +| `*_logs.md` | Журналы выполнения (`ExecutionLog`) для Chairman, KK, сжатия и участников | + +--- + +## 💻 Примеры ConsoleApp + +В репозитории есть [`Delibera.ConsoleApp`](src/Delibera.ConsoleApp/) — запускаемый демо-проект, +который задействует каждую возможность. Запускайте его из корня репозитория: + +```bash +# Клонировать репозиторий +git clone https://github.com/delibera/Delibera.git +cd Delibera/src/Delibera.ConsoleApp + +# Запустить конкретный пример +dotnet run -- --di # Dependency Injection +dotnet run -- --separate-files # Сохранить result.md, statistics.md, logs.md +dotnet run -- --compression # Демо сжатия контекста +dotnet run -- --multiprovider # Совет с несколькими провайдерами (cloud + local) +dotnet run -- --rag # RAG на базе Qdrant с Knowledge Keeper +dotnet run -- --pgvector # RAG на базе pgvector +dotnet run -- --operator # Основы роли Operator (MCP-инструменты) +dotnet run -- --operator-mcp # 🆕 Operator с MCP-серверами browser + Marp + +# Или запустить полное демо по умолчанию (читает appsettings.json) +dotnet run +``` + +Консольное приложение читает [`appsettings.json`](src/Delibera.ConsoleApp/appsettings.json), который +использует секцию `DeliberaApp` — по умолчанию она указывает на Ollama Cloud и показывает, как +подключить несколько провайдеров, RAG и сжатие в одном месте. + +--- + +## 🛠️ Установка и сборка + +### Клонирование и сборка + +```bash +git clone https://github.com/delibera/Delibera.git +cd Delibera + +# Собрать всё решение +dotnet build --configuration Release + +# Запустить консольное демо +cd src/Delibera.ConsoleApp +dotnet run +``` + +### Инфраструктура одной командой (Docker Compose) + +Поднимите **Qdrant + PostgreSQL/pgvector** за один шаг. (Ollama намеренно не включена — +[установите её нативно](https://ollama.com/download), чтобы GPU-драйвер использовался напрямую.) + +```bash +# Из корня репозитория +docker compose up -d + +# В другом терминале запустите консольное приложение +cd src/Delibera.ConsoleApp +dotnet run +``` + +Compose-стек предоставляет: + +| Сервис | URL | Назначение | +| ------------- | -------------------------- | ----------------------------- | +| Qdrant (REST) | `http://localhost:6333` | UI / REST векторного хранилища | +| Qdrant (gRPC) | `localhost:6334` | gRPC-клиент (используется Delibera) | +| PostgreSQL | `localhost:5432` | RAG-хранилище pgvector | + +Учётные данные по умолчанию: `postgres` / `postgres`, база `council_vectors`. + +> Если эти сервисы уже запущены нативно, просто пропустите `docker compose` и направьте консольное +> приложение на них — `appsettings.json` по умолчанию использует `localhost`. + +### Альтернатива вручную (по одному контейнеру) + +```bash +# Запуск с Qdrant (Docker) +docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant + +# Запуск с pgvector (Docker) +docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg16 + +# Затем включите расширение pgvector +docker exec -it psql -U postgres -d council_vectors -c "CREATE EXTENSION IF NOT EXISTS vector;" +``` + +### NuGet-зависимости + +| Пакет | Назначение | +| ------------------------ | -------------------------------- | +| `OllamaSharp` | Клиент API Ollama | +| `Qdrant.Client` | gRPC-клиент векторной БД Qdrant | +| `Npgsql` | ADO.NET-провайдер PostgreSQL | +| `Pgvector` | Поддержка типов pgvector для Npgsql | +| `ModelContextProtocol` | MCP-клиент для роли Operator | +| `Microsoft.Extensions.*` | Конфигурация, DI и Options | + +--- + +## 🏛️ Архитектура + +``` +Delibera.Core +├── Council/ ← CouncilBuilder, CouncilExecutor, Chairman, KnowledgeKeeper, Operator +├── Debate/ ← StandardDebate, CritiqueDebate, ConsensusDebate +├── Compression/ ← Semantic / Deduplication / Summarization / Hybrid +├── Providers/ +│ ├── LLM/ ← OllamaProvider, OllamaEmbeddingProvider +│ ├── RAG/ ← QdrantRagProvider, PgVectorRagProvider +│ └── Mcp/ ← McpClientAdapter (Operator ↔ MCP-серверы) +├── DependencyInjection/ ← AddDelibera() + CouncilOptions +├── Knowledge/ ← MarkdownKnowledgeBase +├── Models/ ← CouncilMember, DebateResult, DebateRound, TokenStatistics, ... +└── Interfaces/ ← ILLMProvider, IRagProvider, IContextCompressor, IOperator, IMcpClient, ... +``` + +--- + +## 🤝 Участие в разработке + +Мы приветствуем вклад! Пожалуйста, прочитайте [CONTRIBUTING.md](CONTRIBUTING.md) для рекомендаций по +настройке окружения разработки, стандартам кодирования и процессу pull-request. + +--- + +## 📄 Лицензия + +Delibera распространяется под [лицензией MIT](LICENSE). + +Copyright © 2026 Delibera Project. + +--- + +
+ +**⚖️ Delibera — Продуманные решения с помощью ИИ** + +*Создано с заботой о коллективном интеллекте на базе ИИ* + +
diff --git a/README.md b/README.md index 5ec7ad6..86caed7 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,14 @@ ### ⚖️ Thoughtful AI Decisions -**Collective decision making through structured AI deliberation — with RAG, pgvector, Knowledge Keeper, Chairman, 🔥 Context Compression, 💉 Dependency Injection & 📋 Execution Logging** +**Collective decision making through structured AI deliberation — with RAG, pgvector, Knowledge Keeper, 🛠️ Operator (MCP tools), Chairman, 🔥 Context Compression, 💉 Dependency Injection & 📋 Execution Logging** [![NuGet](https://img.shields.io/nuget/v/Delibera.Core.svg)](https://www.nuget.org/packages/Delibera.Core) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](LICENSE) [![.NET 10](https://img.shields.io/badge/.NET-10.0-1F2937.svg)](https://dotnet.microsoft.com) -[![C# 14](https://img.shields.io/badge/C%23-14.0-239120.svg)](https://learn.microsoft.com/dotnet/csharp/) +[![C# 15](https://img.shields.io/badge/C%23-15.0--preview-239120.svg)](https://learn.microsoft.com/dotnet/csharp/) + +🇷🇺 [Русская версия (README-RU.md)](README-RU.md) @@ -38,13 +40,14 @@ well-reasoned outcomes** rather than single-model guesses. | **🏛️ Multi-Model Councils** | Orchestrate any number of LLM participants across structured debate rounds | | **⚖️ Chairman Synthesis** | A dedicated moderator opens, regulates, and synthesises the final verdict | | **📚 Knowledge Keeper (RAG)** | Per-round semantic retrieval with structured, cited responses | +| **🛠️ Operator (MCP Tools)** | A micro-agent that delegates tasks to MCP servers (web, files, Marp, Notion, …) on demand during the debate | | **🐘 Qdrant + pgvector** | Pluggable vector stores — use a dedicated DB or your existing PostgreSQL | | **🗜️ Context Compression** | 4 strategies (Semantic, Deduplication, Summarization, Hybrid) save 30–70% of tokens | | **💉 Dependency Injection** | `AddDelibera()` extension for `IServiceCollection` with full options binding | | **📋 Execution Logging** | `ExecutionLog` model with `LogLevel` — Chairman, KK, Compression & participant events | | **📁 Separate File Output** | Export `result.md`, `statistics.md`, and `logs.md` independently | | **🔌 Interface-First** | Clean abstractions for providers, factories, builders and executors | -| **🧱 Modern C# 12** | File-scoped namespaces, records, init-only properties, global usings | +| **🧱 Modern C# 15 (preview)** | Built on .NET 10 with `LangVersion=preview`, file-scoped namespaces, records, span/SIMD hot paths | --- @@ -56,6 +59,7 @@ well-reasoned outcomes** rather than single-model guesses. - [Minimal Example](#minimal-example) - [Run](#run) - [Dependency Injection](#-dependency-injection) +- [Operator (MCP Tools)](#️-operator-mcp-tools) - [Context Compression](#-context-compression) - [RAG Integration](#-rag-integration) - [Debate Strategies](#-debate-strategies) @@ -72,10 +76,14 @@ well-reasoned outcomes** rather than single-model guesses. ### Prerequisites & Models -- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (≥ 10.0.301) — the project targets + `net10.0` and builds with `LangVersion=preview` to enable **C# 15** features. See + [docs/NET10-Upgrade.md](docs/NET10-Upgrade.md) for the full migration notes. - A running [Ollama](https://ollama.com) instance — local (`ollama serve`) or [Ollama Cloud](https://ollama.com/cloud) (just an API key, no install). - The minimal set of models listed below. +- **Optional — for the Operator role:** [Node.js + npx](https://nodejs.org) to launch MCP servers + (e.g. `@playwright/mcp`, `@marp-team/marp-cli`). See [Operator (MCP Tools)](#️-operator-mcp-tools). #### 🟢 Minimal — for a tiny debate (≈ 2 GB total) @@ -234,6 +242,156 @@ console app — see [ConsoleApp Examples](#-consoleapp-examples)). --- +## 🛠️ Operator (MCP Tools) + +The **Operator** is a lightweight micro-agent that connects the council to the outside world through +[**MCP (Model Context Protocol)**](https://modelcontextprotocol.io) servers. It exposes whatever tools +those servers provide — web browsing, file system access, Marp presentation generation, Notion, +PostgreSQL, etc. — to the debate participants. + +**How it works** + +1. The Operator connects to one or more MCP servers and discovers their tools on `InitializeAsync`. +2. Participants are told (in their system prompt) what the Operator can do, and can delegate a task at + **any moment** during the debate by writing a marker in their message: + + ``` + [[OPERATOR: open https://modelcontextprotocol.io and summarize what MCP is]] + ``` + +3. The Operator interprets the request with its **own (cheaper) LLM model**, picks and calls the right + MCP tools, interprets the results, and returns a concise answer that is injected into the next round. +4. If the council uses **context compression**, the Operator can reuse the same strategy to compress + large tool outputs before they re-enter the debate. Its `DisposeAsync` is a `ValueTask` (.NET 10 + `IAsyncDisposable`) so MCP clients are released without a `Task` allocation. + +All Operator interactions are recorded per round and rendered in the final Markdown report under a +**🛠️ Operator Interactions** block. + +### Configuring MCP servers + +```csharp +using Delibera.Core.Models; + +var servers = new[] +{ + // stdio transport — launches a local MCP server process + McpServerConfig.Stdio( + name: "browser", + command: "npx", + arguments: new[] { "-y", "@playwright/mcp@latest", "--headless" }), + + McpServerConfig.Stdio( + name: "marp", + command: "npx", + arguments: new[] { "-y", "@marp-team/marp-cli", "--server", "./out" }), + + // …or an HTTP/SSE transport for a remote MCP server + // McpServerConfig.Http( + // name: "remote", + // endpoint: "https://my-mcp-host.example.com/mcp", + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }), +}; +``` + +| Server | Transport | Launch command | What it provides | +| ------------ | --------- | ----------------------------------------------- | ---------------------------------------------------- | +| 🌐 `browser` | stdio | `npx -y @playwright/mcp@latest --headless` | Site navigation, page reading, clicks, screenshots | +| 🎯 `marp` | stdio | `npx -y @marp-team/marp-cli --server ` | Generating presentations (HTML/PDF/PPTX) from Markdown | + +### Quick usage (inside a council) + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Providers.LLM; + +var ollama = new OllamaProvider("http://localhost:11434"); + +var council = new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Optimist") + .AddMember("qwen2.5:7b", ollama, "Skeptic") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + // Operator uses its own cheaper model; reuseCompression shares the council's compressor + .WithOperator("llama3.2:3b", ollama, servers, reuseCompression: true) + .WithStandardDebate() + .WithUserPrompt("Research the latest .NET 10 features and prepare a short summary.") + .WithMaxRounds(4) + .Build() + .ExecuteAsync(); +``` + +Prefer to build the `Operator` yourself? Pass a pre-built instance: + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Interfaces; +using Delibera.Core.Models; +using Delibera.Core.Providers.Mcp; + +var @operator = new Operator( + new CouncilMember("llama3.2:3b", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }, + compressor: null, // optional IContextCompressor + compressionOptions: null); // optional CompressionOptions + +var council = new CouncilBuilder() + /* …members… */ + .WithOperator(@operator) + .Build(); +``` + +### Direct usage (without a council) + +```csharp +await using var @operator = new Operator( + new CouncilMember("llama3.2:3b", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }); + +await @operator.InitializeAsync(); + +var result = await @operator.ExecuteTaskAsync( + "Open https://modelcontextprotocol.io and briefly summarize what MCP is."); + +Console.WriteLine(result.FinalAnswer); +``` + +### Dependency Injection + +Configure the Operator declaratively in `appsettings.json` under `Delibera:Operator`: + +```json +{ + "Delibera": { + "Operator": { + "Enabled": true, + "ModelName": "llama3.2:3b", + "ReuseCompression": true, + "McpServers": [ + { + "Name": "browser", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@playwright/mcp@latest", "--headless" ] + }, + { + "Name": "remote", + "Transport": "Http", + "Endpoint": "https://my-mcp-host.example.com/mcp", + "AdditionalHeaders": { "Authorization": "Bearer " } + } + ] + } + } +} +``` + +> ▶️ A complete, runnable demo lives in +> [`OperatorMcpToolsExample.cs`](src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs). +> Run it with `dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp`. +> For the full technical write-up, see [docs/NET10-Upgrade.md](docs/NET10-Upgrade.md). + +--- + ## 🗜️ Context Compression Automatically compress context between deliberation rounds — save **30–70% of tokens** without losing meaning. @@ -401,6 +559,8 @@ dotnet run -- --compression # Context compression demo dotnet run -- --multiprovider # Multi-provider (cloud + local) council dotnet run -- --rag # Qdrant-backed RAG with Knowledge Keeper dotnet run -- --pgvector # pgvector-backed RAG +dotnet run -- --operator # Operator role basics (MCP tools) +dotnet run -- --operator-mcp # 🆕 Operator with browser + Marp MCP servers # Or run the full default demo (reads appsettings.json) dotnet run @@ -476,6 +636,7 @@ docker exec -it psql -U postgres -d council_vectors -c "CREATE EXTEN | `Qdrant.Client` | Qdrant vector DB gRPC client | | `Npgsql` | PostgreSQL ADO.NET provider | | `Pgvector` | pgvector type support for Npgsql | +| `ModelContextProtocol` | MCP client for the Operator role | | `Microsoft.Extensions.*` | Configuration, DI and Options | --- @@ -484,16 +645,17 @@ docker exec -it psql -U postgres -d council_vectors -c "CREATE EXTEN ``` Delibera.Core -├── Council/ ← CouncilBuilder, CouncilExecutor, Chairman, KnowledgeKeeper +├── Council/ ← CouncilBuilder, CouncilExecutor, Chairman, KnowledgeKeeper, Operator ├── Debate/ ← StandardDebate, CritiqueDebate, ConsensusDebate ├── Compression/ ← Semantic / Deduplication / Summarization / Hybrid ├── Providers/ │ ├── LLM/ ← OllamaProvider, OllamaEmbeddingProvider -│ └── RAG/ ← QdrantRagProvider, PgVectorRagProvider +│ ├── RAG/ ← QdrantRagProvider, PgVectorRagProvider +│ └── Mcp/ ← McpClientAdapter (Operator ↔ MCP servers) ├── DependencyInjection/ ← AddDelibera() + CouncilOptions ├── Knowledge/ ← MarkdownKnowledgeBase ├── Models/ ← CouncilMember, DebateResult, DebateRound, TokenStatistics, ... -└── Interfaces/ ← ILLMProvider, IRagProvider, IContextCompressor, ... +└── Interfaces/ ← ILLMProvider, IRagProvider, IContextCompressor, IOperator, IMcpClient, ... ``` --- diff --git a/docs/NET10-Upgrade-RU.docx b/docs/NET10-Upgrade-RU.docx new file mode 100644 index 0000000000000000000000000000000000000000..6daba5ddac0f7c453d4fbdeb5e0637953e021937 GIT binary patch literal 20175 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 literal 0 HcmV?d00001 diff --git a/docs/NET10-Upgrade-RU.md b/docs/NET10-Upgrade-RU.md new file mode 100644 index 0000000..aaa2a59 --- /dev/null +++ b/docs/NET10-Upgrade-RU.md @@ -0,0 +1,301 @@ +# Delibera — обновление до .NET 10 и C# 15 + +Документ описывает изменения, внесённые при переводе проекта **Delibera** на платформу +**.NET 10** и язык **C# 15 (preview)**, оптимизации производительности и новые примеры работы +роли **Operator** с MCP-инструментами (браузер и генерация Marp-презентаций). + +> Все изменения выполнены в ветке `feature/operator-mcp-role`. Проект собирается командой +> `dotnet build` без предупреждений и ошибок. + +--- + +## Содержание + +1. [Целевая платформа и версия языка](#1-целевая-платформа-и-версия-языка) +2. [Обновление зависимостей](#2-обновление-зависимостей) +3. [Оптимизации производительности (high performance)](#3-оптимизации-производительности-high-performance) +4. [Примеры Operator + MCP-инструменты](#4-примеры-operator--mcp-инструменты) +5. [Сборка и запуск](#5-сборка-и-запуск) +6. [Краткая сводка изменённых файлов](#6-краткая-сводка-изменённых-файлов) + +--- + +## 1. Целевая платформа и версия языка + +Оба проекта переведены на единый стек: + +| Свойство | Было | Стало | +|---------------------|-------------|----------------| +| `TargetFramework` | `net10.0` | `net10.0` | +| `LangVersion` | `14.0` | `preview` *(C# 15)* | +| `Nullable` | `enable` | `enable` | +| `ImplicitUsings` | `enable` | `enable` | + +Изменения внесены в файлы: + +- `src/Delibera.Core/Delibera.Core.csproj` +- `src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj` + +### ⚠️ Важно про «C# 15» + +На момент обновления установлен GA-релиз **.NET SDK 10.0.301**, в котором компилятор Roslyn +поддерживает версии языка до **14.0** включительно, а следующая (будущая C# 15) доступна +**только** через значение `preview`. Прямое указание `15.0` приводит +к ошибке компиляции: + +``` +error CS1617: Invalid option '15.0' for /langversion +``` + +Поэтому используется `preview` — это включает максимально доступный +набор языковых возможностей следующей версии (C# 15). Когда выйдет SDK с официальной поддержкой +`15.0`, значение можно будет заменить на `15.0` без других изменений в коде. + +--- + +## 2. Обновление зависимостей + +Все NuGet-пакеты приведены к версиям, совместимым с .NET 10. Большинство пакетов уже находились +на актуальных версиях для .NET 10; обновлён `Microsoft.SourceLink.GitHub`. + +### Delibera.Core + +| Пакет | Версия | Назначение | +|--------------------------------------------------------|-----------|---------------------------------------| +| `Microsoft.Extensions.Configuration.Abstractions` | 10.0.9 | Конфигурация (DI) | +| `Microsoft.Extensions.Configuration.Json` | 10.0.9 | Чтение `appsettings.json` | +| `Microsoft.Extensions.DependencyInjection` | 10.0.9 | Контейнер DI | +| `Microsoft.Extensions.DependencyInjection.Abstractions`| 10.0.9 | Абстракции DI | +| `Microsoft.Extensions.Options` | 10.0.9 | Паттерн Options | +| `Microsoft.Extensions.Options.ConfigurationExtensions` | 10.0.9 | Привязка Options к конфигурации | +| `ModelContextProtocol` | 1.4.0 | SDK для MCP-серверов (роль Operator) | +| `Npgsql` | 10.0.3 | PostgreSQL / pgvector | +| `OllamaSharp` | 5.4.25 | LLM-провайдер Ollama | +| `Pgvector` | 0.3.2 | Векторный поиск в PostgreSQL | +| `Qdrant.Client` | 1.18.1 | Векторное хранилище Qdrant | +| `Microsoft.SourceLink.GitHub` | **10.0.300** *(было 10.0.102)* | Source Link для отладки | + +### Delibera.ConsoleApp + +| Пакет | Версия | Назначение | +|------------------------------------------------------|--------|-----------------------------| +| `Microsoft.Extensions.Configuration` | 10.0.9 | Конфигурация | +| `Microsoft.Extensions.Configuration.Binder` | 10.0.9 | Привязка конфигурации | +| `Microsoft.Extensions.Configuration.Json` | 10.0.9 | JSON-конфигурация | +| `Microsoft.Extensions.Configuration.UserSecrets` | 10.0.9 | User Secrets | +| `Microsoft.Extensions.DependencyInjection` | 10.0.9 | Контейнер DI | + +> Проверка актуальности выполнена через `dotnet list package --outdated` и NuGet flat-container +> API — на момент обновления более свежих стабильных версий для перечисленных пакетов нет. + +--- + +## 3. Оптимизации производительности (high performance) + +Внесены целенаправленные оптимизации «горячих» путей с применением `Span`/`ReadOnlySpan`, +SIMD (`System.Numerics.Vector`) и `ArrayPool`. Все оптимизации сохраняют прежнее поведение +(проверено численно). + +### 3.1. SIMD-векторизация косинусной близости + +**Файл:** `src/Delibera.Core/Compression/SemanticCompressor.cs` → `CosineSimilarity` + +Косинусная близость — самый «горячий» цикл при семантическом сжатии и дедупликации (вызывается +для каждой пары предложений по всем измерениям эмбеддинга, обычно 768–1536). Реализация переписана: + +- Сигнатура изменена с `float[]` на `ReadOnlySpan` — это zero-copy и позволяет передавать + массивы, срезы и `stackalloc`-буферы без аллокаций (вызывающий код с `float[]` работает без изменений). +- Основной цикл векторизован через `Vector`: за одну итерацию обрабатывается + `Vector.Count` элементов (на современных x64/ARM64 — 8–16 значений за такт). +- Скалярный «хвост» обрабатывает остаток, а также служит запасным путём при отсутствии аппаратного + ускорения (`Vector.IsHardwareAccelerated == false`). + +```csharp +internal static double CosineSimilarity(ReadOnlySpan a, ReadOnlySpan b) +{ + if (a.Length != b.Length || a.IsEmpty) return 0; + + float dot = 0, magA = 0, magB = 0; + var i = 0; + + if (Vector.IsHardwareAccelerated && a.Length >= Vector.Count) + { + var dotAcc = Vector.Zero; + var magAAcc = Vector.Zero; + var magBAcc = Vector.Zero; + var width = Vector.Count; + + for (; i <= a.Length - width; i += width) + { + var va = new Vector(a.Slice(i, width)); + var vb = new Vector(b.Slice(i, width)); + dotAcc += va * vb; + magAAcc += va * va; + magBAcc += vb * vb; + } + + dot = Vector.Dot(dotAcc, Vector.One); + magA = Vector.Dot(magAAcc, Vector.One); + magB = Vector.Dot(magBAcc, Vector.One); + } + + for (; i < a.Length; i++) { dot += a[i]*b[i]; magA += a[i]*a[i]; magB += b[i]*b[i]; } + + var denom = Math.Sqrt(magA) * Math.Sqrt(magB); + return denom > 0 ? dot / denom : 0; +} +``` + +**Корректность.** Численное сравнение с эталонной (скалярной) реализацией на длинах +`1, 3, 7, 8, 16, 17, 100, 1536` дало максимальную абсолютную погрешность ≈ `6·10⁻⁸` +(на уровне точности `float`), результат идентичен. + +### 3.2. Подсчёт токенов без аллокаций (`ReadOnlySpan`) + +**Файл:** `src/Delibera.Core/Compression/TokenCounter.cs` + +- Добавлена перегрузка `EstimateTokens(ReadOnlySpan text)` — позволяет оценивать токены + для срезов строк без выделения подстрок. +- Внутренний `CountWords` принимает `ReadOnlySpan` и перебирает символы по `ref struct` + enumerator'у — без боксинга и без копий. +- Существующий `EstimateTokens(string?)` сохранён и теперь делегирует в span-перегрузку + (через `text.AsSpan()`), поэтому публичный API полностью обратно совместим. + +### 3.3. Хеширование ключа кэша через `ArrayPool` + `stackalloc` + +**Файл:** `src/Delibera.Core/Compression/CompressionCache.cs` → `ComputeKey` + +Раньше построение ключа кэша создавало две лишние кучи-аллокации на каждый вызов: +интерполированную строку `"{strategy}:{text}"` и массив байтов из `Encoding.UTF8.GetBytes`. +Новая реализация: + +- Арендует буфер символов из `ArrayPool.Shared`, склеивает в него `strategy + ':' + text`. +- Арендует буфер байтов из `ArrayPool.Shared` и кодирует UTF-8 напрямую в него + (`Encoding.UTF8.GetBytes(span, span)`). +- Считает SHA-256 в стек-буфер `stackalloc byte[SHA256.HashSizeInBytes]` и форматирует hex. +- Гарантированно возвращает арендованные буферы в `finally`. + +Итог: на «тёплом» пути кэширования сжатия исключены промежуточные аллокации больших строк/массивов, +что снижает нагрузку на GC при больших объёмах контекста дебатов. + +### 3.4. `ValueTask` в роли Operator + +Метод освобождения ресурсов Operator реализован как `public async ValueTask DisposeAsync()` +(`src/Delibera.Core/Council/Operator.cs`) — стандартный для .NET 10 паттерн `IAsyncDisposable`, +позволяющий избежать аллокации `Task` на пути освобождения MCP-клиентов. Новый пример +(`OperatorMcpToolsExample`) использует `await using`, корректно задействуя этот путь. + +### Сводка оптимизаций + +| Приём | Где применён | Эффект | +|---------------------|------------------------------------------------|---------------------------------------------| +| `ReadOnlySpan` + SIMD | `SemanticCompressor.CosineSimilarity` | 8–16× ширина обработки, без копий | +| `ReadOnlySpan`| `TokenCounter` (подсчёт слов/токенов) | Нет аллокаций подстрок | +| `ArrayPool` + `stackalloc` | `CompressionCache.ComputeKey` | −2 кучи-аллокации на вызов, меньше нагрузки на GC | +| `ValueTask` | `Operator.DisposeAsync` | Освобождение без аллокации `Task` | + +--- + +## 4. Примеры Operator + MCP-инструменты + +Добавлен новый пример **`OperatorMcpToolsExample`** +(`src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs`), демонстрирующий работу роли +**Operator** с реальными MCP-серверами. + +### Подключаемые MCP-серверы + +| Сервер | Транспорт | Команда запуска | Что даёт | +|------------|-----------|------------------------------------------------------------|-----------------------------------------------------| +| 🌐 `browser` | stdio | `npx -y @playwright/mcp@latest --headless` | Навигация по сайтам, чтение страниц, клики, скриншоты | +| 🎯 `marp` | stdio | `npx -y @marp-team/marp-cli --server ` | Генерация презентаций (HTML/PDF/PPTX) из Markdown | + +> Также в примере показан (закомментирован) HTTP/SSE-вариант через `McpServerConfig.Http(...)` +> для удалённых MCP-серверов с заголовками авторизации. + +### Что демонстрирует пример + +**Раздел A — прямой вызов Operator** (без совета): + +- Ручное создание MCP-клиентов (`McpClientAdapter`) и объекта `Operator`. +- `await using` для корректного освобождения через `ValueTask DisposeAsync`. +- `InitializeAsync()` — подключение к серверам и обнаружение инструментов. +- Две задачи через `ExecuteTaskAsync`: + 1. **Браузер:** открыть `https://modelcontextprotocol.io` и кратко пересказать, что такое MCP. + 2. **Marp:** сгенерировать презентацию из 3 слайдов и сохранить как `deck.html`. + +**Раздел B — Operator внутри совета** (делегирование маркером `[[OPERATOR: ...]]`): + +- Построение совета через `CouncilBuilder` с участниками, председателем (`Chairman`) и Operator'ом + (удобная перегрузка `WithOperator(modelName, provider, servers)`). +- Подписка на `OnRoundCompleted` для вывода активности Operator по раундам. +- Сохранение Markdown-отчёта дебатов и готовых презентаций. + +### Запуск примера + +```bash +# из каталога проекта +dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp +``` + +#### Предварительные требования + +- **Node.js + npx** — нужны для запуска npx-MCP-серверов (`@playwright/mcp`, `@marp-team/marp-cli`). +- Для браузера при первом запуске Playwright скачает движки: `npx playwright install`. +- **Ollama** запущен локально (`ollama serve`) с моделями `llama3.2`, `qwen2.5` + (модель Operator можно заменить на любую «дешёвую»). + +> Пример устойчив к отсутствию окружения: при ошибке выводятся понятные подсказки на русском, +> процесс не падает аварийно. + +--- + +## 5. Сборка и запуск + +```bash +# Сборка обоих проектов +dotnet build + +# Сборка в Release +dotnet build -c Release + +# Запуск консольного приложения +dotnet run --project src/Delibera.ConsoleApp + +# Доступные демонстрации (флаги): +dotnet run --project src/Delibera.ConsoleApp -- --operator # базовый пример Operator +dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp # 🆕 браузер + Marp +dotnet run --project src/Delibera.ConsoleApp -- --compression # сжатие контекста +dotnet run --project src/Delibera.ConsoleApp -- --rag # RAG +dotnet run --project src/Delibera.ConsoleApp -- --di # Dependency Injection +``` + +Результат сборки: + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +> **Примечание о localhost.** В примерах используется адрес `http://localhost:11434` (Ollama) и +> локальные пути для вывода презентаций. Это localhost той машины, где запускается приложение +> (среда агента Abacus AI), а не вашего компьютера. Чтобы запустить локально, скачайте файлы через +> иконку «Files», перейдите в скачанную папку, разверните приложение на своей системе и запустите его. + +--- + +## 6. Краткая сводка изменённых файлов + +| Файл | Изменение | +|-----------------------------------------------------------------|--------------------------------------------------------------| +| `src/Delibera.Core/Delibera.Core.csproj` | `LangVersion=preview`, `SourceLink → 10.0.300` | +| `src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj` | `LangVersion=preview` | +| `src/Delibera.Core/Compression/SemanticCompressor.cs` | SIMD-векторизация `CosineSimilarity` + `ReadOnlySpan` | +| `src/Delibera.Core/Compression/TokenCounter.cs` | Перегрузка `ReadOnlySpan`, span-подсчёт слов | +| `src/Delibera.Core/Compression/CompressionCache.cs` | `ArrayPool` + `stackalloc` в `ComputeKey` | +| `src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs` | 🆕 Пример Operator с браузером и Marp | +| `src/Delibera.ConsoleApp/Program.cs` | Флаг `--operator-mcp`, исправлено nullable-предупреждение | + +--- + +*Документ подготовлен в рамках перевода Delibera на .NET 10 / C# 15.* diff --git a/docs/NET10-Upgrade-RU.pdf b/docs/NET10-Upgrade-RU.pdf new file mode 100644 index 0000000000000000000000000000000000000000..26f7eec79bf3337024c6a60f368f788f499f7af3 GIT binary patch literal 112771 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

GxNp>Uitq1L~|A}WFO!K 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 literal 0 HcmV?d00001 diff --git a/docs/NET10-Upgrade.docx b/docs/NET10-Upgrade.docx new file mode 100644 index 0000000000000000000000000000000000000000..70a657774037bf0c3a2b40af44677bf4172f8b35 GIT binary patch literal 18738 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 All changes were made in the `feature/operator-mcp-role` branch. The project builds with +> `dotnet build` without warnings or errors. + +--- + +## Table of Contents + +1. [Target Platform and Language Version](#1-target-platform-and-language-version) +2. [Dependency Updates](#2-dependency-updates) +3. [Performance Optimizations (high performance)](#3-performance-optimizations-high-performance) +4. [Operator + MCP Tools Examples](#4-operator--mcp-tools-examples) +5. [Build and Run](#5-build-and-run) +6. [Summary of Changed Files](#6-summary-of-changed-files) + +--- + +## 1. Target Platform and Language Version + +Both projects were migrated to a single stack: + +| Property | Before | After | +|---------------------|-------------|----------------| +| `TargetFramework` | `net10.0` | `net10.0` | +| `LangVersion` | `14.0` | `preview` *(C# 15)* | +| `Nullable` | `enable` | `enable` | +| `ImplicitUsings` | `enable` | `enable` | + +The changes were made in the files: + +- `src/Delibera.Core/Delibera.Core.csproj` +- `src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj` + +### ⚠️ Important note about "C# 15" + +At the time of the upgrade, the GA release **.NET SDK 10.0.301** is installed, in which the Roslyn +compiler supports language versions up to and including **14.0**, while the next one (the future +C# 15) is available **only** through the `preview` value. Specifying `15.0` +directly results in a compilation error: + +``` +error CS1617: Invalid option '15.0' for /langversion +``` + +Therefore `preview` is used — this enables the maximum available set of +language features of the next version (C# 15). When an SDK with official `15.0` support is released, +the value can be replaced with `15.0` without any other changes to the code. + +--- + +## 2. Dependency Updates + +All NuGet packages were brought to versions compatible with .NET 10. Most packages were already on +their current versions for .NET 10; `Microsoft.SourceLink.GitHub` was updated. + +### Delibera.Core + +| Package | Version | Purpose | +|--------------------------------------------------------|-----------|---------------------------------------| +| `Microsoft.Extensions.Configuration.Abstractions` | 10.0.9 | Configuration (DI) | +| `Microsoft.Extensions.Configuration.Json` | 10.0.9 | Reading `appsettings.json` | +| `Microsoft.Extensions.DependencyInjection` | 10.0.9 | DI container | +| `Microsoft.Extensions.DependencyInjection.Abstractions`| 10.0.9 | DI abstractions | +| `Microsoft.Extensions.Options` | 10.0.9 | Options pattern | +| `Microsoft.Extensions.Options.ConfigurationExtensions` | 10.0.9 | Binding Options to configuration | +| `ModelContextProtocol` | 1.4.0 | SDK for MCP servers (Operator role) | +| `Npgsql` | 10.0.3 | PostgreSQL / pgvector | +| `OllamaSharp` | 5.4.25 | Ollama LLM provider | +| `Pgvector` | 0.3.2 | Vector search in PostgreSQL | +| `Qdrant.Client` | 1.18.1 | Qdrant vector store | +| `Microsoft.SourceLink.GitHub` | **10.0.300** *(was 10.0.102)* | Source Link for debugging | + +### Delibera.ConsoleApp + +| Package | Version | Purpose | +|------------------------------------------------------|---------|-----------------------------| +| `Microsoft.Extensions.Configuration` | 10.0.9 | Configuration | +| `Microsoft.Extensions.Configuration.Binder` | 10.0.9 | Configuration binding | +| `Microsoft.Extensions.Configuration.Json` | 10.0.9 | JSON configuration | +| `Microsoft.Extensions.Configuration.UserSecrets` | 10.0.9 | User Secrets | +| `Microsoft.Extensions.DependencyInjection` | 10.0.9 | DI container | + +> The currency check was performed via `dotnet list package --outdated` and the NuGet flat-container +> API — at the time of the upgrade there are no newer stable versions for the listed packages. + +--- + +## 3. Performance Optimizations (high performance) + +Targeted optimizations of the "hot" paths were made using `Span`/`ReadOnlySpan`, +SIMD (`System.Numerics.Vector`), and `ArrayPool`. All optimizations preserve the previous +behavior (verified numerically). + +### 3.1. SIMD vectorization of cosine similarity + +**File:** `src/Delibera.Core/Compression/SemanticCompressor.cs` → `CosineSimilarity` + +Cosine similarity is the hottest loop during semantic compression and deduplication (it is called +for every pair of sentences across all embedding dimensions, typically 768–1536). The implementation +was rewritten: + +- The signature was changed from `float[]` to `ReadOnlySpan` — this is zero-copy and allows + passing arrays, slices, and `stackalloc` buffers without allocations (calling code using `float[]` + works without changes). +- The main loop is vectorized via `Vector`: each iteration processes + `Vector.Count` elements (on modern x64/ARM64 — 8–16 values per cycle). +- The scalar "tail" handles the remainder and also serves as a fallback path when there is no + hardware acceleration (`Vector.IsHardwareAccelerated == false`). + +```csharp +internal static double CosineSimilarity(ReadOnlySpan a, ReadOnlySpan b) +{ + if (a.Length != b.Length || a.IsEmpty) return 0; + + float dot = 0, magA = 0, magB = 0; + var i = 0; + + if (Vector.IsHardwareAccelerated && a.Length >= Vector.Count) + { + var dotAcc = Vector.Zero; + var magAAcc = Vector.Zero; + var magBAcc = Vector.Zero; + var width = Vector.Count; + + for (; i <= a.Length - width; i += width) + { + var va = new Vector(a.Slice(i, width)); + var vb = new Vector(b.Slice(i, width)); + dotAcc += va * vb; + magAAcc += va * va; + magBAcc += vb * vb; + } + + dot = Vector.Dot(dotAcc, Vector.One); + magA = Vector.Dot(magAAcc, Vector.One); + magB = Vector.Dot(magBAcc, Vector.One); + } + + for (; i < a.Length; i++) { dot += a[i]*b[i]; magA += a[i]*a[i]; magB += b[i]*b[i]; } + + var denom = Math.Sqrt(magA) * Math.Sqrt(magB); + return denom > 0 ? dot / denom : 0; +} +``` + +**Correctness.** A numerical comparison with the reference (scalar) implementation on lengths +`1, 3, 7, 8, 16, 17, 100, 1536` yielded a maximum absolute error of ≈ `6·10⁻⁸` +(at the precision level of `float`); the result is identical. + +### 3.2. Allocation-free token counting (`ReadOnlySpan`) + +**File:** `src/Delibera.Core/Compression/TokenCounter.cs` + +- Added an `EstimateTokens(ReadOnlySpan text)` overload — it allows estimating tokens for + string slices without allocating substrings. +- The internal `CountWords` accepts `ReadOnlySpan` and iterates over characters using a + `ref struct` enumerator — without boxing and without copies. +- The existing `EstimateTokens(string?)` is preserved and now delegates to the span overload + (via `text.AsSpan()`), so the public API is fully backward compatible. + +### 3.3. Cache key hashing via `ArrayPool` + `stackalloc` + +**File:** `src/Delibera.Core/Compression/CompressionCache.cs` → `ComputeKey` + +Previously, building the cache key created two extra heap allocations on every call: +the interpolated string `"{strategy}:{text}"` and a byte array from `Encoding.UTF8.GetBytes`. +The new implementation: + +- Rents a character buffer from `ArrayPool.Shared` and concatenates `strategy + ':' + text` + into it. +- Rents a byte buffer from `ArrayPool.Shared` and encodes UTF-8 directly into it + (`Encoding.UTF8.GetBytes(span, span)`). +- Computes SHA-256 into a stack buffer `stackalloc byte[SHA256.HashSizeInBytes]` and formats hex. +- Guarantees that the rented buffers are returned in `finally`. + +Result: on the "warm" compression-cache path, intermediate allocations of large strings/arrays are +eliminated, which reduces GC pressure for large volumes of debate context. + +### 3.4. `ValueTask` in the Operator role + +The Operator resource-disposal method is implemented as `public async ValueTask DisposeAsync()` +(`src/Delibera.Core/Council/Operator.cs`) — the standard .NET 10 `IAsyncDisposable` pattern, which +avoids a `Task` allocation on the MCP-client disposal path. The new example +(`OperatorMcpToolsExample`) uses `await using`, correctly exercising this path. + +### Optimization summary + +| Technique | Where applied | Effect | +|---------------------|------------------------------------------------|---------------------------------------------| +| `ReadOnlySpan` + SIMD | `SemanticCompressor.CosineSimilarity` | 8–16× processing width, no copies | +| `ReadOnlySpan`| `TokenCounter` (word/token counting) | No substring allocations | +| `ArrayPool` + `stackalloc` | `CompressionCache.ComputeKey` | −2 heap allocations per call, less GC pressure | +| `ValueTask` | `Operator.DisposeAsync` | Disposal without a `Task` allocation | + +--- + +## 4. Operator + MCP Tools Examples + +A new example **`OperatorMcpToolsExample`** was added +(`src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs`), demonstrating the **Operator** role +working with real MCP servers. + +### Connected MCP servers + +| Server | Transport | Launch command | What it provides | +|------------|-----------|------------------------------------------------------------|-----------------------------------------------------| +| 🌐 `browser` | stdio | `npx -y @playwright/mcp@latest --headless` | Site navigation, page reading, clicks, screenshots | +| 🎯 `marp` | stdio | `npx -y @marp-team/marp-cli --server

` | Generating presentations (HTML/PDF/PPTX) from Markdown | + +> The example also shows (commented out) an HTTP/SSE variant via `McpServerConfig.Http(...)` +> for remote MCP servers with authorization headers. + +### What the example demonstrates + +**Section A — direct Operator call** (without a council): + +- Manual creation of MCP clients (`McpClientAdapter`) and an `Operator` object. +- `await using` for correct disposal via `ValueTask DisposeAsync`. +- `InitializeAsync()` — connecting to the servers and discovering tools. +- Two tasks via `ExecuteTaskAsync`: + 1. **Browser:** open `https://modelcontextprotocol.io` and briefly summarize what MCP is. + 2. **Marp:** generate a 3-slide presentation and save it as `deck.html`. + +**Section B — Operator inside a council** (delegation via the `[[OPERATOR: ...]]` marker): + +- Building a council via `CouncilBuilder` with participants, a chairman (`Chairman`), and an Operator + (the convenient `WithOperator(modelName, provider, servers)` overload). +- Subscribing to `OnRoundCompleted` to print Operator activity per round. +- Saving the debate Markdown report and the generated presentations. + +### Running the example + +```bash +# from the project directory +dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp +``` + +#### Prerequisites + +- **Node.js + npx** — required to launch the npx MCP servers (`@playwright/mcp`, `@marp-team/marp-cli`). +- For the browser, on first run Playwright will download the engines: `npx playwright install`. +- **Ollama** running locally (`ollama serve`) with the models `llama3.2`, `qwen2.5` + (the Operator model can be replaced with any "cheap" one). + +> The example is resilient to a missing environment: on error it prints clear hints, and the +> process does not crash. + +--- + +## 5. Build and Run + +```bash +# Build both projects +dotnet build + +# Build in Release +dotnet build -c Release + +# Run the console application +dotnet run --project src/Delibera.ConsoleApp + +# Available demos (flags): +dotnet run --project src/Delibera.ConsoleApp -- --operator # basic Operator example +dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp # 🆕 browser + Marp +dotnet run --project src/Delibera.ConsoleApp -- --compression # context compression +dotnet run --project src/Delibera.ConsoleApp -- --rag # RAG +dotnet run --project src/Delibera.ConsoleApp -- --di # Dependency Injection +``` + +Build result: + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +> **Note about localhost.** The examples use the address `http://localhost:11434` (Ollama) and +> local paths for presentation output. This is the localhost of the machine where the application +> runs (the Abacus AI Agent environment), not your computer. To run it locally, download the files +> via the "Files" icon, go into the downloaded folder, deploy the application on your own system, +> and run it. + +--- + +## 6. Summary of Changed Files + +| File | Change | +|-----------------------------------------------------------------|--------------------------------------------------------------| +| `src/Delibera.Core/Delibera.Core.csproj` | `LangVersion=preview`, `SourceLink → 10.0.300` | +| `src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj` | `LangVersion=preview` | +| `src/Delibera.Core/Compression/SemanticCompressor.cs` | SIMD vectorization of `CosineSimilarity` + `ReadOnlySpan` | +| `src/Delibera.Core/Compression/TokenCounter.cs` | `ReadOnlySpan` overload, span-based word counting | +| `src/Delibera.Core/Compression/CompressionCache.cs` | `ArrayPool` + `stackalloc` in `ComputeKey` | +| `src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs` | 🆕 Operator example with browser and Marp | +| `src/Delibera.ConsoleApp/Program.cs` | `--operator-mcp` flag, fixed nullable warning | + +--- + +*Document prepared as part of the Delibera upgrade to .NET 10 / C# 15.* diff --git a/docs/NET10-Upgrade.pdf b/docs/NET10-Upgrade.pdf new file mode 100644 index 0000000000000000000000000000000000000000..82103f111ebf0b86705af90ad913449c16647b07 GIT binary patch literal 104010 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%0AWd@HkQD3c_*vb*^>clT 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 literal 0 HcmV?d00001 diff --git a/docs/QuickStart-RU.md b/docs/QuickStart-RU.md new file mode 100644 index 0000000..70918c1 --- /dev/null +++ b/docs/QuickStart-RU.md @@ -0,0 +1,451 @@ +
+ +Delibera + +# Delibera — Быстрый старт + +### ⚖️ Продуманные решения с помощью ИИ + +🇬🇧 [English version (QuickStart.md)](QuickStart.md) + +
+ +Это руководство за несколько минут проведёт вас через ваше первое обсуждение с помощью ИИ в +**Delibera**. По его завершении у вас будут запущенные локально многомодельные дебаты и +Markdown-стенограмма на диске. + +--- + +## 1. Требования + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (≥ 10.0.301) — проект нацелен на + `net10.0` и собирается с `LangVersion=preview` для включения возможностей **C# 15**. См. + [NET10-Upgrade-RU.md](NET10-Upgrade-RU.md) для полных заметок о миграции. +- Запущенный экземпляр [Ollama](https://ollama.com) — либо: + - **Локально:** [установите Ollama](https://ollama.com/download) и запустите `ollama serve`, либо + - **Cloud:** зарегистрируйтесь в [Ollama Cloud](https://ollama.com/cloud) и получите API-ключ + (без установки, без GPU). +- **Опционально — для роли Operator:** [Node.js + npx](https://nodejs.org) для запуска MCP-серверов + (например, `@playwright/mcp`, `@marp-team/marp-cli`). См. [раздел 7](#7-использование-operator-mcp-инструменты). + +### Модели для установки + +Выберите **один** из наборов моделей ниже в зависимости от вашего железа. Набор 🟡 «Стандартный» +рекомендуется для большинства случаев. + +#### 🟢 Минимальный — слабое железо / smoke-тесты (≈ 2 ГБ всего) + +```bash +ollama pull llama3.2:1b +ollama pull qwen2.5:1.5b +ollama pull nomic-embed-text +``` + +#### 🟡 Стандартный — рекомендуется (≈ 7 ГБ всего) + +```bash +ollama pull llama3.2:3b +ollama pull qwen2.5:7b +ollama pull nomic-embed-text +``` + +#### 🔴 Высокопроизводительный — GPU с ≥ 24 ГБ VRAM или Ollama Cloud (≈ 30+ ГБ) + +```bash +ollama pull llama3.1:8b +ollama pull qwen2.5:14b +ollama pull mistral:7b +ollama pull nomic-embed-text +``` + +> ℹ️ Ollama Cloud использует те же имена моделей — локальная загрузка не нужна, только API-ключ. + +--- + +## 2. Установка пакета + +```bash +dotnet add package Delibera.Core +``` + +Или клонируйте репозиторий и сошлитесь на `Delibera.Core` напрямую: + +```bash +git clone https://github.com/delibera/Delibera.git +cd Delibera +``` + +--- + +## 3. Ваше первое обсуждение + +Создайте консольный проект и вставьте следующее: + +```csharp +// Program.cs +using Delibera.Core.Council; +using Delibera.Core.Providers; + +using var factory = new ProviderFactory(); +var ollama = factory.CreateOllama("http://localhost:11434"); + +var result = await new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Analyst") + .AddMember("qwen2.5:7b", ollama, "Strategist") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + .WithStandardDebate() + .WithSystemPrompt("You are a software architecture expert.") + .WithUserPrompt("Microservices vs Monolith for a 5-person startup?") + .WithMaxRounds(4) + .SaveResultTo("./deliberation.md") + .Build() + .ExecuteAsync(); + +Console.WriteLine(result.FinalVerdict); +``` + +Запустите: + +```bash +dotnet run +``` + +Delibera проведёт структурированные многораундовые дебаты и запишет полную стенограмму и вердикт +председателя в файл `deliberation.md`. + +> ☁️ **Используете Ollama Cloud?** Замените endpoint на `https://api.ollama.com` и передайте API-ключ: +> `factory.CreateOllama("https://api.ollama.com", "YOUR_API_KEY")`. + +--- + +## 4. Использование Dependency Injection + +```csharp +using Delibera.Core.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Привязывает секцию "Delibera" из appsettings.json +services.AddDelibera(configuration, "Delibera"); + +var provider = services.BuildServiceProvider(); +var builder = provider.GetRequiredService(); +``` + +`appsettings.json`: + +```json +{ + "Delibera": { + "Strategy": "Standard", + "MaxRounds": 4, + "Temperature": 0.7, + "SystemPrompt": "You are a knowledgeable AI expert participating in a council debate.", + "Providers": { + "DefaultType": "Ollama", + "DefaultEndpoint": "http://localhost:11434", + "ApiKey": "", + "EmbeddingModel": "nomic-embed-text" + }, + "Compression": { + "Enabled": true, + "Strategy": "Hybrid", + "TargetRatio": 0.5, + "EnableCache": true, + "MaxCacheEntries": 256 + }, + "Rag": { + "Enabled": false, + "ProviderType": "Qdrant", + "Host": "localhost", + "Port": 6334, + "CollectionName": "council_knowledge", + "ConnectionString": null + }, + "Output": { + "Directory": "./debate_results", + "SeparateFiles": true, + "FilePrefix": null + } + } +} +``` + +> Для **Ollama Cloud** задайте `Providers:DefaultEndpoint` равным `https://api.ollama.com` и +> `Providers:ApiKey` равным вашему ключу. + +--- + +## 5. Добавление сжатия контекста + +```csharp +using Delibera.Core.Compression; +using Delibera.Core.Providers.LLM; + +var ollama = new OllamaProvider("http://localhost:11434"); +var embeddings = new OllamaEmbeddingProvider(ollama, "nomic-embed-text"); + +var result = await new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Analyst") + .AddMember("qwen2.5:7b", ollama, "Strategist") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + .WithCompression(CompressionStrategy.Hybrid, + llmProvider: ollama, + modelName: "llama3.2:3b", + embeddingProvider: embeddings) + .WithCompressionCache() + .WithUserPrompt("Analyze our architecture options...") + .WithMaxRounds(4) + .Build() + .ExecuteAsync(); + +Console.WriteLine(result.TokenStats?.ToSummary()); +``` + +Сжатие экономит примерно **30–70% токенов** без потери смысла контекста дебатов. + +--- + +## 6. Добавление RAG с Qdrant + +```bash +docker run -d -p 6333:6333 -p 6334:6334 qdrant/qdrant +``` + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Models; +using Delibera.Core.Providers.LLM; +using Delibera.Core.Providers.RAG; + +var ollama = new OllamaProvider("http://localhost:11434"); +var embeddings = new OllamaEmbeddingProvider(ollama, "nomic-embed-text"); + +var ragFactory = new RagProviderFactory(); +var rag = ragFactory.CreateQdrant(embeddings, "localhost", 6334); + +var kkMember = new CouncilMember("llama3.2:3b", ollama, "Knowledge Keeper"); +var keeper = new KnowledgeKeeper(rag, kkMember, "architecture_kb"); +await keeper.IndexFileAsync("./docs/architecture.md"); + +var result = await new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Backend Expert") + .AddMember("qwen2.5:7b", ollama, "DevOps Expert") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + .WithKnowledgeKeeper(keeper) + .WithStandardDebate() + .WithUserPrompt("Our startup has 4 developers. Microservices or monolith?") + .WithMaxRounds(4) + .Build() + .ExecuteAsync(); +``` + +> 💡 Замените `CreateQdrant(...)` на `CreatePgVector(embeddings, connectionString)`, чтобы +> использовать PostgreSQL/pgvector. + +--- + +## 7. Использование Operator (MCP-инструменты) + +**Operator** — это микроагент, который соединяет совет с внешними инструментами через серверы +[**MCP (Model Context Protocol)**](https://modelcontextprotocol.io) — веб-навигация, доступ к +файловой системе, генерация Marp-презентаций и многое другое. Участники делегируют задачу в любой +момент, написав маркер `[[OPERATOR: ...]]`, и Operator запускает нужные инструменты и возвращает +результат в следующий раунд. + +### 7.1. Установка требований + +Серверы из примеров ниже запускаются через `npx`, поэтому вам нужны **Node.js + npx**: + +```bash +# Проверьте, что Node.js / npx доступны +node --version +npx --version + +# Для сервера браузера один раз установите движки Playwright: +npx playwright install +``` + +Также нужна запущенная **Ollama** с дешёвой моделью для Operator (например, `llama3.2`) — Operator +использует свою собственную модель, отдельную от участников совета. + +### 7.2. Настройка MCP-серверов + +Каждый MCP-сервер описывается через `McpServerConfig`. Используйте `Stdio(...)` для локальных +процессов или `Http(...)` для удалённых серверов: + +```csharp +using Delibera.Core.Models; + +var servers = new[] +{ + // 🌐 Браузер — навигация, чтение страниц, клики, скриншоты + McpServerConfig.Stdio( + name: "browser", + command: "npx", + arguments: new[] { "-y", "@playwright/mcp@latest", "--headless" }), + + // 🎯 Marp — генерация презентаций HTML/PDF/PPTX из Markdown + McpServerConfig.Stdio( + name: "marp", + command: "npx", + arguments: new[] { "-y", "@marp-team/marp-cli", "--server", "./out" }), + + // …или удалённый HTTP/SSE MCP-сервер с заголовками авторизации: + // McpServerConfig.Http( + // name: "remote", + // endpoint: "https://my-mcp-host.example.com/mcp", + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }), +}; +``` + +| Сервер | Транспорт | Команда запуска | Что даёт | +| ------------ | --------- | ----------------------------------------------- | ---------------------------------------------------- | +| 🌐 `browser` | stdio | `npx -y @playwright/mcp@latest --headless` | Навигация по сайтам, чтение страниц, клики, скриншоты | +| 🎯 `marp` | stdio | `npx -y @marp-team/marp-cli --server ` | Генерация презентаций (HTML/PDF/PPTX) из Markdown | + +### 7.3. Прямое использование (без совета) + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Interfaces; +using Delibera.Core.Models; +using Delibera.Core.Providers.LLM; +using Delibera.Core.Providers.Mcp; + +var ollama = new OllamaProvider("http://localhost:11434"); + +await using var @operator = new Operator( + new CouncilMember("llama3.2", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }); + +await @operator.InitializeAsync(); // подключается к серверам и обнаруживает инструменты + +// 1) Открыть сайт и пересказать его +var browse = await @operator.ExecuteTaskAsync( + "Open https://modelcontextprotocol.io and briefly summarize what MCP is."); +Console.WriteLine(browse.FinalAnswer); + +// 2) Сгенерировать презентацию из 3 слайдов и сохранить как deck.html +var deck = await @operator.ExecuteTaskAsync( + "Generate a 3-slide Marp presentation about .NET 10 and save it as deck.html."); +Console.WriteLine(deck.FinalAnswer); +``` + +> `await using` освобождает MCP-клиентов через `ValueTask DisposeAsync` у Operator. + +### 7.4. Operator внутри дебатов + +Добавьте Operator в совет с помощью удобной перегрузки `WithOperator(...)`. Участники затем +делегируют работу через маркер `[[OPERATOR: ...]]`, и их результаты внедряются в последующие раунды: + +```csharp +var result = await new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Researcher") + .AddMember("qwen2.5:7b", ollama, "Reviewer") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + // Operator использует свою более дешёвую модель; reuseCompression разделяет компрессор совета + .WithOperator("llama3.2", ollama, servers, reuseCompression: true) + .WithStandardDebate() + .WithSystemPrompt( + "You may delegate research or presentation tasks to the Operator by writing " + + "[[OPERATOR: ]] in your message.") + .WithUserPrompt("Research the latest .NET 10 features and prepare a short summary deck.") + .WithMaxRounds(4) + .SaveResultTo("./deliberation.md") + .Build() + .ExecuteAsync(); + +Console.WriteLine(result.FinalVerdict); +``` + +Сообщение участника может выглядеть так: + +``` +I think we should highlight the runtime improvements. +[[OPERATOR: open https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10 and list the top 5 features]] +``` + +Все взаимодействия с Operator записываются по раундам и отображаются в финальном Markdown-отчёте в +блоке **🛠️ Operator Interactions**. + +### 7.5. Настройка через `appsettings.json` + +```json +{ + "Delibera": { + "Operator": { + "Enabled": true, + "ModelName": "llama3.2", + "ReuseCompression": true, + "McpServers": [ + { + "Name": "browser", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@playwright/mcp@latest", "--headless" ] + }, + { + "Name": "marp", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@marp-team/marp-cli", "--server", "./out" ] + } + ] + } + } +} +``` + +> ▶️ Запустите полное демо: `dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp`. +> См. [`OperatorMcpToolsExample.cs`](../src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs) +> и [NET10-Upgrade-RU.md](NET10-Upgrade-RU.md) для деталей. + +--- + +## 8. Сохранение раздельных выходных файлов + +```csharp +var (resultPath, statsPath, logsPath) = await result.SaveAllAsync("./output"); +// → debate__result.md +// → debate__statistics.md +// → debate__logs.md +``` + +Каждый файл — это обычный Markdown, идеальный для коммита в git, публикации в чат-инструменты или +передачи обратно в другую LLM. + +--- + +## 9. Изучите ConsoleApp + +В репозитории есть запускаемое демо в +[`src/Delibera.ConsoleApp/`](../src/Delibera.ConsoleApp/), которое задействует каждую возможность: + +| Пример | Команда | Что демонстрирует | +| -------------------- | -------------------------------- | ---------------------------------------------- | +| Быстрый старт | `dotnet run` | Дебаты по умолчанию из `appsettings.json` | +| Dependency injection | `dotnet run -- --di` | `AddDelibera()` + резолвинг `ICouncilBuilder` | +| Раздельный вывод | `dotnet run -- --separate-files` | `SaveAllAsync()` — три Markdown-файла | +| Сжатие контекста | `dotnet run -- --compression` | Все 4 стратегии + кэш + статистика токенов | +| Несколько провайдеров| `dotnet run -- --multiprovider` | Cloud + локальные модели в одном совете | +| RAG (Qdrant) | `dotnet run -- --rag` | Индексирование документов + Knowledge Keeper | +| RAG (pgvector) | `dotnet run -- --pgvector` | Векторный поиск на базе PostgreSQL | +| Operator (основы) | `dotnet run -- --operator` | Роль Operator с MCP-инструментами | +| Operator + MCP | `dotnet run -- --operator-mcp` | 🆕 MCP-серверы browser + Marp в совете | + +--- + +## Дальнейшие шаги + +- Прочитайте [README](../README-RU.md) для полного обзора возможностей, архитектуры и паттернов проектирования. +- См. [CONTRIBUTING.md](../CONTRIBUTING.md), если хотите внести вклад. + +--- + +
+ +**⚖️ Delibera — Продуманные решения с помощью ИИ** + +
diff --git a/docs/QuickStart.md b/docs/QuickStart.md index 56fbdb0..0fa09b1 100644 --- a/docs/QuickStart.md +++ b/docs/QuickStart.md @@ -6,6 +6,8 @@ ### ⚖️ Thoughtful AI Decisions +🇷🇺 [Русская версия (QuickStart-RU.md)](QuickStart-RU.md) + This guide walks you through your first AI deliberation with **Delibera** in a few minutes. @@ -15,11 +17,15 @@ By the end you'll have a multi-model debate running locally and a Markdown trans ## 1. Prerequisites -- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) (≥ 10.0.301) — the project targets + `net10.0` and builds with `LangVersion=preview` to enable **C# 15** features. See + [NET10-Upgrade.md](NET10-Upgrade.md) for the full migration notes. - A running [Ollama](https://ollama.com) instance — either: - **Local:** [install Ollama](https://ollama.com/download) and run `ollama serve`, or - **Cloud:** sign up for [Ollama Cloud](https://ollama.com/cloud) and grab an API key (no install, no GPU required). +- **Optional — for the Operator role:** [Node.js + npx](https://nodejs.org) to launch MCP servers + (e.g. `@playwright/mcp`, `@marp-team/marp-cli`). See [section 7](#7-using-the-operator-mcp-tools). ### Models to install @@ -239,7 +245,165 @@ var result = await new CouncilBuilder() --- -## 7. Saving Separate Output Files +## 7. Using the Operator (MCP Tools) + +The **Operator** is a micro-agent that connects the council to external tools through +[**MCP (Model Context Protocol)**](https://modelcontextprotocol.io) servers — web browsing, +file system access, Marp presentation generation, and more. Participants delegate a task at any +moment by writing a `[[OPERATOR: ...]]` marker, and the Operator runs the right tools and feeds the +result back into the next round. + +### 7.1. Install the prerequisites + +The example servers below are launched via `npx`, so you need **Node.js + npx**: + +```bash +# Verify Node.js / npx are available +node --version +npx --version + +# For the browser server, install the Playwright engines once: +npx playwright install +``` + +You also need **Ollama** running with a cheap model for the Operator (e.g. `llama3.2`) — the +Operator uses its own model, separate from the council members. + +### 7.2. Configure MCP servers + +Each MCP server is described by an `McpServerConfig`. Use `Stdio(...)` for local processes or +`Http(...)` for remote servers: + +```csharp +using Delibera.Core.Models; + +var servers = new[] +{ + // 🌐 Browser — navigation, page reading, clicks, screenshots + McpServerConfig.Stdio( + name: "browser", + command: "npx", + arguments: new[] { "-y", "@playwright/mcp@latest", "--headless" }), + + // 🎯 Marp — generate HTML/PDF/PPTX presentations from Markdown + McpServerConfig.Stdio( + name: "marp", + command: "npx", + arguments: new[] { "-y", "@marp-team/marp-cli", "--server", "./out" }), + + // …or a remote HTTP/SSE MCP server with auth headers: + // McpServerConfig.Http( + // name: "remote", + // endpoint: "https://my-mcp-host.example.com/mcp", + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }), +}; +``` + +| Server | Transport | Launch command | What it provides | +| ------------ | --------- | ----------------------------------------------- | ---------------------------------------------------- | +| 🌐 `browser` | stdio | `npx -y @playwright/mcp@latest --headless` | Site navigation, page reading, clicks, screenshots | +| 🎯 `marp` | stdio | `npx -y @marp-team/marp-cli --server ` | Generating presentations (HTML/PDF/PPTX) from Markdown | + +### 7.3. Direct usage (without a council) + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Interfaces; +using Delibera.Core.Models; +using Delibera.Core.Providers.LLM; +using Delibera.Core.Providers.Mcp; + +var ollama = new OllamaProvider("http://localhost:11434"); + +await using var @operator = new Operator( + new CouncilMember("llama3.2", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }); + +await @operator.InitializeAsync(); // connects to servers and discovers tools + +// 1) Browse a website and summarize it +var browse = await @operator.ExecuteTaskAsync( + "Open https://modelcontextprotocol.io and briefly summarize what MCP is."); +Console.WriteLine(browse.FinalAnswer); + +// 2) Generate a 3-slide presentation and save it as deck.html +var deck = await @operator.ExecuteTaskAsync( + "Generate a 3-slide Marp presentation about .NET 10 and save it as deck.html."); +Console.WriteLine(deck.FinalAnswer); +``` + +> `await using` releases the MCP clients through the Operator's `ValueTask DisposeAsync`. + +### 7.4. Operator inside a debate + +Add the Operator to a council with the convenience `WithOperator(...)` overload. Participants then +delegate work via the `[[OPERATOR: ...]]` marker, and their results are injected into later rounds: + +```csharp +var result = await new CouncilBuilder() + .AddMember("llama3.2:3b", ollama, "Researcher") + .AddMember("qwen2.5:7b", ollama, "Reviewer") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + // Operator uses its own cheaper model; reuseCompression shares the council's compressor + .WithOperator("llama3.2", ollama, servers, reuseCompression: true) + .WithStandardDebate() + .WithSystemPrompt( + "You may delegate research or presentation tasks to the Operator by writing " + + "[[OPERATOR: ]] in your message.") + .WithUserPrompt("Research the latest .NET 10 features and prepare a short summary deck.") + .WithMaxRounds(4) + .SaveResultTo("./deliberation.md") + .Build() + .ExecuteAsync(); + +Console.WriteLine(result.FinalVerdict); +``` + +A participant message might look like: + +``` +I think we should highlight the runtime improvements. +[[OPERATOR: open https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10 and list the top 5 features]] +``` + +All Operator interactions are recorded per round and rendered in the final Markdown report under a +**🛠️ Operator Interactions** block. + +### 7.5. Configure via `appsettings.json` + +```json +{ + "Delibera": { + "Operator": { + "Enabled": true, + "ModelName": "llama3.2", + "ReuseCompression": true, + "McpServers": [ + { + "Name": "browser", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@playwright/mcp@latest", "--headless" ] + }, + { + "Name": "marp", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@marp-team/marp-cli", "--server", "./out" ] + } + ] + } + } +} +``` + +> ▶️ Run the full demo: `dotnet run --project src/Delibera.ConsoleApp -- --operator-mcp`. +> See [`OperatorMcpToolsExample.cs`](../src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs) +> and [NET10-Upgrade.md](NET10-Upgrade.md) for details. + +--- + +## 8. Saving Separate Output Files ```csharp var (resultPath, statsPath, logsPath) = await result.SaveAllAsync("./output"); @@ -253,7 +417,7 @@ back into another LLM. --- -## 8. Explore the ConsoleApp +## 9. Explore the ConsoleApp The repository ships with a runnable demo under [`src/Delibera.ConsoleApp/`](../src/Delibera.ConsoleApp/) that exercises every feature: @@ -267,6 +431,8 @@ The repository ships with a runnable demo under | Multi-provider | `dotnet run -- --multiprovider` | Cloud + local models in one council | | RAG (Qdrant) | `dotnet run -- --rag` | Document indexing + Knowledge Keeper | | RAG (pgvector) | `dotnet run -- --pgvector` | PostgreSQL-backed vector search | +| Operator (basics) | `dotnet run -- --operator` | Operator role with MCP tools | +| Operator + MCP | `dotnet run -- --operator-mcp` | 🆕 Browser + Marp MCP servers in a council | --- diff --git a/src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj b/src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj index 07e29a6..7474cda 100644 --- a/src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj +++ b/src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj @@ -5,7 +5,7 @@ net10.0 enable enable - 14.0 + preview Delibera Delibera.ConsoleApp $(NoWarn);CS9057 diff --git a/src/Delibera.ConsoleApp/Examples/OperatorExample.cs b/src/Delibera.ConsoleApp/Examples/OperatorExample.cs new file mode 100644 index 0000000..e1d4dfa --- /dev/null +++ b/src/Delibera.ConsoleApp/Examples/OperatorExample.cs @@ -0,0 +1,111 @@ +using Delibera.Core.Council; +using Delibera.Core.Models; +using Delibera.Core.Providers; + +namespace Delibera.ConsoleApp.Examples; + +/// +/// Operator example — demonstrates the 🛠️ Operator role: a micro-agent that connects to one or +/// more MCP (Model Context Protocol) servers and exposes their tools to the debate participants. +/// During the debate, participants delegate natural-language tasks to the Operator using the +/// [[OPERATOR: ...]] marker (e.g., web search, database lookups, writing to Notion, etc.). +/// The Operator uses its own (cheaper) model to plan tool calls, execute them, and synthesise an +/// answer that is fed back into the next round. +/// +public static class OperatorExample +{ + public static async Task RunAsync() + { + Console.WriteLine("🛠️ Operator (MCP) Example\n"); + + // ── 1. LLM providers ── + // Participants typically use stronger (more expensive) models; the Operator uses a cheaper one. + using var factory = new ProviderFactory(); + var ollama = factory.CreateOllama("http://localhost:11434"); + + // ── 2. Configure MCP servers ── + // (a) Stdio transport — a local MCP server launched as a child process. + // The "everything" reference server ships a handful of demo tools (echo, add, etc.). + var everythingServer = McpServerConfig.Stdio( + name: "demo", + command: "npx", + arguments: ["-y", "@modelcontextprotocol/server-everything"]); + + // (b) A filesystem MCP server scoped to a working directory (also stdio). + var filesystemServer = McpServerConfig.Stdio( + name: "files", + command: "npx", + arguments: ["-y", "@modelcontextprotocol/server-filesystem", Directory.GetCurrentDirectory()]); + + // (c) HTTP/SSE transport — a remote MCP server (e.g., a hosted Notion or web-search server). + // Supply auth headers as needed. Commented out: requires a real endpoint + token. + // var notionServer = McpServerConfig.Http( + // name: "notion", + // endpoint: new Uri("https://mcp.notion.com/sse"), + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }); + + var servers = new[] { everythingServer, filesystemServer }; + + // ── 3. Build the council with an Operator ── + // The convenience overload creates the Operator from a (cheap) model + MCP servers and wires + // it through the whole pipeline. The Operator is DI-friendly and initialised automatically + // (connects to the servers and discovers their tools) before the debate starts. + var executor = new CouncilBuilder() + .AddMember("qwen2.5", ollama, "Researcher") + .AddMember("llama2", ollama, "Analyst") + .SetChairman(Chairman.CreateStandard("qwen2.5", ollama)) + .WithOperator( + modelName: "llama3.2", // cheaper model for tool orchestration + provider: ollama, + servers: servers) + .WithStandardDebate() + .WithSystemPrompt("You are a meticulous research council. Delegate factual lookups to the Operator.") + .WithUserPrompt( + "Summarise the key risks of adopting microservices for a 4-developer startup. " + + "If you need an external lookup or calculation, delegate it to the Operator.") + .WithMaxRounds(4) + .SaveResultTo("./results/operator_debate.md") + .Build(); + + Console.WriteLine(executor.GetInfo()); + + // ── 4. Observe Operator activity per round ── + executor.OnRoundCompleted += round => + { + Console.WriteLine($"✅ Round {round.RoundNumber}: {round.RoundName}"); + foreach (var oi in round.OperatorInteractions) + { + Console.WriteLine($" 🛠️ {oi.RequesterName} → \"{oi.Task}\""); + Console.WriteLine($" tools: {oi.ToolCallCount}, answer: {Preview(oi.Answer, 120)}"); + } + }; + + // ── 5. Run the debate ── + try + { + var result = await executor.ExecuteAsync(); + + Console.WriteLine($"\n🏆 Debate completed in {result.TotalDuration.TotalSeconds:F1}s"); + if (result.OperatorName is not null) + Console.WriteLine($" Operator: {result.OperatorName}"); + Console.WriteLine("📁 Saved to: ./results/operator_debate.md"); + + if (!string.IsNullOrWhiteSpace(result.FinalVerdict)) + { + Console.WriteLine("\n══ FINAL VERDICT ══\n"); + Console.WriteLine(result.FinalVerdict); + } + } + catch (Exception ex) + { + Console.WriteLine($"\n❌ Operator example failed: {ex.Message}"); + Console.WriteLine("\n💡 Tips:"); + Console.WriteLine(" • Node.js + npx must be installed for the npx-based MCP servers."); + Console.WriteLine(" • Ensure a local Ollama server is running: ollama serve"); + Console.WriteLine(" • Swap in your own MCP servers via McpServerConfig.Stdio / .Http."); + } + } + + private static string Preview(string text, int max) => + string.IsNullOrEmpty(text) ? "(empty)" : text.Length <= max ? text : text[..max] + "…"; +} diff --git a/src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs b/src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs new file mode 100644 index 0000000..2df7d14 --- /dev/null +++ b/src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs @@ -0,0 +1,237 @@ +using Delibera.Core.Council; +using Delibera.Core.Interfaces; +using Delibera.Core.Models; +using Delibera.Core.Providers; +using Delibera.Core.Providers.Mcp; + +namespace Delibera.ConsoleApp.Examples; + +/// +/// Расширенный пример работы 🛠️ Operator с реальными MCP-инструментами. +/// +/// Демонстрирует два практических MCP-сервера: +/// +/// +/// +/// +/// Браузер — официальный сервер @playwright/mcp, который даёт Operator'у +/// инструменты для навигации по сайтам, чтения содержимого страниц, кликов и снятия +/// скриншотов (browser_navigate, browser_snapshot, browser_click и т. д.). +/// +/// +/// +/// +/// Marp-презентации — сервер на базе Marp CLI (@marp-team/marp-cli), +/// запускаемый как stdio-MCP, который превращает Markdown в готовые презентации +/// (HTML/PDF/PPTX). Operator формирует Markdown по запросу участника и собирает слайды. +/// +/// +/// +/// +/// Пример показывает оба способа использования Operator: +/// +/// +/// Прямой вызов (раздел A). +/// Делегирование внутри совета через маркер [[OPERATOR: ...]] (раздел B). +/// +/// +public static class OperatorMcpToolsExample +{ + public static async Task RunAsync() + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + Console.WriteLine("🛠️ Operator + MCP-инструменты (браузер · Marp-презентации)\n"); + + // ── LLM-провайдер ── + // Для оркестрации инструментов достаточно «дешёвой» модели — её задача планировать + // и интерпретировать вызовы, а не вести глубокие рассуждения. + using var factory = new ProviderFactory(); + var ollama = factory.CreateOllama("http://localhost:11434"); + + // ════════════════════════════════════════════════════════════════ + // Конфигурация MCP-серверов + // ════════════════════════════════════════════════════════════════ + + // (1) 🌐 Браузер — Playwright MCP. + // Предоставляет инструменты управления реальным браузером. + // Запускается как дочерний процесс через npx (нужен Node.js). + var browserServer = McpServerConfig.Stdio( + name: "browser", + command: "npx", + arguments: ["-y", "@playwright/mcp@latest", "--headless"]); + + // (2) 🎯 Marp — генерация презентаций из Markdown. + // Каталог вывода монтируется через рабочую директорию; готовые файлы + // (например, deck.html / deck.pdf) появятся в ./results/presentations. + var presentationsDir = Path.Combine(Directory.GetCurrentDirectory(), "results", "presentations"); + Directory.CreateDirectory(presentationsDir); + + var marpServer = McpServerConfig.Stdio( + name: "marp", + command: "npx", + arguments: ["-y", "@marp-team/marp-cli", "--server", presentationsDir], + workingDirectory: presentationsDir); + + // (3) 🌐 HTTP-вариант (закомментирован): удалённый MCP-сервер браузера/поиска. + // Передавайте заголовки авторизации при необходимости. + // var remoteBrowser = McpServerConfig.Http( + // name: "browser-remote", + // endpoint: new Uri("https://mcp.example.com/sse"), + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }); + + var servers = new[] { browserServer, marpServer }; + + // ════════════════════════════════════════════════════════════════ + // Раздел A. Прямое использование Operator (без совета) + // ════════════════════════════════════════════════════════════════ + await RunDirectOperatorAsync(ollama, servers); + + // ════════════════════════════════════════════════════════════════ + // Раздел B. Operator внутри совета (делегирование [[OPERATOR: ...]]) + // ════════════════════════════════════════════════════════════════ + await RunCouncilWithOperatorAsync(ollama, servers); + } + + // ────────────────────────────────────────────────────────────────── + // A. Прямой Operator: формируем MCP-клиентов вручную и вызываем задачи + // ────────────────────────────────────────────────────────────────── + private static async Task RunDirectOperatorAsync( + ILLMProvider ollama, + IReadOnlyList servers) + { + Console.WriteLine("══ Раздел A. Прямой вызов Operator ══\n"); + + // Operator использует «дешёвую» модель для планирования инструментов. + var operatorModel = new CouncilMember("llama3.2", ollama, "Operator"); + + // Создаём по одному MCP-клиенту на каждый сервер. + var clients = servers.Select(IMcpClientFor).ToArray(); + + await using var @operator = new Operator(operatorModel, clients); + + try + { + await @operator.InitializeAsync(); + Console.WriteLine($"✅ Operator инициализирован: {@operator.DisplayName}"); + Console.WriteLine($" Обнаружено инструментов: {@operator.AvailableTools.Count}"); + foreach (var tool in @operator.AvailableTools.Take(8)) + Console.WriteLine($" • [{tool.ServerName}] {tool.Name} — {Preview(tool.Description, 60)}"); + + // A.1 🌐 Браузерная задача — Operator сам выберет browser_navigate + browser_snapshot. + Console.WriteLine("\n▶ Задача 1 (браузер): прочитать заголовок и краткое содержание страницы."); + var browseResult = await @operator.ExecuteTaskAsync( + requesterName: "Аналитик", + task: "Открой https://modelcontextprotocol.io и кратко перескажи, что такое MCP, в 3 предложениях."); + PrintResult(browseResult); + + // A.2 🎯 Marp-задача — Operator сформирует Markdown и соберёт презентацию. + Console.WriteLine("\n▶ Задача 2 (Marp): сгенерировать презентацию из Markdown."); + var deckResult = await @operator.ExecuteTaskAsync( + requesterName: "Докладчик", + task: "Создай Marp-презентацию из 3 слайдов о преимуществах .NET 10 и сохрани её как deck.html."); + PrintResult(deckResult); + } + catch (Exception ex) + { + PrintFailureTips(ex); + } + } + + // ────────────────────────────────────────────────────────────────── + // B. Совет с Operator: участники делегируют задачи маркером [[OPERATOR: ...]] + // ────────────────────────────────────────────────────────────────── + private static async Task RunCouncilWithOperatorAsync( + ILLMProvider ollama, + IReadOnlyList servers) + { + Console.WriteLine("\n══ Раздел B. Operator внутри совета ══\n"); + + var executor = new CouncilBuilder() + .AddMember("qwen2.5", ollama, "Исследователь") + .AddMember("llama2", ollama, "Архитектор") + .SetChairman(Chairman.CreateStandard("qwen2.5", ollama)) + .WithOperator( + modelName: "llama3.2", // дешёвая модель для оркестрации инструментов + provider: ollama, + servers: servers) + .WithStandardDebate() + .WithSystemPrompt( + "Вы — совет по технологической стратегии. Когда нужны свежие факты из интернета " + + "или сборка презентации — делегируйте задачу Operator'у маркером [[OPERATOR: задача]].") + .WithUserPrompt( + "Подготовьте краткий обзор «Что нового в .NET 10 для high-performance кода». " + + "Сначала проверьте актуальную информацию через браузер, затем соберите итоговую " + + "Marp-презентацию из 4 слайдов.") + .WithMaxRounds(4) + .SaveResultTo("./results/operator_mcp_debate.md") + .Build(); + + Console.WriteLine(executor.GetInfo()); + + // Наблюдаем за активностью Operator по раундам. + executor.OnRoundCompleted += round => + { + Console.WriteLine($"✅ Раунд {round.RoundNumber}: {round.RoundName}"); + foreach (var oi in round.OperatorInteractions) + { + Console.WriteLine($" 🛠️ {oi.RequesterName} → \"{Preview(oi.Task, 70)}\""); + Console.WriteLine($" инструментов: {oi.ToolCallCount}, ответ: {Preview(oi.Answer, 100)}"); + } + }; + + try + { + var result = await executor.ExecuteAsync(); + + Console.WriteLine($"\n🏆 Дебаты завершены за {result.TotalDuration.TotalSeconds:F1} c"); + if (result.OperatorName is not null) + Console.WriteLine($" Operator: {result.OperatorName}"); + Console.WriteLine("📁 Отчёт: ./results/operator_mcp_debate.md"); + Console.WriteLine("📁 Презентации: ./results/presentations/"); + + if (!string.IsNullOrWhiteSpace(result.FinalVerdict)) + { + Console.WriteLine("\n══ ИТОГОВОЕ РЕШЕНИЕ ══\n"); + Console.WriteLine(result.FinalVerdict); + } + } + catch (Exception ex) + { + PrintFailureTips(ex); + } + } + + // ────────────────────────────────────────────────────────────────── + // Вспомогательные методы + // ────────────────────────────────────────────────────────────────── + + /// Создаёт MCP-клиент для конфигурации сервера. + private static IMcpClient IMcpClientFor(McpServerConfig config) => new McpClientAdapter(config); + + private static void PrintResult(OperatorResult result) + { + Console.WriteLine($" Вызвано инструментов: {result.ToolCalls.Count}" + + (result.Compressed ? " (ответ сжат)" : string.Empty)); + foreach (var call in result.ToolCalls) + { + var status = call.IsError ? "❌" : "✓"; + Console.WriteLine($" {status} [{call.ServerName}] {call.ToolName}"); + } + + Console.WriteLine($" 💬 Ответ: {Preview(result.Answer, 240)}"); + } + + private static void PrintFailureTips(Exception ex) + { + Console.WriteLine($"\n❌ Пример завершился ошибкой: {ex.Message}"); + Console.WriteLine("\n💡 Подсказки:"); + Console.WriteLine(" • Установите Node.js + npx (нужны для npx-MCP-серверов)."); + Console.WriteLine(" • Для браузера: первый запуск Playwright скачает движки (npx playwright install)."); + Console.WriteLine(" • Запустите локальный Ollama: ollama serve (модели llama3.2, qwen2.5)."); + Console.WriteLine(" • Свои MCP-серверы подключаются через McpServerConfig.Stdio / .Http."); + } + + private static string Preview(string? text, int max) => + string.IsNullOrEmpty(text) ? "(пусто)" : + text.Length <= max ? text : text[..max] + "…"; +} diff --git a/src/Delibera.ConsoleApp/Program.cs b/src/Delibera.ConsoleApp/Program.cs index c608f45..f6444bc 100644 --- a/src/Delibera.ConsoleApp/Program.cs +++ b/src/Delibera.ConsoleApp/Program.cs @@ -65,6 +65,18 @@ public static async Task Main(string[] args) return; } + if (args.Contains("--operator")) + { + await OperatorExample.RunAsync(); + return; + } + + if (args.Contains("--operator-mcp")) + { + await OperatorMcpToolsExample.RunAsync(); + return; + } + // Quick DI showcase before main demo Console.WriteLine("🆕 v3.1 DI Quick Demo:"); Console.WriteLine(" Run with --di for full DI example"); @@ -78,7 +90,7 @@ public static async Task Main(string[] args) var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", false, true) - .AddUserSecrets(Assembly.GetEntryAssembly()) + .AddUserSecrets(Assembly.GetEntryAssembly()!) .Build(); var cfg = configuration.GetSection("DeliberaApp"); @@ -394,6 +406,13 @@ public static async Task Main(string[] args) Console.WriteLine($" Q: {ki.Query[..Math.Min(80, ki.Query.Length)]}… → {ki.SourceChunks} chunks"); } + if (round.OperatorInteractions.Count > 0) + { + Console.WriteLine(" 🛠️ Operator interactions:"); + foreach (var oi in round.OperatorInteractions) + Console.WriteLine($" {oi.RequesterName}: {oi.Task[..Math.Min(80, oi.Task.Length)]}… → {oi.ToolCallCount} tool call(s)"); + } + foreach (var (member, response) in round.Responses) { var preview = response.Length > 300 @@ -421,6 +440,8 @@ public static async Task Main(string[] args) Console.WriteLine($" Participants: {string.Join(", ", result.Participants)}"); if (result.KnowledgeKeeperName is not null) Console.WriteLine($" Knowledge Keeper: {result.KnowledgeKeeperName}"); + if (result.OperatorName is not null) + Console.WriteLine($" Operator: {result.OperatorName}"); // Token statistics if (result.TokenStats is not null) Console.WriteLine($"\n{result.TokenStats.ToSummary()}"); diff --git a/src/Delibera.Core/Compression/CompressionCache.cs b/src/Delibera.Core/Compression/CompressionCache.cs index 8ebecf7..a619f81 100644 --- a/src/Delibera.Core/Compression/CompressionCache.cs +++ b/src/Delibera.Core/Compression/CompressionCache.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Collections.Concurrent; using System.Security.Cryptography; @@ -110,9 +111,33 @@ public void Clear() private static string ComputeKey(string text, string strategyName) { - var input = $"{strategyName}:{text}"; - var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(input)); - return Convert.ToHexString(hashBytes); + // Compose "{strategy}:{text}" into a pooled char buffer, then UTF-8 encode + // into a pooled byte buffer — avoids two intermediate heap allocations + // (the interpolated string and the GetBytes array) on this hot path. + var charCount = strategyName.Length + 1 + text.Length; + var chars = ArrayPool.Shared.Rent(charCount); + byte[]? bytes = null; + try + { + strategyName.AsSpan().CopyTo(chars); + chars[strategyName.Length] = ':'; + text.AsSpan().CopyTo(chars.AsSpan(strategyName.Length + 1)); + var charSpan = chars.AsSpan(0, charCount); + + var maxBytes = Encoding.UTF8.GetMaxByteCount(charCount); + bytes = ArrayPool.Shared.Rent(maxBytes); + var byteCount = Encoding.UTF8.GetBytes(charSpan, bytes); + + Span hash = stackalloc byte[SHA256.HashSizeInBytes]; + SHA256.HashData(bytes.AsSpan(0, byteCount), hash); + return Convert.ToHexString(hash); + } + finally + { + ArrayPool.Shared.Return(chars); + if (bytes is not null) + ArrayPool.Shared.Return(bytes); + } } private sealed class CacheEntry diff --git a/src/Delibera.Core/Compression/SemanticCompressor.cs b/src/Delibera.Core/Compression/SemanticCompressor.cs index 8000cdf..cbbc3fb 100644 --- a/src/Delibera.Core/Compression/SemanticCompressor.cs +++ b/src/Delibera.Core/Compression/SemanticCompressor.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Numerics; namespace Delibera.Core.Compression; @@ -151,11 +152,38 @@ internal static List SplitSentences(string text) return sentences; } - internal static double CosineSimilarity(float[] a, float[] b) + internal static double CosineSimilarity(ReadOnlySpan a, ReadOnlySpan b) { - if (a.Length != b.Length) return 0; - double dot = 0, magA = 0, magB = 0; - for (var i = 0; i < a.Length; i++) + if (a.Length != b.Length || a.IsEmpty) return 0; + + float dot = 0, magA = 0, magB = 0; + var i = 0; + + // SIMD fast-path: process Vector.Count lanes at a time. + // On modern x64/ARM64 this is 8–16 floats per iteration. + if (Vector.IsHardwareAccelerated && a.Length >= Vector.Count) + { + var dotAcc = Vector.Zero; + var magAAcc = Vector.Zero; + var magBAcc = Vector.Zero; + var width = Vector.Count; + + for (; i <= a.Length - width; i += width) + { + var va = new Vector(a.Slice(i, width)); + var vb = new Vector(b.Slice(i, width)); + dotAcc += va * vb; + magAAcc += va * va; + magBAcc += vb * vb; + } + + dot = Vector.Dot(dotAcc, Vector.One); + magA = Vector.Dot(magAAcc, Vector.One); + magB = Vector.Dot(magBAcc, Vector.One); + } + + // Scalar tail (and full loop when SIMD is unavailable). + for (; i < a.Length; i++) { dot += a[i] * b[i]; magA += a[i] * a[i]; diff --git a/src/Delibera.Core/Compression/TokenCounter.cs b/src/Delibera.Core/Compression/TokenCounter.cs index 7d90948..6f7a1dc 100644 --- a/src/Delibera.Core/Compression/TokenCounter.cs +++ b/src/Delibera.Core/Compression/TokenCounter.cs @@ -44,6 +44,19 @@ public int EstimateTokens(string? text) if (TokenizerFunc is not null) return TokenizerFunc(text); + return EstimateTokens(text.AsSpan()); + } + + /// + /// Estimates the token count for the given text span without allocating. + /// Note: a custom is ignored on this allocation-free path. + /// + /// Input text span. + /// Estimated token count. + public int EstimateTokens(ReadOnlySpan text) + { + if (text.IsEmpty) return 0; + // Heuristic: count words + account for sub-word tokenization // Blend word-count and char-count estimates for better accuracy var wordCount = CountWords(text); @@ -95,7 +108,7 @@ public string TruncateToTokenLimit(string text, int maxTokens) // ────────────────────────────────────────────── - private static int CountWords(string text) + private static int CountWords(ReadOnlySpan text) { var count = 0; var inWord = false; diff --git a/src/Delibera.Core/Council/CouncilBuilder.cs b/src/Delibera.Core/Council/CouncilBuilder.cs index 72f6f2d..37fa7bc 100644 --- a/src/Delibera.Core/Council/CouncilBuilder.cs +++ b/src/Delibera.Core/Council/CouncilBuilder.cs @@ -1,5 +1,6 @@ using Delibera.Core.Compression; using Delibera.Core.Debate; +using Delibera.Core.Providers.Mcp; namespace Delibera.Core.Council; @@ -15,6 +16,10 @@ public sealed class CouncilBuilder : ICouncilBuilder private IContextCompressor? _compressor; private IKnowledgeBase? _knowledgeBase; private KnowledgeKeeper? _knowledgeKeeper; + private Operator? _operator; + private bool _operatorReuseCompression; + private CouncilMember? _operatorModel; + private IReadOnlyList? _operatorServers; private int _maxRounds = 4; private string? _outputPath; private IDebateStrategy _strategy = new StandardDebate(); @@ -89,6 +94,37 @@ public ICouncilBuilder WithKnowledgeKeeper( return this; } + // ── Operator (MCP tool micro-agent) ── + + /// + public ICouncilBuilder WithOperator(Operator @operator) + { + _operator = @operator ?? throw new ArgumentNullException(nameof(@operator)); + return this; + } + + /// + public ICouncilBuilder WithOperator( + string modelName, + ILLMProvider provider, + IEnumerable servers, + bool reuseCompression = true) + { + ArgumentException.ThrowIfNullOrWhiteSpace(modelName); + ArgumentNullException.ThrowIfNull(provider); + ArgumentNullException.ThrowIfNull(servers); + + var serverList = servers.ToList(); + if (serverList.Count == 0) + throw new ArgumentException("At least one MCP server configuration is required.", nameof(servers)); + + // Operator construction is deferred to Build() so it can reuse the council's compressor. + _operatorModel = new CouncilMember(modelName, provider, "Operator"); + _operatorServers = serverList; + _operatorReuseCompression = reuseCompression; + return this; + } + // ── Strategy ── /// @@ -202,6 +238,19 @@ public CouncilExecutor Build() KnowledgeFiles = _knowledgeBase?.GetLoadedSources().ToList() ?? [] }; + // Build the deferred Operator (if configured via the convenience overload) so it can reuse + // the council's compressor when requested. + var @operator = _operator; + if (@operator is null && _operatorModel is not null && _operatorServers is not null) + { + var clients = _operatorServers.Select(s => (IMcpClient)new McpClientAdapter(s)).ToList(); + @operator = new Operator( + _operatorModel, + clients, + _operatorReuseCompression ? _compressor : null, + _operatorReuseCompression ? _compressionOptions : null); + } + return new CouncilExecutor( _members.AsReadOnly(), _chairman, @@ -213,7 +262,8 @@ public CouncilExecutor Build() _outputPath, _compressor, _compressionOptions, - _compressionCache); + _compressionCache, + @operator); } /// diff --git a/src/Delibera.Core/Council/CouncilExecutor.cs b/src/Delibera.Core/Council/CouncilExecutor.cs index bc91974..cc2823a 100644 --- a/src/Delibera.Core/Council/CouncilExecutor.cs +++ b/src/Delibera.Core/Council/CouncilExecutor.cs @@ -26,11 +26,13 @@ internal CouncilExecutor( string? outputPath, IContextCompressor? compressor = null, CompressionOptions? compressionOptions = null, - CompressionCache? compressionCache = null) + CompressionCache? compressionCache = null, + Operator? @operator = null) { Members = members; Chairman = chairman; KnowledgeKeeper = knowledgeKeeper; + Operator = @operator; Strategy = strategy; _context = context; _maxRounds = maxRounds; @@ -53,6 +55,9 @@ internal CouncilExecutor( /// Knowledge Keeper (may be null). public KnowledgeKeeper? KnowledgeKeeper { get; } + /// Operator (may be null). + public Operator? Operator { get; } + /// Debate strategy. public IDebateStrategy Strategy { get; } @@ -80,6 +85,25 @@ public async Task ExecuteAsync(CancellationToken ct = default) if (KnowledgeKeeper is not null) Log(ExecutionLog.Info("KnowledgeKeeper", $"Knowledge Keeper ready: {KnowledgeKeeper.DisplayName} (collection: {KnowledgeKeeper.CollectionName})")); + // Initialise the Operator (connect to MCP servers, discover tools) before the debate begins. + if (Operator is not null) + { + if (!Operator.IsInitialized) + { + Log(ExecutionLog.Info("Operator", $"Initialising Operator: {Operator.DisplayName}…")); + try + { + await Operator.InitializeAsync(ct); + } + catch (Exception ex) + { + Log(ExecutionLog.Error("Operator", $"Operator initialisation failed: {ex.Message}")); + } + } + + Log(ExecutionLog.Info("Operator", $"Operator ready: {Operator.DisplayName} ({Operator.AvailableTools.Count} tool(s) available)")); + } + if (Compressor is not null) Log(ExecutionLog.Info("Compression", $"Compression enabled: {Compressor.StrategyName}")); @@ -91,6 +115,7 @@ public async Task ExecuteAsync(CancellationToken ct = default) _context, Chairman, KnowledgeKeeper, + Operator, _maxRounds, _temperature, round => @@ -101,6 +126,10 @@ public async Task ExecuteAsync(CancellationToken ct = default) foreach (var ki in round.KnowledgeInteractions) Log(ExecutionLog.Info("KnowledgeKeeper", $"Query: \"{Truncate(ki.Query, 100)}\" → {ki.SourceChunks} chunks")); + // Log operator interactions + foreach (var oi in round.OperatorInteractions) + Log(ExecutionLog.Info("Operator", $"{oi.RequesterName} → \"{Truncate(oi.Task, 100)}\" ({oi.ToolCallCount} tool call(s))")); + // Log participant responses foreach (var (member, response) in round.Responses) Log(ExecutionLog.Trace("Participant", $"{member} responded ({response.Length} chars)")); @@ -186,6 +215,13 @@ public string GetInfo() sb.AppendLine($" 📚 {KnowledgeKeeper.DisplayName} (collection: {KnowledgeKeeper.CollectionName})"); } + if (Operator is not null) + { + sb.AppendLine(); + sb.AppendLine(" ── Operator ──"); + sb.AppendLine($" 🛠️ {Operator.DisplayName} ({Operator.AvailableTools.Count} tool(s))"); + } + if (Compressor is not null) { sb.AppendLine(); diff --git a/src/Delibera.Core/Council/Operator.cs b/src/Delibera.Core/Council/Operator.cs new file mode 100644 index 0000000..d9385d5 --- /dev/null +++ b/src/Delibera.Core/Council/Operator.cs @@ -0,0 +1,354 @@ +using System.Text.Json; + +namespace Delibera.Core.Council; + +/// +/// The Operator — a special council role implemented as a self-contained micro-agent. +/// It connects to one or more MCP (Model Context Protocol) servers, exposes their tools +/// to debate participants, and fulfils natural-language tasks delegated by participants: +/// it interprets the task with its own (typically cheaper) LLM model, selects and invokes +/// the appropriate MCP tools, interprets the raw results, optionally compresses them using +/// the debate's compression strategy, and returns a concise answer to the requester. +/// +/// +/// The Operator deliberately uses a less expensive model than the debate participants — +/// its job is orchestration and interpretation of tool output, not deep reasoning, so a +/// cheaper model keeps tool usage economical. +/// +public sealed class Operator : IOperator +{ + private readonly CompressionOptions? _compressionOptions; + private readonly IContextCompressor? _compressor; + private readonly List _interactions = []; + private readonly Dictionary _mcpClients; + private readonly CouncilMember _model; + private readonly List _tools = []; + + /// + /// Creates an Operator. + /// + /// The (cheaper) LLM model the Operator uses to plan and interpret tool output. + /// MCP server clients the Operator can use. + /// Optional compressor used to shrink large tool results before returning them. + /// Optional compression options. + public Operator( + CouncilMember model, + IEnumerable mcpClients, + IContextCompressor? compressor = null, + CompressionOptions? compressionOptions = null) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + ArgumentNullException.ThrowIfNull(mcpClients); + + _mcpClients = mcpClients.ToDictionary(c => c.ServerName, StringComparer.OrdinalIgnoreCase); + _compressor = compressor; + _compressionOptions = compressionOptions; + + if (_model.Role is null or "Expert") + _model.Role = "Operator"; + } + + /// + public string DisplayName => $"🛠️ Operator ({_model.ModelName})"; + + /// + public bool IsInitialized { get; private set; } + + /// + public IReadOnlyList AvailableTools => _tools.AsReadOnly(); + + /// + public IReadOnlyList Interactions => _interactions.AsReadOnly(); + + /// + public async Task InitializeAsync(CancellationToken ct = default) + { + if (IsInitialized) return; + + _tools.Clear(); + foreach (var client in _mcpClients.Values) + try + { + await client.ConnectAsync(ct); + var tools = await client.ListToolsAsync(ct); + _tools.AddRange(tools); + } + catch (Exception ex) + { + // A failing server should not abort the whole debate — skip it but keep going. + _interactions.Add(new OperatorInteraction( + "System", + $"Connect to MCP server '{client.ServerName}'", + $"[Failed to initialise server '{client.ServerName}': {ex.Message}]", + [], + false, + DateTime.UtcNow)); + } + + IsInitialized = true; + } + + /// + public string GetToolCatalog() + { + if (_tools.Count == 0) + return "The Operator currently has no tools available."; + + var sb = new StringBuilder(); + sb.AppendLine("The Operator can perform the following actions via connected MCP servers:"); + foreach (var byServer in _tools.GroupBy(t => t.ServerName)) + { + sb.AppendLine($"\n• Server \"{byServer.Key}\":"); + foreach (var tool in byServer) + { + var desc = string.IsNullOrWhiteSpace(tool.Description) ? "(no description)" : tool.Description.Trim(); + sb.AppendLine($" - {tool.QualifiedName}: {desc}"); + } + } + + return sb.ToString(); + } + + /// + public async Task ExecuteTaskAsync( + string requesterName, + string task, + CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(requesterName); + ArgumentException.ThrowIfNullOrWhiteSpace(task); + + if (!IsInitialized) + await InitializeAsync(ct); + + // 1. Plan: ask the model which tool(s) to call. + var plannedCalls = _tools.Count == 0 + ? [] + : await PlanToolCallsAsync(task, ct); + + // 2. Execute the planned tool calls. + var executed = new List(); + foreach (var plan in plannedCalls) + { + if (!_mcpClients.TryGetValue(plan.ServerName, out var client)) + { + executed.Add(new OperatorToolCall(plan.ServerName, plan.ToolName, plan.Arguments, + $"[Unknown MCP server '{plan.ServerName}']", true)); + continue; + } + + var toolResult = await client.CallToolAsync(plan.ToolName, plan.Arguments, ct); + executed.Add(new OperatorToolCall(plan.ServerName, plan.ToolName, plan.Arguments, + toolResult.Text, toolResult.IsError)); + } + + // 3. Interpret the results (or answer directly if no tools were used). + var answer = await InterpretAsync(requesterName, task, executed, ct); + + // 4. Optionally compress the answer using the debate's compression strategy. + var compressed = false; + if (_compressor is not null && !string.IsNullOrWhiteSpace(answer)) + try + { + var result = await _compressor.CompressAsync(answer, _compressionOptions, ct); + if (!string.IsNullOrWhiteSpace(result.Text)) + { + answer = result.Text; + compressed = true; + } + } + catch + { + // Compression is best-effort; keep the uncompressed answer on failure. + } + + var operatorResult = new OperatorResult(requesterName, task, answer, executed.AsReadOnly(), compressed); + _interactions.Add(operatorResult.ToInteraction()); + return operatorResult; + } + + // ────────────────────────────────────────────── + // Micro-agent internals + // ────────────────────────────────────────────── + + private async Task> PlanToolCallsAsync(string task, CancellationToken ct) + { + const string SystemPrompt = """ + You are the Operator's planner — a tool-routing micro-agent. + Given a task and a list of available MCP tools, decide which tools to call. + Respond with STRICT JSON only, no prose, in exactly this shape: + {"tool_calls":[{"server":"","tool":"","arguments":{ ... }}]} + Rules: + - Use only tools from the provided list (match server and tool names exactly). + - Provide arguments that satisfy each tool's input schema. + - If no tool is appropriate, return {"tool_calls":[]}. + - Do not wrap the JSON in markdown fences. + """; + + var toolsText = new StringBuilder(); + foreach (var tool in _tools) + toolsText.AppendLine($"- server=\"{tool.ServerName}\" tool=\"{tool.Name}\" description=\"{tool.Description}\" input_schema={tool.InputSchemaJson}"); + + var userPrompt = $""" + ### Available tools: + {toolsText} + + ### Task: + {task} + + Return the JSON plan now. + """; + + string raw; + try + { + raw = await _model.AskAsync(SystemPrompt, userPrompt, 0.1f, ct); + } + catch + { + return []; + } + + return ParsePlan(raw); + } + + private static IReadOnlyList ParsePlan(string raw) + { + var json = ExtractJson(raw); + if (string.IsNullOrWhiteSpace(json)) return []; + + try + { + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("tool_calls", out var callsEl) || + callsEl.ValueKind != JsonValueKind.Array) + return []; + + var calls = new List(); + foreach (var callEl in callsEl.EnumerateArray()) + { + var server = callEl.TryGetProperty("server", out var s) ? s.GetString() : null; + var tool = callEl.TryGetProperty("tool", out var t) ? t.GetString() : null; + if (string.IsNullOrWhiteSpace(server) || string.IsNullOrWhiteSpace(tool)) + continue; + + var args = new Dictionary(); + if (callEl.TryGetProperty("arguments", out var argsEl) && argsEl.ValueKind == JsonValueKind.Object) + foreach (var prop in argsEl.EnumerateObject()) + args[prop.Name] = JsonElementToObject(prop.Value); + + // ResultText is filled in after execution; placeholder here. + calls.Add(new OperatorToolCall(server, tool, args, string.Empty, false)); + } + + return calls; + } + catch + { + return []; + } + } + + private async Task InterpretAsync( + string requesterName, + string task, + IReadOnlyList toolCalls, + CancellationToken ct) + { + string systemPrompt; + string userPrompt; + + if (toolCalls.Count == 0) + { + systemPrompt = """ + You are the Operator — an assistant micro-agent serving an AI council debate. + No MCP tool was applicable to this task. Answer the requester as best you can + from general knowledge, and clearly state that the answer is not tool-sourced. + Be concise and factual. + """; + userPrompt = $""" + Requester: {requesterName} + Task: {task} + + Provide a concise answer. + """; + } + else + { + var resultsText = new StringBuilder(); + for (var i = 0; i < toolCalls.Count; i++) + { + var c = toolCalls[i]; + resultsText.AppendLine($"[Tool {i + 1}] {c.ServerName}.{c.ToolName}{(c.IsError ? " (ERROR)" : "")}"); + resultsText.AppendLine(c.ResultText); + resultsText.AppendLine(); + } + + systemPrompt = """ + You are the Operator — an assistant micro-agent serving an AI council debate. + You delegated the requester's task to MCP tools and received their raw output below. + Interpret and synthesise the results into a clear, concise answer for the requester. + Cite which tool(s) produced the information. Do not invent facts beyond the tool output. + If a tool errored or returned nothing useful, say so plainly. + """; + userPrompt = $""" + Requester: {requesterName} + Task: {task} + + ### Raw tool output: + {resultsText} + + Synthesise the final answer for the requester now. + """; + } + + try + { + return await _model.AskAsync(systemPrompt, userPrompt, 0.3f, ct); + } + catch (Exception ex) + { + return $"[Operator failed to interpret results: {ex.Message}]"; + } + } + + // ────────────────────────────────────────────── + // JSON helpers + // ────────────────────────────────────────────── + + /// Extracts the first JSON object from a model response, tolerating markdown fences/prose. + private static string ExtractJson(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) return string.Empty; + + var start = raw.IndexOf('{'); + var end = raw.LastIndexOf('}'); + return start >= 0 && end > start ? raw[start..(end + 1)] : string.Empty; + } + + private static object? JsonElementToObject(JsonElement el) => el.ValueKind switch + { + JsonValueKind.String => el.GetString(), + JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Array => el.EnumerateArray().Select(JsonElementToObject).ToList(), + JsonValueKind.Object => el.EnumerateObject().ToDictionary(p => p.Name, p => JsonElementToObject(p.Value)), + _ => el.GetRawText() + }; + + /// + public async ValueTask DisposeAsync() + { + foreach (var client in _mcpClients.Values) + try + { + await client.DisposeAsync(); + } + catch + { + // ignore disposal errors + } + } +} diff --git a/src/Delibera.Core/Debate/ConsensusDebate.cs b/src/Delibera.Core/Debate/ConsensusDebate.cs index de994fa..65e8069 100644 --- a/src/Delibera.Core/Debate/ConsensusDebate.cs +++ b/src/Delibera.Core/Debate/ConsensusDebate.cs @@ -26,6 +26,7 @@ public override async Task ExecuteAsync( PromptContext context, CouncilMember? chairman, KnowledgeKeeper? knowledgeKeeper, + Operator? @operator, int maxRounds = 4, float temperature = 0.7f, Action? onRoundCompleted = null, @@ -33,9 +34,13 @@ public override async Task ExecuteAsync( { ArgumentNullException.ThrowIfNull(members); ArgumentNullException.ThrowIfNull(context); - var builder = new DebateResultBuilder(this, members, context, chairman, knowledgeKeeper); + var builder = new DebateResultBuilder(this, members, context, chairman, knowledgeKeeper, @operator); var fullUserPrompt = context.GetFullUserPrompt(); + // Operator briefing appended to participant system prompts. + var operatorBriefing = BuildOperatorBriefing(@operator); + var baseSystemPrompt = context.SystemPrompt + operatorBriefing; + if (chairman is not null) builder.SetOpeningStatement(await Chairman.OpenDebateAsync(chairman, context, members, StrategyName, maxRounds, temperature, ct)); @@ -54,8 +59,9 @@ public override async Task ExecuteAsync( : $"{fullUserPrompt}\n\n📚 Knowledge:\n{knowledgeCtx}"; // Round 1: Initial Perspectives - var r1 = await CollectResponsesAsync(members, context.SystemPrompt, enrichedPrompt, temperature, ct); - var round1 = CreateRound(1, "Initial Perspectives", "Each model shares their perspective.", r1, knowledgeInteractions: r1Ki); + var r1 = await CollectResponsesAsync(members, baseSystemPrompt, enrichedPrompt, temperature, ct); + var r1Op = await ProcessOperatorRequestsAsync(@operator, r1, ct); + var round1 = CreateRound(1, "Initial Perspectives", "Each model shares their perspective.", r1, knowledgeInteractions: r1Ki, operatorInteractions: r1Op); builder.AddRound(round1); onRoundCompleted?.Invoke(round1); if (maxRounds < 2) return BuildAndComplete(builder); @@ -70,8 +76,9 @@ public override async Task ExecuteAsync( // Round 2: Finding Common Ground var r1Text = FormatRoundResponses(round1); + var r1OpText = FormatOperatorInteractions(r1Op); var r2Sys = $""" - {context.SystemPrompt} + {baseSystemPrompt} Work collaboratively. Find COMMON GROUND. Identify agreements, disagreements, and propose bridges. Be open to changing your position. """; @@ -79,10 +86,12 @@ and propose bridges. Be open to changing your position. Original question: {fullUserPrompt} All perspectives: {r1Text} + {(string.IsNullOrWhiteSpace(r1OpText) ? "" : $"\n{r1OpText}")} Identify: 1) Points of Agreement 2) Points of Disagreement 3) Bridge Proposals 4) Your Updated Position """; var r2 = await CollectResponsesAsync(members, r2Sys, r2Prompt, temperature, ct); - var round2 = CreateRound(2, "Finding Common Ground", "Models identify agreements and disagreements.", r2, knowledgeInteractions: r2Ki); + var r2Op = await ProcessOperatorRequestsAsync(@operator, r2, ct); + var round2 = CreateRound(2, "Finding Common Ground", "Models identify agreements and disagreements.", r2, knowledgeInteractions: r2Ki, operatorInteractions: r2Op); builder.AddRound(round2); onRoundCompleted?.Invoke(round2); if (maxRounds < 3) return BuildAndComplete(builder); @@ -97,8 +106,9 @@ and propose bridges. Be open to changing your position. // Round 3: Consensus Building var r2Text = FormatRoundResponses(round2); + var r2OpText = FormatOperatorInteractions(r2Op); var r3Sys = $""" - {context.SystemPrompt} + {baseSystemPrompt} This is the CONSENSUS round. Formulate a single, unified answer. If full consensus is impossible, state what can be agreed upon. """; @@ -106,10 +116,12 @@ and propose bridges. Be open to changing your position. Original question: {fullUserPrompt} Perspectives: {r1Text} Common ground: {r2Text} + {(string.IsNullOrWhiteSpace(r1OpText) && string.IsNullOrWhiteSpace(r2OpText) ? "" : $"\n{r1OpText}\n{r2OpText}")} Formulate: 1) Agreed points 2) Proposed unified answer 3) Remaining disagreements 4) Confidence (Low/Medium/High) """; var r3 = await CollectResponsesAsync(members, r3Sys, r3Prompt, temperature, ct); - var round3 = CreateRound(3, "Consensus Building", "Models attempt a unified answer.", r3, knowledgeInteractions: r3Ki); + var r3Op = await ProcessOperatorRequestsAsync(@operator, r3, ct); + var round3 = CreateRound(3, "Consensus Building", "Models attempt a unified answer.", r3, knowledgeInteractions: r3Ki, operatorInteractions: r3Op); builder.AddRound(round3); onRoundCompleted?.Invoke(round3); diff --git a/src/Delibera.Core/Debate/CritiqueDebate.cs b/src/Delibera.Core/Debate/CritiqueDebate.cs index 1fb3ed0..3f39690 100644 --- a/src/Delibera.Core/Debate/CritiqueDebate.cs +++ b/src/Delibera.Core/Debate/CritiqueDebate.cs @@ -26,6 +26,7 @@ public override async Task ExecuteAsync( PromptContext context, CouncilMember? chairman, KnowledgeKeeper? knowledgeKeeper, + Operator? @operator, int maxRounds = 4, float temperature = 0.7f, Action? onRoundCompleted = null, @@ -33,9 +34,13 @@ public override async Task ExecuteAsync( { ArgumentNullException.ThrowIfNull(members); ArgumentNullException.ThrowIfNull(context); - var builder = new DebateResultBuilder(this, members, context, chairman, knowledgeKeeper); + var builder = new DebateResultBuilder(this, members, context, chairman, knowledgeKeeper, @operator); var fullUserPrompt = context.GetFullUserPrompt(); + // Operator briefing appended to participant system prompts. + var operatorBriefing = BuildOperatorBriefing(@operator); + var baseSystemPrompt = context.SystemPrompt + operatorBriefing; + if (chairman is not null) builder.SetOpeningStatement(await Chairman.OpenDebateAsync(chairman, context, members, StrategyName, maxRounds, temperature, ct)); @@ -54,8 +59,9 @@ public override async Task ExecuteAsync( : $"{fullUserPrompt}\n\n📚 Knowledge:\n{knowledgeCtx}"; // Round 1: Initial Positions - var r1 = await CollectResponsesAsync(members, context.SystemPrompt, enrichedPrompt, temperature, ct); - var round1 = CreateRound(1, "Initial Positions", "Each model states their initial position.", r1, knowledgeInteractions: r1Ki); + var r1 = await CollectResponsesAsync(members, baseSystemPrompt, enrichedPrompt, temperature, ct); + var r1Op = await ProcessOperatorRequestsAsync(@operator, r1, ct); + var round1 = CreateRound(1, "Initial Positions", "Each model states their initial position.", r1, knowledgeInteractions: r1Ki, operatorInteractions: r1Op); builder.AddRound(round1); onRoundCompleted?.Invoke(round1); if (maxRounds < 2) return BuildAndComplete(builder); @@ -70,8 +76,9 @@ public override async Task ExecuteAsync( // Round 2: Directed Critique var r1Text = FormatRoundResponses(round1); + var r1OpText = FormatOperatorInteractions(r1Op); var r2Sys = $""" - {context.SystemPrompt} + {baseSystemPrompt} You are a sharp analytical critic. Find the WEAKEST points in other participants' arguments. Challenge assumptions, find logical flaws, identify missing evidence, propose counter-examples. """; @@ -79,10 +86,12 @@ You are a sharp analytical critic. Find the WEAKEST points in other participants Original question: {fullUserPrompt} Responses to critique: {r1Text} + {(string.IsNullOrWhiteSpace(r1OpText) ? "" : $"\n{r1OpText}")} For each response: 1) Weakest argument 2) Logical fallacies 3) Counter-example 4) Quality (1-10) """; var r2 = await CollectResponsesAsync(members, r2Sys, r2Prompt, temperature, ct); - var round2 = CreateRound(2, "Directed Critique", "Models attack weaknesses in each other's positions.", r2, knowledgeInteractions: r2Ki); + var r2Op = await ProcessOperatorRequestsAsync(@operator, r2, ct); + var round2 = CreateRound(2, "Directed Critique", "Models attack weaknesses in each other's positions.", r2, knowledgeInteractions: r2Ki, operatorInteractions: r2Op); builder.AddRound(round2); onRoundCompleted?.Invoke(round2); if (maxRounds < 3) return BuildAndComplete(builder); @@ -97,8 +106,9 @@ You are a sharp analytical critic. Find the WEAKEST points in other participants // Round 3: Defence var r2Text = FormatRoundResponses(round2); + var r2OpText = FormatOperatorInteractions(r2Op); var r3Sys = $""" - {context.SystemPrompt} + {baseSystemPrompt} Defend your position. Address every critique, strengthen weak arguments, provide additional evidence, and concede where critics are right. """; @@ -106,10 +116,12 @@ You are a sharp analytical critic. Find the WEAKEST points in other participants Original question: {fullUserPrompt} Initial responses: {r1Text} Critiques: {r2Text} + {(string.IsNullOrWhiteSpace(r1OpText) && string.IsNullOrWhiteSpace(r2OpText) ? "" : $"\n{r1OpText}\n{r2OpText}")} Defend your position: 1) Address each criticism 2) Strengthen weak points 3) Concede where right 4) Final answer """; var r3 = await CollectResponsesAsync(members, r3Sys, r3Prompt, temperature, ct); - var round3 = CreateRound(3, "Defence & Counter-Arguments", "Models defend their positions.", r3, knowledgeInteractions: r3Ki); + var r3Op = await ProcessOperatorRequestsAsync(@operator, r3, ct); + var round3 = CreateRound(3, "Defence & Counter-Arguments", "Models defend their positions.", r3, knowledgeInteractions: r3Ki, operatorInteractions: r3Op); builder.AddRound(round3); onRoundCompleted?.Invoke(round3); diff --git a/src/Delibera.Core/Debate/DebateResultBuilder.cs b/src/Delibera.Core/Debate/DebateResultBuilder.cs index 1b81201..cf518ec 100644 --- a/src/Delibera.Core/Debate/DebateResultBuilder.cs +++ b/src/Delibera.Core/Debate/DebateResultBuilder.cs @@ -12,7 +12,8 @@ internal sealed class DebateResultBuilder( IReadOnlyList members, PromptContext context, CouncilMember? chairman, - KnowledgeKeeper? knowledgeKeeper) + KnowledgeKeeper? knowledgeKeeper, + Operator? @operator = null) { private readonly List _rounds = []; private string? _openingStatement; @@ -37,6 +38,7 @@ internal sealed class DebateResultBuilder( Participants = members.Select(m => m.DisplayName).ToList(), ChairmanName = chairman?.DisplayName, KnowledgeKeeperName = knowledgeKeeper?.DisplayName, + OperatorName = @operator?.DisplayName, OpeningStatement = _openingStatement, Rounds = _rounds, FinalVerdict = _finalVerdict, diff --git a/src/Delibera.Core/Debate/DebateScenario.cs b/src/Delibera.Core/Debate/DebateScenario.cs index 286d22b..c0f7fa5 100644 --- a/src/Delibera.Core/Debate/DebateScenario.cs +++ b/src/Delibera.Core/Debate/DebateScenario.cs @@ -21,6 +21,7 @@ public abstract Task ExecuteAsync( PromptContext context, CouncilMember? chairman, KnowledgeKeeper? knowledgeKeeper, + Operator? @operator, int maxRounds = 4, float temperature = 0.7f, Action? onRoundCompleted = null, @@ -81,7 +82,8 @@ protected static DebateRound CreateRound( string? description, Dictionary responses, string? prompt = null, - IReadOnlyList? knowledgeInteractions = null) => + IReadOnlyList? knowledgeInteractions = null, + IReadOnlyList? operatorInteractions = null) => new() { RoundNumber = number, @@ -90,6 +92,7 @@ protected static DebateRound CreateRound( Responses = responses, RoundPrompt = prompt, KnowledgeInteractions = knowledgeInteractions ?? [], + OperatorInteractions = operatorInteractions ?? [], CompletedAt = DateTime.UtcNow }; @@ -155,4 +158,105 @@ protected static DebateRound CreateRound( return (string.Empty, null); } } + + // ────────────────────────────────────────────── + // Operator helpers + // ────────────────────────────────────────────── + + /// + /// Marker participants use to delegate a task to the Operator, e.g.: + /// [[OPERATOR: search the web for the latest .NET 10 release notes]]. + /// + private static readonly System.Text.RegularExpressions.Regex OperatorRequestRegex = + new(@"\[\[\s*OPERATOR\s*:\s*(?.+?)\]\]", + System.Text.RegularExpressions.RegexOptions.Singleline | + System.Text.RegularExpressions.RegexOptions.IgnoreCase | + System.Text.RegularExpressions.RegexOptions.Compiled); + + /// + /// Builds an "Operator briefing" describing the Operator's tools and how to delegate + /// tasks to it. Appended to the participants' system prompt so they know what is available. + /// Returns an empty string when no Operator is configured. + /// + protected static string BuildOperatorBriefing(Operator? @operator) + { + if (@operator is null) return string.Empty; + + return $""" + + ── OPERATOR (tools available) ── + A shared Operator agent is available to all participants. + {@operator.GetToolCatalog()} + + To delegate a task to the Operator, include a line in your response using this exact marker: + [[OPERATOR: ]] + For example: [[OPERATOR: search the web for recent benchmarks comparing PostgreSQL and MySQL]] + The Operator's answer will be provided to all participants in the next round. + Only delegate when external information or actions (web search, database lookup, writing to Notion, etc.) are genuinely needed. + """; + } + + /// + /// Scans participant responses for Operator request markers, executes each delegated + /// task via the Operator, and returns the recorded interactions. + /// + /// Operator instance (may be null). + /// Participant responses keyed by display name. + /// Cancellation token. + /// Operator interactions produced during this round. + protected static async Task> ProcessOperatorRequestsAsync( + Operator? @operator, + IReadOnlyDictionary responses, + CancellationToken ct = default) + { + if (@operator is null || responses.Count == 0) return []; + + var interactions = new List(); + foreach (var (member, response) in responses) + { + if (string.IsNullOrWhiteSpace(response)) continue; + + foreach (System.Text.RegularExpressions.Match match in OperatorRequestRegex.Matches(response)) + { + var task = match.Groups["task"].Value.Trim(); + if (string.IsNullOrWhiteSpace(task)) continue; + + try + { + var result = await @operator.ExecuteTaskAsync(member, task, ct); + interactions.Add(result.ToInteraction()); + } + catch (Exception ex) + { + interactions.Add(new OperatorInteraction( + member, task, $"[Operator error: {ex.Message}]", [], false, DateTime.UtcNow)); + } + } + } + + return interactions; + } + + /// + /// Formats Operator interactions into a context block that can be injected into the + /// next round's prompt so participants can use the Operator's findings. + /// + protected static string FormatOperatorInteractions(IReadOnlyList interactions) + { + if (interactions is not { Count: > 0 }) return string.Empty; + + var sb = new StringBuilder(); + sb.AppendLine("🛠️ Operator results (requested by participants):"); + foreach (var i in interactions) + { + var tools = i.ToolCalls.Count > 0 + ? string.Join(", ", i.ToolCalls.Select(c => $"{c.ServerName}.{c.ToolName}")) + : "no tools"; + sb.AppendLine($"\n• {i.RequesterName} asked: {i.Task}"); + sb.AppendLine($" Tools used: {tools}"); + sb.AppendLine($" Answer: {i.Answer}"); + } + + return sb.ToString(); + } } diff --git a/src/Delibera.Core/Debate/StandardDebate.cs b/src/Delibera.Core/Debate/StandardDebate.cs index 1694070..83e9289 100644 --- a/src/Delibera.Core/Debate/StandardDebate.cs +++ b/src/Delibera.Core/Debate/StandardDebate.cs @@ -30,6 +30,7 @@ public override async Task ExecuteAsync( PromptContext context, CouncilMember? chairman, KnowledgeKeeper? knowledgeKeeper, + Operator? @operator, int maxRounds = 4, float temperature = 0.7f, Action? onRoundCompleted = null, @@ -37,9 +38,13 @@ public override async Task ExecuteAsync( { ArgumentNullException.ThrowIfNull(members); ArgumentNullException.ThrowIfNull(context); - var builder = new DebateResultBuilder(this, members, context, chairman, knowledgeKeeper); + var builder = new DebateResultBuilder(this, members, context, chairman, knowledgeKeeper, @operator); var fullUserPrompt = context.GetFullUserPrompt(); + // Operator briefing is appended to participant system prompts so they know what tools exist. + var operatorBriefing = BuildOperatorBriefing(@operator); + var baseSystemPrompt = context.SystemPrompt + operatorBriefing; + // ── Chairman opening ── if (chairman is not null) builder.SetOpeningStatement(await Chairman.OpenDebateAsync( @@ -60,9 +65,11 @@ public override async Task ExecuteAsync( ? fullUserPrompt : $"{fullUserPrompt}\n\n📚 Knowledge Keeper context:\n{knowledgeContext}"; - var r1Responses = await CollectResponsesAsync(members, context.SystemPrompt, r1Prompt, temperature, ct); + var r1Responses = await CollectResponsesAsync(members, baseSystemPrompt, r1Prompt, temperature, ct); + // Operator: fulfil any [[OPERATOR: ...]] requests raised in round 1. + var r1Op = await ProcessOperatorRequestsAsync(@operator, r1Responses, ct); var round1 = CreateRound(1, "Initial Responses", - "All models provide their initial answers.", r1Responses, r1Prompt, r1Ki); + "All models provide their initial answers.", r1Responses, r1Prompt, r1Ki, r1Op); builder.AddRound(round1); onRoundCompleted?.Invoke(round1); @@ -79,8 +86,9 @@ public override async Task ExecuteAsync( // ══════════ Round 2: Critique ══════════ var r1Text = FormatRoundResponses(round1); + var r1OpText = FormatOperatorInteractions(r1Op); var r2System = $""" - {context.SystemPrompt} + {baseSystemPrompt} You are now in critique mode. Critically analyse the responses from other council members. Identify strengths, weaknesses, factual errors, logical gaps, and areas for improvement. @@ -91,13 +99,14 @@ Be constructive but thorough. Initial responses: {r1Text} - + {(string.IsNullOrWhiteSpace(r1OpText) ? "" : $"\n{r1OpText}")} Provide your detailed critique of each response. """; var r2Responses = await CollectResponsesAsync(members, r2System, r2Prompt, temperature, ct); + var r2Op = await ProcessOperatorRequestsAsync(@operator, r2Responses, ct); var round2 = CreateRound(2, "Critique", - "Models critically analyse each other's responses.", r2Responses, r2Prompt, r2Ki); + "Models critically analyse each other's responses.", r2Responses, r2Prompt, r2Ki, r2Op); builder.AddRound(round2); onRoundCompleted?.Invoke(round2); @@ -114,8 +123,9 @@ Provide your detailed critique of each response. // ══════════ Round 3: Improved Responses ══════════ var r2Text = FormatRoundResponses(round2); + var r2OpText = FormatOperatorInteractions(r2Op); var r3System = $""" - {context.SystemPrompt} + {baseSystemPrompt} You have received critiques. Provide your FINAL, IMPROVED answer. Take the best ideas from all participants, address the critiques, @@ -129,13 +139,14 @@ and synthesise the most comprehensive response possible. Critiques: {r2Text} - + {(string.IsNullOrWhiteSpace(r1OpText) && string.IsNullOrWhiteSpace(r2OpText) ? "" : $"\n{r1OpText}\n{r2OpText}")} Provide your final, improved answer. """; var r3Responses = await CollectResponsesAsync(members, r3System, r3Prompt, temperature, ct); + var r3Op = await ProcessOperatorRequestsAsync(@operator, r3Responses, ct); var round3 = CreateRound(3, "Final Improved Responses", - "Models provide refined answers incorporating critiques.", r3Responses, r3Prompt, r3Ki); + "Models provide refined answers incorporating critiques.", r3Responses, r3Prompt, r3Ki, r3Op); builder.AddRound(round3); onRoundCompleted?.Invoke(round3); diff --git a/src/Delibera.Core/Delibera.Core.csproj b/src/Delibera.Core/Delibera.Core.csproj index a216279..998588b 100644 --- a/src/Delibera.Core/Delibera.Core.csproj +++ b/src/Delibera.Core/Delibera.Core.csproj @@ -4,7 +4,7 @@ net10.0 enable enable - 14.0 + preview true true true @@ -44,11 +44,12 @@ + - + \ No newline at end of file diff --git a/src/Delibera.Core/DependencyInjection/CouncilOptions.cs b/src/Delibera.Core/DependencyInjection/CouncilOptions.cs index 202144f..d24f156 100644 --- a/src/Delibera.Core/DependencyInjection/CouncilOptions.cs +++ b/src/Delibera.Core/DependencyInjection/CouncilOptions.cs @@ -30,6 +30,9 @@ public sealed class CouncilOptions /// RAG configuration options. public RagOptions Rag { get; set; } = new(); + /// Operator (MCP tool micro-agent) configuration options. + public OperatorConfig Operator { get; set; } = new(); + /// Output configuration options. public OutputOptions Output { get; set; } = new(); } @@ -101,6 +104,81 @@ public sealed class RagOptions public string? ConnectionString { get; set; } } +/// +/// Configuration options for the Operator (MCP tool micro-agent). +/// +/// +/// Bind this from configuration (e.g., Delibera:Operator) and use +/// to materialise instances +/// for ICouncilBuilder.WithOperator(...). +/// +public sealed class OperatorConfig +{ + /// Whether the Operator is enabled. + public bool Enabled { get; set; } + + /// Model name used by the Operator (typically a cheaper model than the participants). + public string ModelName { get; set; } = string.Empty; + + /// Whether the Operator reuses the council's compressor for large tool results. + public bool ReuseCompression { get; set; } = true; + + /// MCP servers the Operator connects to. + public List McpServers { get; set; } = []; + + /// Materialises the configured MCP servers into instances. + public IReadOnlyList ToServerConfigs() => + McpServers.Select(s => s.ToConfig()).ToList(); +} + +/// +/// Configuration for a single MCP server bound from configuration. +/// +public sealed class McpServerOptions +{ + /// Logical server name surfaced to participants (e.g., "web", "notion", "postgres"). + public string Name { get; set; } = string.Empty; + + /// Transport type: "Stdio" (default) or "Http". + public string Transport { get; set; } = "Stdio"; + + // ── Stdio ── + + /// Executable command for stdio transport (e.g., "npx", "uvx", "dotnet"). + public string? Command { get; set; } + + /// Command-line arguments for stdio transport. + public List Arguments { get; set; } = []; + + /// Working directory for the spawned stdio process. + public string? WorkingDirectory { get; set; } + + /// Environment variables passed to the stdio process. + public Dictionary EnvironmentVariables { get; set; } = []; + + // ── Http ── + + /// Endpoint URL for HTTP/SSE transport. + public string? Endpoint { get; set; } + + /// Additional HTTP headers (e.g., authorization) for HTTP transport. + public Dictionary AdditionalHeaders { get; set; } = []; + + /// Converts these options into a . + public Models.McpServerConfig ToConfig() => + string.Equals(Transport, "Http", StringComparison.OrdinalIgnoreCase) + ? Models.McpServerConfig.Http( + Name, + new Uri(Endpoint ?? throw new InvalidOperationException($"MCP server '{Name}' uses Http transport but has no Endpoint.")), + AdditionalHeaders) + : Models.McpServerConfig.Stdio( + Name, + Command ?? throw new InvalidOperationException($"MCP server '{Name}' uses Stdio transport but has no Command."), + Arguments, + EnvironmentVariables, + WorkingDirectory); +} + /// /// Configuration options for debate output files. /// diff --git a/src/Delibera.Core/Interfaces/ICouncilBuilder.cs b/src/Delibera.Core/Interfaces/ICouncilBuilder.cs index a5c9fd4..6f8cf80 100644 --- a/src/Delibera.Core/Interfaces/ICouncilBuilder.cs +++ b/src/Delibera.Core/Interfaces/ICouncilBuilder.cs @@ -43,6 +43,30 @@ public interface ICouncilBuilder /// This builder for fluent chaining. ICouncilBuilder WithKnowledgeKeeper(KnowledgeKeeper knowledgeKeeper); + /// Attaches a pre-configured Operator (MCP tool micro-agent). + /// Configured Operator instance. + /// This builder for fluent chaining. + ICouncilBuilder WithOperator(Operator @operator); + + /// + /// Creates and attaches an Operator from a (cheaper) model and one or more MCP server + /// configurations. The Operator connects to the servers, discovers their tools, and lets + /// participants delegate natural-language tasks to it during the debate. + /// + /// Model name used by the Operator (typically a cheaper model). + /// LLM provider for the Operator model. + /// MCP server configurations the Operator connects to. + /// + /// When true (default), the Operator reuses the council's configured compressor + /// (if any) to compress large tool results before returning them. + /// + /// This builder for fluent chaining. + ICouncilBuilder WithOperator( + string modelName, + ILLMProvider provider, + IEnumerable servers, + bool reuseCompression = true); + /// Sets the debate strategy. /// Strategy implementation. /// This builder for fluent chaining. diff --git a/src/Delibera.Core/Interfaces/ICouncilExecutor.cs b/src/Delibera.Core/Interfaces/ICouncilExecutor.cs index 0d0e29a..74a1362 100644 --- a/src/Delibera.Core/Interfaces/ICouncilExecutor.cs +++ b/src/Delibera.Core/Interfaces/ICouncilExecutor.cs @@ -17,6 +17,9 @@ public interface ICouncilExecutor /// Knowledge Keeper (may be null). KnowledgeKeeper? KnowledgeKeeper { get; } + /// Operator (may be null). + Operator? Operator { get; } + /// Debate strategy. IDebateStrategy Strategy { get; } diff --git a/src/Delibera.Core/Interfaces/IDebateStrategy.cs b/src/Delibera.Core/Interfaces/IDebateStrategy.cs index f79c518..9116976 100644 --- a/src/Delibera.Core/Interfaces/IDebateStrategy.cs +++ b/src/Delibera.Core/Interfaces/IDebateStrategy.cs @@ -21,6 +21,7 @@ public interface IDebateStrategy /// Prompt context (system / user prompt, knowledge). /// Chairman for moderation and verdict synthesis (may be null). /// Knowledge Keeper for RAG queries (may be null). + /// Operator micro-agent for tool/MCP delegation (may be null). /// Maximum number of rounds. /// Generation temperature. /// Callback invoked after each round. @@ -31,6 +32,7 @@ Task ExecuteAsync( PromptContext context, CouncilMember? chairman, KnowledgeKeeper? knowledgeKeeper, + Operator? @operator, int maxRounds = 4, float temperature = 0.7f, Action? onRoundCompleted = null, diff --git a/src/Delibera.Core/Interfaces/IMcpClient.cs b/src/Delibera.Core/Interfaces/IMcpClient.cs new file mode 100644 index 0000000..bd4649b --- /dev/null +++ b/src/Delibera.Core/Interfaces/IMcpClient.cs @@ -0,0 +1,41 @@ +namespace Delibera.Core.Interfaces; + +/// +/// Thin abstraction over a connection to a single MCP (Model Context Protocol) server. +/// Allows the micro-agent to discover and invoke tools +/// without coupling the rest of the framework to a specific MCP SDK, and keeps the +/// Operator unit-testable via fakes. +/// +public interface IMcpClient : IAsyncDisposable +{ + /// Logical name of the server (e.g., "web", "notion", "postgres"). + string ServerName { get; } + + /// Whether the client has been connected via . + bool IsConnected { get; } + + /// + /// Establishes the connection to the MCP server and performs the initial handshake. + /// + /// Cancellation token. + Task ConnectAsync(CancellationToken ct = default); + + /// + /// Lists the tools exposed by the connected MCP server. + /// + /// Cancellation token. + /// Discovered tools, tagged with this client's . + Task> ListToolsAsync(CancellationToken ct = default); + + /// + /// Invokes a tool on the MCP server. + /// + /// Name of the tool to call. + /// Arguments passed to the tool. + /// Cancellation token. + /// The tool's textual result and error flag. + Task CallToolAsync( + string toolName, + IReadOnlyDictionary arguments, + CancellationToken ct = default); +} diff --git a/src/Delibera.Core/Interfaces/IOperator.cs b/src/Delibera.Core/Interfaces/IOperator.cs new file mode 100644 index 0000000..5340091 --- /dev/null +++ b/src/Delibera.Core/Interfaces/IOperator.cs @@ -0,0 +1,51 @@ +namespace Delibera.Core.Interfaces; + +/// +/// The Operator — a special council role implemented as a self-contained micro-agent. +/// During a debate, participants can delegate tasks to the Operator in natural language +/// (e.g., "search the web for …", "save the debate summary to Notion", "query the +/// PostgreSQL employee table for …"). The Operator interprets the task with its own +/// (typically cheaper) LLM model, selects and invokes the appropriate MCP tools, +/// interprets the raw tool output, optionally compresses it, and returns a concise +/// answer to the requesting participant. +/// +public interface IOperator : IAsyncDisposable +{ + /// Display name shown in debate logs (e.g., "🛠️ Operator (llama3.2)"). + string DisplayName { get; } + + /// Whether has completed successfully. + bool IsInitialized { get; } + + /// All tools discovered across the configured MCP servers. + IReadOnlyList AvailableTools { get; } + + /// All interactions recorded during this session. + IReadOnlyList Interactions { get; } + + /// + /// Connects to every configured MCP server and discovers their tools. + /// Safe to call multiple times — subsequent calls are no-ops once initialised. + /// + /// Cancellation token. + Task InitializeAsync(CancellationToken ct = default); + + /// + /// Returns a human-readable catalogue of the Operator's capabilities, suitable for + /// injection into participant prompts so they understand what they can delegate. + /// + string GetToolCatalog(); + + /// + /// Executes a natural-language task on behalf of a participant: selects MCP tools, + /// invokes them, interprets the results, optionally compresses, and returns an answer. + /// + /// Display name of the requesting participant. + /// The natural-language task to fulfil. + /// Cancellation token. + /// The Operator's result, including the answer and the tool calls performed. + Task ExecuteTaskAsync( + string requesterName, + string task, + CancellationToken ct = default); +} diff --git a/src/Delibera.Core/Models/DebateResult.cs b/src/Delibera.Core/Models/DebateResult.cs index 4e76932..927aefd 100644 --- a/src/Delibera.Core/Models/DebateResult.cs +++ b/src/Delibera.Core/Models/DebateResult.cs @@ -25,6 +25,9 @@ public sealed record DebateResult /// Knowledge Keeper name (if assigned). public string? KnowledgeKeeperName { get; init; } + /// Operator name (if assigned). + public string? OperatorName { get; init; } + /// All debate rounds. public IReadOnlyList Rounds { get; init; } = []; @@ -97,6 +100,8 @@ public string ToMarkdown() sb.AppendLine($"**Chairman:** {ChairmanName}"); if (!string.IsNullOrEmpty(KnowledgeKeeperName)) sb.AppendLine($"**Knowledge Keeper:** {KnowledgeKeeperName}"); + if (!string.IsNullOrEmpty(OperatorName)) + sb.AppendLine($"**Operator:** {OperatorName}"); sb.AppendLine(); // Opening statement @@ -129,6 +134,23 @@ public string ToMarkdown() } } + // Operator interactions + if (round.OperatorInteractions is { Count: > 0 }) + { + sb.AppendLine("### 🛠️ Operator Interactions"); + sb.AppendLine(); + foreach (var oi in round.OperatorInteractions) + { + var tools = oi.ToolCalls.Count > 0 + ? string.Join(", ", oi.ToolCalls.Select(c => $"`{c.ServerName}.{c.ToolName}`")) + : "none"; + sb.AppendLine($"> **{oi.RequesterName}** requested: {oi.Task}"); + sb.AppendLine($"> **Tools used:** {tools}"); + sb.AppendLine($"> **Answer:** {oi.Answer}{(oi.Compressed ? " *(compressed)*" : "")}"); + sb.AppendLine(); + } + } + foreach (var (member, response) in round.Responses) { sb.AppendLine($"### {member}"); diff --git a/src/Delibera.Core/Models/DebateRound.cs b/src/Delibera.Core/Models/DebateRound.cs index 5a2495d..3a075c5 100644 --- a/src/Delibera.Core/Models/DebateRound.cs +++ b/src/Delibera.Core/Models/DebateRound.cs @@ -55,6 +55,9 @@ public sealed record DebateRound /// Knowledge Keeper queries & answers that occurred during this round. public IReadOnlyList KnowledgeInteractions { get; init; } = []; + /// Operator tasks delegated by participants that occurred during this round. + public IReadOnlyList OperatorInteractions { get; init; } = []; + /// Timestamp when the round started. public DateTime StartedAt { get; init; } = DateTime.UtcNow; diff --git a/src/Delibera.Core/Models/OperatorModels.cs b/src/Delibera.Core/Models/OperatorModels.cs new file mode 100644 index 0000000..fa163f8 --- /dev/null +++ b/src/Delibera.Core/Models/OperatorModels.cs @@ -0,0 +1,176 @@ +namespace Delibera.Core.Models; + +/// +/// Transport type used to connect to an MCP (Model Context Protocol) server. +/// +public enum McpTransportType +{ + /// Local server launched as a child process and communicating over standard I/O. + Stdio = 0, + + /// Remote server reached over HTTP / Server-Sent Events. + Http = 1 +} + +/// +/// Configuration describing how the connects to a single +/// MCP server. Supports both local (stdio) and remote (HTTP/SSE) servers. +/// +/// +/// Examples: +/// +/// +/// +/// Stdio: McpServerConfig.Stdio("web", "npx", ["-y", "@modelcontextprotocol/server-everything"]) +/// +/// +/// +/// +/// HTTP: McpServerConfig.Http("notion", new Uri("https://mcp.notion.com/sse")) +/// +/// +/// +/// +public sealed record McpServerConfig +{ + /// Logical server name surfaced to participants (e.g., "web", "notion", "postgres"). + public required string Name { get; init; } + + /// Transport used to reach this server. + public McpTransportType TransportType { get; init; } = McpTransportType.Stdio; + + // ── Stdio transport ── + + /// Executable command for stdio transport (e.g., "npx", "dotnet", "uvx"). + public string? Command { get; init; } + + /// Command-line arguments for stdio transport. + public IReadOnlyList Arguments { get; init; } = []; + + /// Working directory for the spawned stdio process. + public string? WorkingDirectory { get; init; } + + /// Environment variables passed to the stdio process (e.g., API keys). + public IReadOnlyDictionary EnvironmentVariables { get; init; } = + new Dictionary(); + + // ── HTTP transport ── + + /// Endpoint URI for HTTP/SSE transport. + public Uri? Endpoint { get; init; } + + /// Additional HTTP headers (e.g., authorization) for HTTP transport. + public IReadOnlyDictionary AdditionalHeaders { get; init; } = + new Dictionary(); + + /// Creates a stdio MCP server configuration. + public static McpServerConfig Stdio( + string name, + string command, + IReadOnlyList? arguments = null, + IReadOnlyDictionary? environmentVariables = null, + string? workingDirectory = null) => + new() + { + Name = name, + TransportType = McpTransportType.Stdio, + Command = command, + Arguments = arguments ?? [], + EnvironmentVariables = environmentVariables ?? new Dictionary(), + WorkingDirectory = workingDirectory + }; + + /// Creates an HTTP/SSE MCP server configuration. + public static McpServerConfig Http( + string name, + Uri endpoint, + IReadOnlyDictionary? additionalHeaders = null) => + new() + { + Name = name, + TransportType = McpTransportType.Http, + Endpoint = endpoint, + AdditionalHeaders = additionalHeaders ?? new Dictionary() + }; +} + +/// +/// Describes a single tool exposed by an MCP server, as discovered by the Operator. +/// +/// Logical name of the server that owns this tool. +/// Tool name (as defined by the MCP server). +/// Human-readable tool description. +/// JSON schema for the tool's input arguments (may be empty). +public sealed record OperatorTool( + string ServerName, + string Name, + string Description, + string InputSchemaJson) +{ + /// Fully-qualified tool reference used when selecting tools (e.g., "web.search"). + public string QualifiedName => $"{ServerName}.{Name}"; +} + +/// +/// A single tool invocation performed by the Operator while fulfilling a request. +/// +/// Server that owns the tool. +/// Name of the invoked tool. +/// Arguments passed to the tool. +/// Textual result returned by the tool. +/// Whether the tool reported an error. +public sealed record OperatorToolCall( + string ServerName, + string ToolName, + IReadOnlyDictionary Arguments, + string ResultText, + bool IsError); + +/// +/// Result of a single MCP tool call at the transport level. +/// +/// Concatenated text content blocks returned by the tool. +/// Whether the tool reported an error. +public sealed record McpToolResult(string Text, bool IsError); + +/// +/// Records one complete Operator interaction during a debate — a participant's task, +/// the tools used to fulfil it, and the Operator's synthesised answer. +/// +/// Display name of the participant who requested the Operator. +/// The natural-language task delegated to the Operator. +/// The Operator's synthesised answer returned to the requester. +/// All MCP tool calls performed while fulfilling the task. +/// Whether the answer was compressed before being returned. +/// UTC timestamp when the interaction completed. +public sealed record OperatorInteraction( + string RequesterName, + string Task, + string Answer, + IReadOnlyList ToolCalls, + bool Compressed, + DateTime Timestamp) +{ + /// Number of tools invoked for this interaction. + public int ToolCallCount => ToolCalls.Count; +} + +/// +/// The Operator's response to a delegated task. +/// +/// Display name of the requester. +/// The original task. +/// The synthesised answer. +/// Tool calls performed. +/// Whether the answer was compressed. +public sealed record OperatorResult( + string RequesterName, + string Task, + string Answer, + IReadOnlyList ToolCalls, + bool Compressed) +{ + /// Converts this result into a loggable . + public OperatorInteraction ToInteraction() => + new(RequesterName, Task, Answer, ToolCalls, Compressed, DateTime.UtcNow); +} diff --git a/src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs b/src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs new file mode 100644 index 0000000..a4484e1 --- /dev/null +++ b/src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs @@ -0,0 +1,162 @@ +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; + +namespace Delibera.Core.Providers.Mcp; + +/// +/// Default implementation backed by the official +/// ModelContextProtocol C# SDK. Supports both stdio (local child process) and +/// HTTP/SSE (remote) MCP servers, selected via . +/// +public sealed class McpClientAdapter : IMcpClient +{ + private readonly McpServerConfig _config; + private McpClient? _client; + private bool _disposed; + + /// Creates an adapter for the given MCP server configuration. + /// Server connection configuration. + public McpClientAdapter(McpServerConfig config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + ArgumentException.ThrowIfNullOrWhiteSpace(config.Name); + } + + /// + public string ServerName => _config.Name; + + /// + public bool IsConnected => _client is not null; + + /// + public async Task ConnectAsync(CancellationToken ct = default) + { + if (_client is not null) return; + + var transport = CreateTransport(_config); + _client = await McpClient.CreateAsync(transport, cancellationToken: ct); + } + + /// + public async Task> ListToolsAsync(CancellationToken ct = default) + { + EnsureConnected(); + var tools = await _client!.ListToolsAsync(cancellationToken: ct); + + return tools + .Select(t => new OperatorTool( + ServerName, + t.Name, + t.Description ?? string.Empty, + SchemaToJson(t))) + .ToList() + .AsReadOnly(); + } + + /// + public async Task CallToolAsync( + string toolName, + IReadOnlyDictionary arguments, + CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(toolName); + ArgumentNullException.ThrowIfNull(arguments); + EnsureConnected(); + + CallToolResult result; + try + { + result = await _client!.CallToolAsync(toolName, arguments, cancellationToken: ct); + } + catch (Exception ex) + { + return new McpToolResult($"[MCP tool '{toolName}' failed: {ex.Message}]", true); + } + + var text = string.Join( + "\n", + result.Content.OfType().Select(b => b.Text)); + + if (string.IsNullOrWhiteSpace(text)) + text = "[Tool returned no textual content]"; + + return new McpToolResult(text, result.IsError ?? false); + } + + /// + public async ValueTask DisposeAsync() + { + if (_disposed) return; + _disposed = true; + + if (_client is not null) + await _client.DisposeAsync(); + } + + private void EnsureConnected() + { + if (_client is null) + throw new InvalidOperationException( + $"MCP client for server '{ServerName}' is not connected. Call ConnectAsync() first."); + } + + private static string SchemaToJson(McpClientTool tool) + { + try + { + return tool.JsonSchema.GetRawText(); + } + catch + { + return "{}"; + } + } + + private static IClientTransport CreateTransport(McpServerConfig config) => + config.TransportType switch + { + McpTransportType.Stdio => CreateStdioTransport(config), + McpTransportType.Http => CreateHttpTransport(config), + _ => throw new NotSupportedException($"Unsupported MCP transport: {config.TransportType}") + }; + + private static StdioClientTransport CreateStdioTransport(McpServerConfig config) + { + if (string.IsNullOrWhiteSpace(config.Command)) + throw new InvalidOperationException( + $"MCP server '{config.Name}' uses stdio transport but no Command was provided."); + + var options = new StdioClientTransportOptions + { + Name = config.Name, + Command = config.Command, + Arguments = config.Arguments.ToList(), + WorkingDirectory = config.WorkingDirectory + }; + + if (config.EnvironmentVariables.Count > 0) + options.EnvironmentVariables = config.EnvironmentVariables + .ToDictionary(kv => kv.Key, kv => (string?)kv.Value); + + return new StdioClientTransport(options); + } + + private static HttpClientTransport CreateHttpTransport(McpServerConfig config) + { + if (config.Endpoint is null) + throw new InvalidOperationException( + $"MCP server '{config.Name}' uses HTTP transport but no Endpoint was provided."); + + var options = new HttpClientTransportOptions + { + Name = config.Name, + Endpoint = config.Endpoint + }; + + if (config.AdditionalHeaders.Count > 0) + options.AdditionalHeaders = config.AdditionalHeaders + .ToDictionary(kv => kv.Key, kv => kv.Value); + + return new HttpClientTransport(options); + } +} diff --git a/src/Delibera.Core/README.md b/src/Delibera.Core/README.md index b9b9d6b..eb4f5f2 100644 --- a/src/Delibera.Core/README.md +++ b/src/Delibera.Core/README.md @@ -4,7 +4,7 @@ ### ⚖️ Thoughtful AI Decisions -**Collective decision making through structured AI deliberation — with RAG, pgvector, Knowledge Keeper, Chairman, Context Compression, Dependency Injection & Execution Logging.** +**Collective decision making through structured AI deliberation — with RAG, pgvector, Knowledge Keeper, Operator (MCP tools), Chairman, Context Compression, Dependency Injection & Execution Logging.** [![NuGet](https://img.shields.io/nuget/v/Delibera.Core.svg)](https://www.nuget.org/packages/Delibera.Core) [![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](../LICENSE) @@ -28,6 +28,7 @@ backed by **Qdrant** or **PostgreSQL/pgvector** (RAG), with **context compressio - 🏛️ **Multi-Model Councils** — orchestrate any number of LLM participants across structured debate rounds - ⚖️ **Chairman Synthesis** — a dedicated moderator opens, regulates and synthesises the final verdict - 📚 **Knowledge Keeper (RAG)** — per-round semantic retrieval with Qdrant or pgvector +- 🛠️ **Operator (MCP Tools)** — a micro-agent that delegates tasks to MCP servers (web search, file system, Notion, PostgreSQL…) on demand during the debate - 🗜️ **Context Compression** — 4 strategies (Semantic, Deduplication, Summarization, Hybrid) save 30–70% of tokens - 💉 **Dependency Injection** — `AddDelibera()` extension for `IServiceCollection` - 📋 **Execution Logging** — `ExecutionLog` with `LogLevel` for Chairman, KK, Compression & participants @@ -171,6 +172,95 @@ await keeper.IndexFileAsync("./docs/architecture.md"); --- +## 🛠️ Operator (MCP Tools) + +The **Operator** is a lightweight micro-agent that connects the council to the outside world through +[**MCP (Model Context Protocol)**](https://modelcontextprotocol.io) servers. It exposes whatever tools +those servers provide — web search, file system access, Notion, PostgreSQL, a debate-history store, etc. — +to the debate participants. + +**How it works** + +1. The Operator connects to one or more MCP servers and discovers their tools on `InitializeAsync`. +2. Participants are told (in their system prompt) what the Operator can do, and can delegate a task at + **any moment** during the debate by writing a marker in their message: + + ``` + [[OPERATOR: search the web for the latest EU AI Act timeline and save it to Notion]] + ``` + +3. The Operator interprets the request with its **own (cheaper) LLM model**, picks and calls the right + MCP tools, interprets the results, and returns a concise answer that is injected into the next round. +4. If the council uses **context compression**, the Operator can reuse the same strategy to compress + large tool outputs before they re-enter the debate. + +All Operator interactions are recorded per round and rendered in the final Markdown report under a +**🛠️ Operator Interactions** block. + +### Quick usage + +```csharp +using Delibera.Core.Council; +using Delibera.Core.Models; +using Delibera.Core.Providers.LLM; + +var ollama = new OllamaProvider("http://localhost:11434"); + +// Define the MCP servers the Operator may use +var servers = new[] +{ + // stdio transport — launches a local MCP server process + McpServerConfig.Stdio( + name: "everything", + command: "npx", + arguments: new[] { "-y", "@modelcontextprotocol/server-everything" }), + + McpServerConfig.Stdio( + name: "filesystem", + command: "npx", + arguments: new[] { "-y", "@modelcontextprotocol/server-filesystem", "/data" }), + + // …or an HTTP/SSE transport for a remote MCP server + // McpServerConfig.Http( + // name: "remote", + // endpoint: "https://my-mcp-host.example.com/mcp", + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }), +}; + +var council = new CouncilBuilder() + .AddMember("gpt-oss:20b", ollama, "Optimist") + .AddMember("llama3.1:8b", ollama, "Skeptic") + .WithChairman("gpt-oss:20b", ollama) + // Operator uses its own cheaper model; reuseCompression shares the council's compressor + .WithOperator("llama3.2:3b", ollama, servers, reuseCompression: true) + .WithTopic("Should we migrate the data pipeline to Kafka?") + .Build(); + +var result = await council.ExecuteAsync(); +``` + +Prefer to build the `Operator` yourself? Pass a pre-built instance: + +```csharp +var @operator = new Operator( + new CouncilMember("llama3.2:3b", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }, + compressor: null, // optional IContextCompressor + compressionOptions: null); // optional CompressionOptions + +var council = new CouncilBuilder() + /* …members… */ + .WithOperator(@operator) + .Build(); +``` + +### Dependency Injection + +Configure the Operator declaratively in `appsettings.json` under `Delibera:Operator` (see the +configuration section below), then build the council from `CouncilOptions` as usual. + +--- + ## 🗣️ Debate Strategies | Strategy | Flow | Use Case | @@ -214,6 +304,25 @@ var (resultPath, statsPath, logsPath) = await result.SaveAllAsync("./output"); }, "Compression": { "Enabled": true, "Strategy": "Hybrid", "TargetRatio": 0.5 }, "Rag": { "Enabled": false, "ProviderType": "Qdrant", "Host": "localhost", "Port": 6334 }, + "Operator": { + "Enabled": true, + "ModelName": "llama3.2:3b", + "ReuseCompression": true, + "McpServers": [ + { + "Name": "filesystem", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@modelcontextprotocol/server-filesystem", "/data" ] + }, + { + "Name": "remote", + "Transport": "Http", + "Endpoint": "https://my-mcp-host.example.com/mcp", + "AdditionalHeaders": { "Authorization": "Bearer " } + } + ] + }, "Output": { "Directory": "./debate_results", "SeparateFiles": true } } } @@ -231,6 +340,7 @@ in `Providers:ApiKey`. | `OllamaSharp` | Ollama API client | | `Qdrant.Client` | Qdrant vector DB gRPC client | | `Npgsql` / `Pgvector` | PostgreSQL/pgvector support | +| `ModelContextProtocol` | MCP client for the Operator role | | `Microsoft.Extensions.*` | Configuration, DI and Options | --- From eb557784beabb7e18ca8f46cd2bdae15be0bc7fd Mon Sep 17 00:00:00 2001 From: Victor Buzin Date: Tue, 16 Jun 2026 11:13:14 +0300 Subject: [PATCH 2/5] feat: integrate Microsoft.Extensions.AI (IChatClient/IEmbeddingGenerator) [v10.1.1] (#2) Add first-class interop with the standard Microsoft.Extensions.AI v10.7.0 abstractions while preserving the existing public API (additive only). - Delibera.Core: add Microsoft.Extensions.AI 10.7.0; bump 10.1.0 -> 10.1.1 - ChatClientLLMProvider: ILLMProvider implemented over IChatClient (+ streaming) - EmbeddingGeneratorProvider: IEmbeddingProvider over IEmbeddingGenerator - MicrosoftAIExtensions: AsLLMProvider/AsEmbeddingProvider/AsChatClient bridges + WithMiddleware (logging + function invocation) - ILLMProvider: additive default ChatStreamAsync (non-breaking) - OllamaProvider: expose native AsChatClient()/AsEmbeddingGenerator() - ProviderFactory.CreateFromChatClient + DI AddDeliberaChatClient/EmbeddingGenerator - ConsoleApp: --msai example - tests: new Delibera.Core.Tests (20 tests, fakes; no live network) - docs: README/README-RU/QuickStart/CHANGELOG + solution file --- CHANGELOG.md | 50 ++++++ Delibera.slnx | 9 + README-RU.md | 88 +++++++++- README.md | 88 +++++++++- docs/QuickStart-RU.md | 1 + docs/QuickStart.md | 1 + .../Examples/MicrosoftExtensionsAiExample.cs | 130 ++++++++++++++ src/Delibera.ConsoleApp/Program.cs | 6 + src/Delibera.Core/Delibera.Core.csproj | 5 +- .../ServiceCollectionExtensions.cs | 76 +++++++++ .../Extensions/MicrosoftAIExtensions.cs | 143 ++++++++++++++++ src/Delibera.Core/Interfaces/ILLMProvider.cs | 26 +++ .../Providers/LLM/ChatClientLLMProvider.cs | 160 ++++++++++++++++++ .../LLM/EmbeddingGeneratorProvider.cs | 104 ++++++++++++ .../Providers/LLM/OllamaProvider.cs | 15 ++ .../Providers/ProviderFactory.cs | 30 ++++ src/Delibera.Core/README.md | 2 + .../ChatClientLLMProviderTests.cs | 119 +++++++++++++ .../Delibera.Core.Tests.csproj | 26 +++ .../EmbeddingAndBridgeTests.cs | 142 ++++++++++++++++ .../Fakes/FakeChatClient.cs | 55 ++++++ .../Fakes/FakeEmbeddingGenerator.cs | 38 +++++ 22 files changed, 1308 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 Delibera.slnx create mode 100644 src/Delibera.ConsoleApp/Examples/MicrosoftExtensionsAiExample.cs create mode 100644 src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs create mode 100644 src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs create mode 100644 src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs create mode 100644 tests/Delibera.Core.Tests/ChatClientLLMProviderTests.cs create mode 100644 tests/Delibera.Core.Tests/Delibera.Core.Tests.csproj create mode 100644 tests/Delibera.Core.Tests/EmbeddingAndBridgeTests.cs create mode 100644 tests/Delibera.Core.Tests/Fakes/FakeChatClient.cs create mode 100644 tests/Delibera.Core.Tests/Fakes/FakeEmbeddingGenerator.cs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f84b6b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +All notable changes to **Delibera** are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [10.1.1] - 2026 + +### Added — Microsoft.Extensions.AI integration + +- **`Microsoft.Extensions.AI` 10.7.0** dependency in `Delibera.Core`. +- **`ChatClientLLMProvider`** — adapts any Microsoft.Extensions.AI `IChatClient` to Delibera's + `ILLMProvider`. Works with OpenAI, Azure OpenAI, Ollama, Anthropic and local OpenAI-compatible + servers (LM Studio, LocalAI, vLLM) without a bespoke provider per vendor. +- **`EmbeddingGeneratorProvider`** — adapts any `IEmbeddingGenerator>` + to Delibera's `IEmbeddingProvider` for RAG indexing/querying. +- **`ILLMProvider.ChatStreamAsync(...)`** — additive default interface method for token-by-token + streaming. `ChatClientLLMProvider` overrides it for true streaming; existing providers fall back + to a single `ChatAsync` call, so nothing breaks. +- **`MicrosoftAIExtensions`** bridge helpers: + - `IChatClient.AsLLMProvider(...)`, `IEmbeddingGenerator.AsEmbeddingProvider(...)` + - `ILLMProvider.AsChatClient(...)` (reverse bridge so Delibera providers can join a + Microsoft.Extensions.AI middleware pipeline) + - `IChatClient.WithMiddleware(...)` to compose function invocation + logging. +- **`ProviderFactory.CreateFromChatClient(...)`** — build a cached provider directly from an `IChatClient`. +- **`OllamaProvider.AsChatClient()` / `AsEmbeddingGenerator()`** — expose the underlying + OllamaSharp client (which natively implements the Microsoft.Extensions.AI interfaces). +- **DI helpers** `AddDeliberaChatClient(...)` and `AddDeliberaEmbeddingGenerator(...)` in + `ServiceCollectionExtensions`. +- **Console example** `MicrosoftExtensionsAiExample` (run with `--msai`). +- **Unit test project** `tests/Delibera.Core.Tests` (xUnit) covering the new providers, the bridge + adapters and the factory — plus a solution file `Delibera.slnx`. + +### Changed + +- Bumped `Delibera.Core` package version `10.1.0` → `10.1.1`. +- Documentation: `README.md` / `README-RU.md` and `docs/QuickStart*.md` updated with a + Microsoft.Extensions.AI section. + +### Compatibility + +- **No breaking changes.** The public API is fully backward compatible; all existing + `ILLMProvider` / `IEmbeddingProvider` consumers continue to work unchanged. + +## [10.1.0] - 2026 + +- Operator (MCP tools) role, .NET 10 / C# 15 (preview) upgrade, high-performance hot-path + optimizations (SIMD cosine similarity, allocation-free token counting, pooled cache hashing). +- Dependency injection, context compression, and RAG (Qdrant, pgvector) support. diff --git a/Delibera.slnx b/Delibera.slnx new file mode 100644 index 0000000..0534774 --- /dev/null +++ b/Delibera.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/README-RU.md b/README-RU.md index d4811dd..02d0b84 100644 --- a/README-RU.md +++ b/README-RU.md @@ -48,6 +48,7 @@ | **📋 Журналирование выполнения** | Модель `ExecutionLog` с `LogLevel` — события Chairman, KK, сжатия и участников | | **📁 Раздельный вывод файлов** | Экспорт `result.md`, `statistics.md` и `logs.md` по отдельности | | **🔌 Interface-First** | Чистые абстракции для провайдеров, фабрик, билдеров и исполнителей | +| **🤝 Microsoft.Extensions.AI** | Поддержка `IChatClient` / `IEmbeddingGenerator` — подключайте OpenAI, Azure OpenAI, Ollama и любые совместимые бэкенды, с middleware (function calling, логирование) | | **🧱 Современный C# 15 (preview)** | Построено на .NET 10 с `LangVersion=preview`, file-scoped namespaces, records, span/SIMD горячие пути | --- @@ -642,10 +643,92 @@ docker exec -it psql -U postgres -d council_vectors -c "CREATE EXTEN | `Npgsql` | ADO.NET-провайдер PostgreSQL | | `Pgvector` | Поддержка типов pgvector для Npgsql | | `ModelContextProtocol` | MCP-клиент для роли Operator | +| `Microsoft.Extensions.AI`| Унифицированные AI-абстракции `IChatClient` / `IEmbeddingGenerator` и middleware | | `Microsoft.Extensions.*` | Конфигурация, DI и Options | --- +## 🤝 Microsoft.Extensions.AI + +Delibera интегрирована с [**Microsoft.Extensions.AI**](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai) +(v10.7.0) — стандартным слоем абстракций .NET для генеративного AI. Это позволяет запускать совет +с **любым** бэкендом, реализующим `IChatClient` (OpenAI, Azure OpenAI, Ollama, Anthropic, +LM Studio / LocalAI / vLLM, …) и собирать **конвейер middleware** (вызов функций/инструментов, +логирование, кэширование, телеметрия) — без написания отдельного провайдера под каждого поставщика. + +### Что вы получаете + +| Тип | Роль | +| ---- | ---- | +| `ChatClientLLMProvider` | Адаптирует любой `IChatClient` к `ILLMProvider` Delibera (со **стримингом** через `ChatStreamAsync`) | +| `EmbeddingGeneratorProvider` | Адаптирует любой `IEmbeddingGenerator>` к `IEmbeddingProvider` для RAG | +| `MicrosoftAIExtensions` | Мосты: `AsLLMProvider()`, `AsEmbeddingProvider()`, `AsChatClient()`, `WithMiddleware()` | +| `ProviderFactory.CreateFromChatClient(...)` | Создаёт кэшируемый провайдер прямо из `IChatClient` | +| `AddDeliberaChatClient(...)` / `AddDeliberaEmbeddingGenerator(...)` | Регистрация стандартных AI-сервисов в DI | + +### Использование любого `IChatClient` как провайдера Delibera + +```csharp +using Delibera.Core.Extensions; +using Microsoft.Extensions.AI; + +// 1) Подключите свой IChatClient. Для OpenAI: +// dotnet add package Microsoft.Extensions.AI.OpenAI +IChatClient client = new OpenAI.Chat.ChatClient("gpt-4o-mini", apiKey).AsIChatClient(); + +// 2) (опционально) middleware — вызов функций + логирование +client = client.WithMiddleware(enableFunctionInvocation: true, loggerFactory); + +// 3) отдайте его Delibera +ILLMProvider provider = client.AsLLMProvider("OpenAI"); + +var executor = new CouncilBuilder() + .AddMember("gpt-4o-mini", provider, "Архитектор") + .AddMember("gpt-4o-mini", provider, "Скептик") + .SetChairman(Chairman.CreateStandard("gpt-4o-mini", provider)) + .WithStandardDebate() + .WithUserPrompt("Модульный монолит или микросервисы для команды из 5 человек?") + .Build(); +``` + +Ollama работает «из коробки», так как `OllamaApiClient` из OllamaSharp нативно реализует +`IChatClient` и `IEmbeddingGenerator`: + +```csharp +using var ollama = new OllamaProvider("http://localhost:11434"); +IChatClient chat = ollama.AsChatClient(); // готов к middleware +ILLMProvider provider = chat.AsLLMProvider("Ollama"); +IEmbeddingProvider embeddings = ollama.AsEmbeddingGenerator().AsEmbeddingProvider("nomic-embed-text"); +``` + +### Стриминг + +```csharp +await foreach (var chunk in provider.ChatStreamAsync("gpt-4o-mini", systemPrompt, userPrompt)) + Console.Write(chunk); +``` + +`ChatStreamAsync` — это additive-метод по умолчанию в `ILLMProvider`: провайдеры на базе +`IChatClient` стримят токен за токеном, а старые провайдеры прозрачно используют один вызов. + +### Внедрение зависимостей (DI) + +```csharp +services.AddDeliberaChatClient( + sp => new OpenAI.Chat.ChatClient("gpt-4o-mini", apiKey) + .AsIChatClient() + .WithMiddleware(enableFunctionInvocation: true), + providerName: "OpenAI"); + +services.AddDeliberaEmbeddingGenerator( + sp => /* ваш IEmbeddingGenerator */, + modelName: "text-embedding-3-small"); +``` + +> Попробуйте: `dotnet run --project src/Delibera.ConsoleApp -- --msai` + +--- + ## 🏛️ Архитектура ``` @@ -654,10 +737,11 @@ Delibera.Core ├── Debate/ ← StandardDebate, CritiqueDebate, ConsensusDebate ├── Compression/ ← Semantic / Deduplication / Summarization / Hybrid ├── Providers/ -│ ├── LLM/ ← OllamaProvider, OllamaEmbeddingProvider +│ ├── LLM/ ← OllamaProvider, ChatClientLLMProvider, EmbeddingGeneratorProvider │ ├── RAG/ ← QdrantRagProvider, PgVectorRagProvider │ └── Mcp/ ← McpClientAdapter (Operator ↔ MCP-серверы) -├── DependencyInjection/ ← AddDelibera() + CouncilOptions +├── Extensions/ ← MicrosoftAIExtensions (мосты IChatClient ↔ ILLMProvider) +├── DependencyInjection/ ← AddDelibera() / AddDeliberaChatClient() + CouncilOptions ├── Knowledge/ ← MarkdownKnowledgeBase ├── Models/ ← CouncilMember, DebateResult, DebateRound, TokenStatistics, ... └── Interfaces/ ← ILLMProvider, IRagProvider, IContextCompressor, IOperator, IMcpClient, ... diff --git a/README.md b/README.md index 86caed7..15c220d 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ well-reasoned outcomes** rather than single-model guesses. | **📋 Execution Logging** | `ExecutionLog` model with `LogLevel` — Chairman, KK, Compression & participant events | | **📁 Separate File Output** | Export `result.md`, `statistics.md`, and `logs.md` independently | | **🔌 Interface-First** | Clean abstractions for providers, factories, builders and executors | +| **🤝 Microsoft.Extensions.AI**| First-class support for `IChatClient` / `IEmbeddingGenerator` — plug in OpenAI, Azure OpenAI, Ollama or any compatible backend, with middleware (function calling, logging) | | **🧱 Modern C# 15 (preview)** | Built on .NET 10 with `LangVersion=preview`, file-scoped namespaces, records, span/SIMD hot paths | --- @@ -637,10 +638,92 @@ docker exec -it psql -U postgres -d council_vectors -c "CREATE EXTEN | `Npgsql` | PostgreSQL ADO.NET provider | | `Pgvector` | pgvector type support for Npgsql | | `ModelContextProtocol` | MCP client for the Operator role | +| `Microsoft.Extensions.AI`| Unified `IChatClient` / `IEmbeddingGenerator` AI abstractions & middleware | | `Microsoft.Extensions.*` | Configuration, DI and Options | --- +## 🤝 Microsoft.Extensions.AI + +Delibera integrates with [**Microsoft.Extensions.AI**](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai) +(v10.7.0) — the standard .NET abstraction layer for generative-AI services. This lets you drive a +council with **any** backend that ships an `IChatClient` (OpenAI, Azure OpenAI, Ollama, Anthropic, +LM Studio / LocalAI / vLLM, …) and compose a **middleware pipeline** (function/tool invocation, +logging, caching, telemetry) — all without writing a provider per vendor. + +### What you get + +| Type | Role | +| ---- | ---- | +| `ChatClientLLMProvider` | Adapts any `IChatClient` to Delibera's `ILLMProvider` (with **streaming** via `ChatStreamAsync`) | +| `EmbeddingGeneratorProvider` | Adapts any `IEmbeddingGenerator>` to `IEmbeddingProvider` for RAG | +| `MicrosoftAIExtensions` | Bridge helpers: `AsLLMProvider()`, `AsEmbeddingProvider()`, `AsChatClient()`, `WithMiddleware()` | +| `ProviderFactory.CreateFromChatClient(...)` | Build a cached provider straight from an `IChatClient` | +| `AddDeliberaChatClient(...)` / `AddDeliberaEmbeddingGenerator(...)` | Register the standard AI services in DI | + +### Use any `IChatClient` as a Delibera provider + +```csharp +using Delibera.Core.Extensions; +using Microsoft.Extensions.AI; + +// 1) Bring your own IChatClient. For OpenAI: +// dotnet add package Microsoft.Extensions.AI.OpenAI +IChatClient client = new OpenAI.Chat.ChatClient("gpt-4o-mini", apiKey).AsIChatClient(); + +// 2) (optional) compose middleware — function calling + logging +client = client.WithMiddleware(enableFunctionInvocation: true, loggerFactory); + +// 3) expose it to Delibera +ILLMProvider provider = client.AsLLMProvider("OpenAI"); + +var executor = new CouncilBuilder() + .AddMember("gpt-4o-mini", provider, "Architect") + .AddMember("gpt-4o-mini", provider, "Skeptic") + .SetChairman(Chairman.CreateStandard("gpt-4o-mini", provider)) + .WithStandardDebate() + .WithUserPrompt("Modular monolith or microservices for a 5-person team?") + .Build(); +``` + +Ollama works out of the box because OllamaSharp's `OllamaApiClient` natively implements +`IChatClient` and `IEmbeddingGenerator`: + +```csharp +using var ollama = new OllamaProvider("http://localhost:11434"); +IChatClient chat = ollama.AsChatClient(); // ready for middleware +ILLMProvider provider = chat.AsLLMProvider("Ollama"); +IEmbeddingProvider embeddings = ollama.AsEmbeddingGenerator().AsEmbeddingProvider("nomic-embed-text"); +``` + +### Streaming + +```csharp +await foreach (var chunk in provider.ChatStreamAsync("gpt-4o-mini", systemPrompt, userPrompt)) + Console.Write(chunk); +``` + +`ChatStreamAsync` is an additive default method on `ILLMProvider` — providers backed by an +`IChatClient` stream token-by-token, while older providers transparently fall back to a single call. + +### Dependency Injection + +```csharp +services.AddDeliberaChatClient( + sp => new OpenAI.Chat.ChatClient("gpt-4o-mini", apiKey) + .AsIChatClient() + .WithMiddleware(enableFunctionInvocation: true), + providerName: "OpenAI"); + +services.AddDeliberaEmbeddingGenerator( + sp => /* your IEmbeddingGenerator */, + modelName: "text-embedding-3-small"); +``` + +> Try it: `dotnet run --project src/Delibera.ConsoleApp -- --msai` + +--- + ## 🏛️ Architecture ``` @@ -649,10 +732,11 @@ Delibera.Core ├── Debate/ ← StandardDebate, CritiqueDebate, ConsensusDebate ├── Compression/ ← Semantic / Deduplication / Summarization / Hybrid ├── Providers/ -│ ├── LLM/ ← OllamaProvider, OllamaEmbeddingProvider +│ ├── LLM/ ← OllamaProvider, ChatClientLLMProvider, EmbeddingGeneratorProvider │ ├── RAG/ ← QdrantRagProvider, PgVectorRagProvider │ └── Mcp/ ← McpClientAdapter (Operator ↔ MCP servers) -├── DependencyInjection/ ← AddDelibera() + CouncilOptions +├── Extensions/ ← MicrosoftAIExtensions (IChatClient ↔ ILLMProvider bridges) +├── DependencyInjection/ ← AddDelibera() / AddDeliberaChatClient() + CouncilOptions ├── Knowledge/ ← MarkdownKnowledgeBase ├── Models/ ← CouncilMember, DebateResult, DebateRound, TokenStatistics, ... └── Interfaces/ ← ILLMProvider, IRagProvider, IContextCompressor, IOperator, IMcpClient, ... diff --git a/docs/QuickStart-RU.md b/docs/QuickStart-RU.md index 70918c1..c68ac59 100644 --- a/docs/QuickStart-RU.md +++ b/docs/QuickStart-RU.md @@ -434,6 +434,7 @@ var (resultPath, statsPath, logsPath) = await result.SaveAllAsync("./output"); | RAG (pgvector) | `dotnet run -- --pgvector` | Векторный поиск на базе PostgreSQL | | Operator (основы) | `dotnet run -- --operator` | Роль Operator с MCP-инструментами | | Operator + MCP | `dotnet run -- --operator-mcp` | 🆕 MCP-серверы browser + Marp в совете | +| Microsoft.Extensions.AI | `dotnet run -- --msai` | 🆕 `IChatClient`/`IEmbeddingGenerator` + middleware | --- diff --git a/docs/QuickStart.md b/docs/QuickStart.md index 0fa09b1..f1fda84 100644 --- a/docs/QuickStart.md +++ b/docs/QuickStart.md @@ -433,6 +433,7 @@ The repository ships with a runnable demo under | RAG (pgvector) | `dotnet run -- --pgvector` | PostgreSQL-backed vector search | | Operator (basics) | `dotnet run -- --operator` | Operator role with MCP tools | | Operator + MCP | `dotnet run -- --operator-mcp` | 🆕 Browser + Marp MCP servers in a council | +| Microsoft.Extensions.AI | `dotnet run -- --msai` | 🆕 `IChatClient`/`IEmbeddingGenerator` + middleware | --- diff --git a/src/Delibera.ConsoleApp/Examples/MicrosoftExtensionsAiExample.cs b/src/Delibera.ConsoleApp/Examples/MicrosoftExtensionsAiExample.cs new file mode 100644 index 0000000..fc6e05c --- /dev/null +++ b/src/Delibera.ConsoleApp/Examples/MicrosoftExtensionsAiExample.cs @@ -0,0 +1,130 @@ +using Delibera.Core.Council; +using Delibera.Core.Extensions; +using Delibera.Core.Interfaces; +using Delibera.Core.Providers; +using Delibera.Core.Providers.LLM; +using Microsoft.Extensions.AI; + +namespace Delibera.ConsoleApp.Examples; + +/// +/// Demonstrates the Microsoft.Extensions.AI integration: using the standard +/// / abstractions +/// inside Delibera, composing middleware, and bridging providers in both directions. +/// +/// +/// Run with: dotnet run --project src/Delibera.ConsoleApp -- --msai +/// +public static class MicrosoftExtensionsAiExample +{ + public static async Task RunAsync() + { + Console.WriteLine("🤝 Microsoft.Extensions.AI integration demo\n"); + + // ────────────────────────────────────────────────────────────── + // A. Use ANY IChatClient as a Delibera ILLMProvider + // ────────────────────────────────────────────────────────────── + // OllamaSharp's OllamaApiClient natively implements IChatClient, so we use it here. + // The same pattern applies to OpenAI / Azure OpenAI clients from Microsoft.Extensions.AI.OpenAI: + // + // IChatClient openAi = new OpenAI.Chat.ChatClient("gpt-4o-mini", apiKey).AsIChatClient(); + // ILLMProvider provider = openAi.AsLLMProvider("OpenAI"); + // + const string endpoint = "http://localhost:11434"; // local Ollama (agent VM) + const string model = "llama3.2"; + + using var ollama = new OllamaProvider(endpoint); + + // Grab the underlying IChatClient and wrap it with the standard middleware pipeline + // (function invocation + logging). This is the heart of "maximising the package potential". + IChatClient pipeline = ollama + .AsChatClient() + .WithMiddleware(enableFunctionInvocation: true); + + // Expose the decorated client back to Delibera as a normal provider. + ILLMProvider msaiProvider = pipeline.AsLLMProvider("Ollama (via Microsoft.Extensions.AI)"); + Console.WriteLine($" ✦ Provider name: {msaiProvider.ProviderName}"); + + // ────────────────────────────────────────────────────────────── + // B. Streaming via the unified abstraction + // ────────────────────────────────────────────────────────────── + Console.WriteLine("\n 💬 Streaming response (token-by-token):\n"); + try + { + await foreach (var chunk in msaiProvider.ChatStreamAsync( + model, + systemPrompt: "You are a concise assistant.", + userPrompt: "In one sentence, what is Microsoft.Extensions.AI?")) + Console.Write(chunk); + Console.WriteLine(); + } + catch (Exception ex) + { + PrintTips(ex); + return; + } + + // ────────────────────────────────────────────────────────────── + // C. Embeddings through IEmbeddingGenerator + // ────────────────────────────────────────────────────────────── + Console.WriteLine("\n 🧮 Embedding via IEmbeddingGenerator:"); + try + { + IEmbeddingProvider embeddings = ollama + .AsEmbeddingGenerator() + .AsEmbeddingProvider("nomic-embed-text"); + + var vector = await embeddings.EmbedAsync("Delibera deliberates."); + Console.WriteLine($" dimensions = {vector.Length}"); + } + catch (Exception ex) + { + Console.WriteLine($" ⚠️ {ex.Message} (embedding model may not be pulled)"); + } + + // ────────────────────────────────────────────────────────────── + // D. Plug the standard client into a full council via the factory + // ────────────────────────────────────────────────────────────── + Console.WriteLine("\n 🏛️ Building a council from an IChatClient..."); + using var factory = new ProviderFactory(); + var councilProvider = factory.CreateFromChatClient( + name: "msai", + chatClient: ollama.AsChatClient(), + providerName: "Ollama"); + + var executor = new CouncilBuilder() + .AddMember(model, councilProvider, "Architect") + .AddMember(model, councilProvider, "Skeptic") + .SetChairman(Chairman.CreateStandard(model, councilProvider)) + .WithStandardDebate() + .WithSystemPrompt("You are a software architecture expert.") + .WithUserPrompt("Is Microsoft.Extensions.AI a good abstraction for multi-provider apps?") + .WithMaxRounds(2) + .SaveResultTo("./results/msai_debate.md") + .Build(); + + Console.WriteLine(executor.GetInfo()); + + try + { + var result = await executor.ExecuteAsync(); + Console.WriteLine($"\n🏆 Completed in {result.TotalDuration.TotalSeconds:F1}s"); + Console.WriteLine("📁 Saved to: ./results/msai_debate.md"); + } + catch (Exception ex) + { + PrintTips(ex); + } + } + + private static void PrintTips(Exception ex) + { + Console.WriteLine($"\n❌ {ex.Message}"); + Console.WriteLine("\n💡 Tips:"); + Console.WriteLine(" • Start a local Ollama server: ollama serve"); + Console.WriteLine(" • Pull the model: ollama pull llama3.2"); + Console.WriteLine(" • Pull an embedding model: ollama pull nomic-embed-text"); + Console.WriteLine(" • To use OpenAI instead, add the Microsoft.Extensions.AI.OpenAI package"); + Console.WriteLine(" and build an IChatClient from your OpenAI key, then call .AsLLMProvider()."); + } +} diff --git a/src/Delibera.ConsoleApp/Program.cs b/src/Delibera.ConsoleApp/Program.cs index f6444bc..1fcb618 100644 --- a/src/Delibera.ConsoleApp/Program.cs +++ b/src/Delibera.ConsoleApp/Program.cs @@ -77,6 +77,12 @@ public static async Task Main(string[] args) return; } + if (args.Contains("--msai")) + { + await MicrosoftExtensionsAiExample.RunAsync(); + return; + } + // Quick DI showcase before main demo Console.WriteLine("🆕 v3.1 DI Quick Demo:"); Console.WriteLine(" Run with --di for full DI example"); diff --git a/src/Delibera.Core/Delibera.Core.csproj b/src/Delibera.Core/Delibera.Core.csproj index 998588b..2471c5b 100644 --- a/src/Delibera.Core/Delibera.Core.csproj +++ b/src/Delibera.Core/Delibera.Core.csproj @@ -12,7 +12,7 @@ Delibera.Core - 10.1.0 + 10.1.1 Delibera Delibera — Thoughtful AI Decisions. A framework for collective decision making through structured AI deliberation. Supports multi-model councils with RAG (Qdrant, PgVector), debate strategies, Knowledge Keeper, Chairman roles, dependency injection, and context compression. Victor Buzin @@ -25,7 +25,7 @@ https://github.com/techbuzzz/Delibera git https://github.com/techbuzzz/Delibera - v10.1.0 — Major version update with improved dependency injection, context compression, and RAG support. + v10.1.1 — Microsoft.Extensions.AI (10.7.0) integration: ChatClientLLMProvider and EmbeddingGeneratorProvider adapt any IChatClient / IEmbeddingGenerator (OpenAI, Azure OpenAI, Ollama, …), streaming via ChatStreamAsync, middleware composition, and AddDeliberaChatClient DI helpers. Fully backward compatible. true snupkg true @@ -38,6 +38,7 @@ + diff --git a/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs index b9e37be..6bd81d5 100644 --- a/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,7 +1,10 @@ using Delibera.Core.Compression; using Delibera.Core.Council; +using Delibera.Core.Interfaces; using Delibera.Core.Providers; +using Delibera.Core.Providers.LLM; using Delibera.Core.Providers.RAG; +using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -73,4 +76,77 @@ public static IServiceCollection AddDelibera( return services; } + + /// + /// Registers a Microsoft.Extensions.AI and exposes it as a Delibera + /// (). + /// + /// + /// Lets you wire any Microsoft.Extensions.AI backend (OpenAI, Azure OpenAI, Ollama, local + /// OpenAI-compatible servers) into the container and consume it through Delibera's provider + /// abstraction. The factory delegate may compose a middleware pipeline (function invocation, + /// logging, caching) before returning the client. + /// + /// The service collection. + /// Factory that builds the chat client (optionally with middleware). + /// Optional friendly provider name surfaced by . + public static IServiceCollection AddDeliberaChatClient( + this IServiceCollection services, + Func chatClientFactory, + string? providerName = null) + { + ArgumentNullException.ThrowIfNull(chatClientFactory); + + services.AddDelibera(); + services.TryAddSingleton(chatClientFactory); + // The DI container owns the IChatClient lifetime, so the provider must not dispose it. + services.TryAddSingleton(sp => + new ChatClientLLMProvider(sp.GetRequiredService(), providerName, ownsClient: false)); + + return services; + } + + /// + /// Registers an already-constructed Microsoft.Extensions.AI and exposes it + /// as a Delibera . + /// + /// The service collection. + /// The chat client instance. + /// Optional friendly provider name. + public static IServiceCollection AddDeliberaChatClient( + this IServiceCollection services, + IChatClient chatClient, + string? providerName = null) + { + ArgumentNullException.ThrowIfNull(chatClient); + return services.AddDeliberaChatClient(_ => chatClient, providerName); + } + + /// + /// Registers a Microsoft.Extensions.AI and exposes + /// it as a Delibera (). + /// + /// The service collection. + /// Factory that builds the embedding generator. + /// Optional friendly model name. + /// Optional known vector dimensionality. + public static IServiceCollection AddDeliberaEmbeddingGenerator( + this IServiceCollection services, + Func>> generatorFactory, + string? modelName = null, + int? vectorSize = null) + { + ArgumentNullException.ThrowIfNull(generatorFactory); + + services.AddDelibera(); + services.TryAddSingleton(generatorFactory); + services.TryAddSingleton(sp => + new EmbeddingGeneratorProvider( + sp.GetRequiredService>>(), + modelName, + vectorSize, + ownsGenerator: false)); + + return services; + } } diff --git a/src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs b/src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs new file mode 100644 index 0000000..66cfe27 --- /dev/null +++ b/src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs @@ -0,0 +1,143 @@ +using System.Runtime.CompilerServices; +using System.Text; +using Delibera.Core.Interfaces; +using Delibera.Core.Providers.LLM; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; + +namespace Delibera.Core.Extensions; + +/// +/// Bridging helpers between Delibera's provider abstractions (, +/// ) and the Microsoft.Extensions.AI abstractions +/// (, ). +/// +/// +/// These extensions let the two worlds interoperate in either direction: +/// +/// Adopt any Microsoft.Extensions.AI client as a Delibera provider via / . +/// Expose a Delibera provider as a Microsoft.Extensions.AI via , so it can participate in the standard middleware pipeline. +/// Compose middleware (function invocation, logging) with . +/// +/// +public static class MicrosoftAIExtensions +{ + /// + /// Adopts a Microsoft.Extensions.AI as a Delibera . + /// + /// The chat client to wrap. + /// Optional friendly provider name (defaults to client metadata). + /// Whether disposing the provider also disposes the client. + public static ILLMProvider AsLLMProvider(this IChatClient chatClient, string? providerName = null, bool ownsClient = true) + => new ChatClientLLMProvider(chatClient, providerName, ownsClient); + + /// + /// Adopts a Microsoft.Extensions.AI as a + /// Delibera . + /// + public static IEmbeddingProvider AsEmbeddingProvider( + this IEmbeddingGenerator> generator, + string? modelName = null, + int? vectorSize = null, + bool ownsGenerator = true) + => new EmbeddingGeneratorProvider(generator, modelName, vectorSize, ownsGenerator); + + /// + /// Exposes a Delibera as a Microsoft.Extensions.AI . + /// + /// + /// Use this to drop an existing Delibera provider into a Microsoft.Extensions.AI middleware pipeline + /// (caching, telemetry, function invocation). If the provider already is a + /// , its underlying client is returned directly to avoid a needless layer. + /// + /// The Delibera provider to expose. + /// Model id used when a request does not specify one. + public static IChatClient AsChatClient(this ILLMProvider provider, string? defaultModel = null) + { + ArgumentNullException.ThrowIfNull(provider); + if (provider is ChatClientLLMProvider ccp) return ccp.ChatClient; + return new LLMProviderChatClient(provider, defaultModel); + } + + /// + /// Composes a standard Microsoft.Extensions.AI middleware pipeline around an . + /// + /// The inner client. + /// Add automatic function (tool) invocation middleware. + /// When supplied, adds logging middleware. + /// The decorated client. + public static IChatClient WithMiddleware( + this IChatClient chatClient, + bool enableFunctionInvocation = false, + ILoggerFactory? loggerFactory = null) + { + ArgumentNullException.ThrowIfNull(chatClient); + + var builder = chatClient.AsBuilder(); + if (loggerFactory is not null) builder = builder.UseLogging(loggerFactory); + if (enableFunctionInvocation) builder = builder.UseFunctionInvocation(loggerFactory); + return builder.Build(); + } + + /// + /// Minimal adapter over a Delibera . + /// + private sealed class LLMProviderChatClient(ILLMProvider provider, string? defaultModel) : IChatClient + { + private readonly ChatClientMetadata _metadata = new(provider.ProviderName, defaultModelId: defaultModel); + + public async Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + var (system, user) = SplitMessages(messages); + var text = await provider.ChatAsync( + ResolveModel(options), + system, + user, + options?.Temperature ?? 0.7f, + cancellationToken); + + return new ChatResponse(new ChatMessage(ChatRole.Assistant, text)) { ModelId = ResolveModel(options) }; + } + + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var (system, user) = SplitMessages(messages); + await foreach (var chunk in provider.ChatStreamAsync( + ResolveModel(options), system, user, options?.Temperature ?? 0.7f, cancellationToken)) + yield return new ChatResponseUpdate(ChatRole.Assistant, chunk); + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + ArgumentNullException.ThrowIfNull(serviceType); + if (serviceKey is null && serviceType.IsInstanceOfType(_metadata)) return _metadata; + if (serviceKey is null && serviceType.IsInstanceOfType(provider)) return provider; + return null; + } + + public void Dispose() => provider.Dispose(); + + private string ResolveModel(ChatOptions? options) => + options?.ModelId is { Length: > 0 } m ? m : defaultModel ?? string.Empty; + + private static (string System, string User) SplitMessages(IEnumerable messages) + { + var system = new StringBuilder(); + var user = new StringBuilder(); + foreach (var message in messages) + { + var target = message.Role == ChatRole.System ? system : user; + if (target.Length > 0) target.Append('\n'); + target.Append(message.Text); + } + + return (system.ToString(), user.ToString()); + } + } +} diff --git a/src/Delibera.Core/Interfaces/ILLMProvider.cs b/src/Delibera.Core/Interfaces/ILLMProvider.cs index fc08380..0c9e300 100644 --- a/src/Delibera.Core/Interfaces/ILLMProvider.cs +++ b/src/Delibera.Core/Interfaces/ILLMProvider.cs @@ -30,4 +30,30 @@ Task ChatAsync( string userPrompt, float temperature = 0.7f, CancellationToken ct = default); + + /// + /// Streams a chat-completion response incrementally as text chunks. + /// + /// + /// This is an additive capability introduced alongside the Microsoft.Extensions.AI integration. + /// The default implementation falls back to a single call and yields the + /// whole answer at once, so existing providers keep working unchanged. Providers backed by an + /// IChatClient (see ) override it to deliver + /// true token-by-token streaming. + /// + /// Model name (e.g., "llama3.2", "gpt-4o-mini"). + /// System prompt that defines the model's role / context. + /// User prompt with the question or task. + /// Generation temperature (0.0 = deterministic, 1.0+ = creative). + /// Cancellation token. + /// An asynchronous stream of text chunks that together form the full response. + async IAsyncEnumerable ChatStreamAsync( + string model, + string systemPrompt, + string userPrompt, + float temperature = 0.7f, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default) + { + yield return await ChatAsync(model, systemPrompt, userPrompt, temperature, ct); + } } \ No newline at end of file diff --git a/src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs b/src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs new file mode 100644 index 0000000..76c5629 --- /dev/null +++ b/src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs @@ -0,0 +1,160 @@ +using Delibera.Core.Interfaces; +using Microsoft.Extensions.AI; + +namespace Delibera.Core.Providers.LLM; + +/// +/// A universal implemented on top of the Microsoft.Extensions.AI +/// abstraction. +/// +/// +/// +/// This provider lets Delibera talk to any chat backend that ships an +/// implementation — Ollama (via OllamaSharp), OpenAI / Azure OpenAI +/// (via Microsoft.Extensions.AI.OpenAI), Anthropic, local OpenAI-compatible servers +/// (LM Studio, LocalAI, vLLM), and so on — without writing a bespoke provider for each one. +/// +/// +/// Because it wraps an , it also benefits from the +/// Microsoft.Extensions.AI middleware pipeline (function invocation, caching, telemetry, +/// logging). Build a decorated client with helpers and +/// hand it to this provider. +/// +/// +public sealed class ChatClientLLMProvider : ILLMProvider +{ + private readonly IChatClient _chatClient; + private readonly bool _ownsClient; + private bool _disposed; + + /// + /// Wraps an existing as an . + /// + /// The Microsoft.Extensions.AI chat client to wrap. + /// + /// Friendly provider name. When null the name is taken from the client metadata + /// (), falling back to "ChatClient". + /// + /// + /// When true (default) the wrapped client is disposed together with this provider. + /// Set to false when the client lifetime is owned elsewhere (e.g., a DI container). + /// + public ChatClientLLMProvider(IChatClient chatClient, string? providerName = null, bool ownsClient = true) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + _ownsClient = ownsClient; + + var metadata = chatClient.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata; + ProviderName = providerName + ?? (string.IsNullOrWhiteSpace(metadata?.ProviderName) ? "ChatClient" : metadata!.ProviderName!); + DefaultModelId = metadata?.DefaultModelId; + } + + /// The default model id reported by the underlying client metadata (may be null). + public string? DefaultModelId { get; } + + /// Exposes the wrapped for advanced scenarios and middleware composition. + public IChatClient ChatClient => _chatClient; + + /// + public string ProviderName { get; } + + /// + public Task IsAvailableAsync(CancellationToken ct = default) + { + // A generic IChatClient has no universal health-check. We treat a live client as available; + // concrete transports surface failures on the first ChatAsync call. + return Task.FromResult(!_disposed); + } + + /// + /// + /// The Microsoft.Extensions.AI abstraction does not define a model-enumeration contract, + /// so this returns the default model id when known, otherwise an empty list. + /// + public Task> ListModelsAsync(CancellationToken ct = default) + { + IReadOnlyList models = string.IsNullOrWhiteSpace(DefaultModelId) + ? [] + : [DefaultModelId!]; + return Task.FromResult(models); + } + + /// + public async Task ChatAsync( + string model, + string systemPrompt, + string userPrompt, + float temperature = 0.7f, + CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(userPrompt); + + var messages = BuildMessages(systemPrompt, userPrompt); + var options = BuildOptions(model, temperature); + + try + { + var response = await _chatClient.GetResponseAsync(messages, options, ct); + var text = response.Text?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(text)) + throw new InvalidOperationException($"Empty response from model '{model}' ({ProviderName})."); + return text; + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + throw; + } + catch (Exception ex) when (ex is not InvalidOperationException) + { + throw new InvalidOperationException( + $"Error talking to {ProviderName} (model: {model}): {ex.Message}", ex); + } + } + + /// + public async IAsyncEnumerable ChatStreamAsync( + string model, + string systemPrompt, + string userPrompt, + float temperature = 0.7f, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(userPrompt); + + var messages = BuildMessages(systemPrompt, userPrompt); + var options = BuildOptions(model, temperature); + + await foreach (var update in _chatClient.GetStreamingResponseAsync(messages, options, ct)) + { + var text = update.Text; + if (!string.IsNullOrEmpty(text)) + yield return text; + } + } + + private static List BuildMessages(string systemPrompt, string userPrompt) + { + var messages = new List(2); + if (!string.IsNullOrWhiteSpace(systemPrompt)) + messages.Add(new ChatMessage(ChatRole.System, systemPrompt)); + messages.Add(new ChatMessage(ChatRole.User, userPrompt)); + return messages; + } + + private ChatOptions BuildOptions(string model, float temperature) => new() + { + // Prefer the explicitly requested model; fall back to the client's default. + ModelId = string.IsNullOrWhiteSpace(model) ? DefaultModelId : model, + Temperature = temperature + }; + + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + if (_ownsClient) _chatClient.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs b/src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs new file mode 100644 index 0000000..4de303f --- /dev/null +++ b/src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs @@ -0,0 +1,104 @@ +using Delibera.Core.Interfaces; +using Microsoft.Extensions.AI; + +namespace Delibera.Core.Providers.LLM; + +/// +/// A universal implemented on top of the Microsoft.Extensions.AI +/// abstraction. +/// +/// +/// Wraps any IEmbeddingGenerator<string, Embedding<float>> — Ollama (OllamaSharp), +/// OpenAI / Azure OpenAI, local OpenAI-compatible servers, etc. — so RAG indexing and querying +/// work with the standard .NET AI embedding contract instead of a provider-specific SDK call. +/// +public sealed class EmbeddingGeneratorProvider : IEmbeddingProvider, IDisposable +{ + private readonly IEmbeddingGenerator> _generator; + private readonly bool _ownsGenerator; + private int? _cachedVectorSize; + private bool _disposed; + + /// + /// Wraps an existing . + /// + /// The Microsoft.Extensions.AI embedding generator to wrap. + /// + /// Friendly model name. When null it is read from the generator metadata + /// (), falling back to "embedding". + /// + /// + /// Known vector dimensionality. When null it is auto-detected from metadata or on the + /// first call. + /// + /// + /// When true (default) the wrapped generator is disposed together with this provider. + /// + public EmbeddingGeneratorProvider( + IEmbeddingGenerator> generator, + string? modelName = null, + int? vectorSize = null, + bool ownsGenerator = true) + { + _generator = generator ?? throw new ArgumentNullException(nameof(generator)); + _ownsGenerator = ownsGenerator; + + var metadata = generator.GetService(typeof(EmbeddingGeneratorMetadata)) as EmbeddingGeneratorMetadata; + EmbeddingModelName = modelName + ?? (string.IsNullOrWhiteSpace(metadata?.DefaultModelId) ? "embedding" : metadata!.DefaultModelId!); + _cachedVectorSize = vectorSize ?? metadata?.DefaultModelDimensions; + } + + /// Exposes the wrapped generator for advanced scenarios and middleware composition. + public IEmbeddingGenerator> Generator => _generator; + + /// + public string EmbeddingModelName { get; } + + /// + public int VectorSize => _cachedVectorSize ?? + throw new InvalidOperationException( + "Vector size unknown — call EmbedAsync at least once so the provider can probe the model."); + + /// + public async Task EmbedAsync(string text, CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(text); + + var result = await _generator.GenerateAsync([text], cancellationToken: ct); + var embedding = result.FirstOrDefault() + ?? throw new InvalidOperationException($"No embedding returned by model '{EmbeddingModelName}'."); + + var vector = embedding.Vector.ToArray(); + _cachedVectorSize ??= vector.Length; + return vector; + } + + /// + public async Task> EmbedBatchAsync(IReadOnlyList texts, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(texts); + if (texts.Count == 0) return []; + + var result = await _generator.GenerateAsync(texts, cancellationToken: ct); + + var vectors = new List(texts.Count); + foreach (var embedding in result) + { + var vector = embedding.Vector.ToArray(); + _cachedVectorSize ??= vector.Length; + vectors.Add(vector); + } + + return vectors.AsReadOnly(); + } + + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + if (_ownsGenerator) _generator.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/Delibera.Core/Providers/LLM/OllamaProvider.cs b/src/Delibera.Core/Providers/LLM/OllamaProvider.cs index b9489cd..726dde9 100644 --- a/src/Delibera.Core/Providers/LLM/OllamaProvider.cs +++ b/src/Delibera.Core/Providers/LLM/OllamaProvider.cs @@ -41,6 +41,21 @@ public OllamaProvider(string endpoint, string apiKey = "") /// public string ProviderName => "Ollama"; + /// + /// Exposes the underlying OllamaSharp client as a Microsoft.Extensions.AI . + /// + /// + /// natively implements , so this lets the + /// Ollama provider plug into the standard Microsoft.Extensions.AI middleware pipeline + /// (function invocation, caching, telemetry). + /// + public Microsoft.Extensions.AI.IChatClient AsChatClient() => Client; + + /// + /// Exposes the underlying OllamaSharp client as a Microsoft.Extensions.AI embedding generator. + /// + public Microsoft.Extensions.AI.IEmbeddingGenerator> AsEmbeddingGenerator() => Client; + /// public async Task IsAvailableAsync(CancellationToken ct = default) { diff --git a/src/Delibera.Core/Providers/ProviderFactory.cs b/src/Delibera.Core/Providers/ProviderFactory.cs index e88bc5c..28a1455 100644 --- a/src/Delibera.Core/Providers/ProviderFactory.cs +++ b/src/Delibera.Core/Providers/ProviderFactory.cs @@ -1,4 +1,5 @@ using Delibera.Core.Providers.LLM; +using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; namespace Delibera.Core.Providers; @@ -126,6 +127,35 @@ public OllamaProvider CreateOllama(string endpoint, string apiKey = "") return provider; } + /// + /// Creates (or returns a cached) from any Microsoft.Extensions.AI + /// . + /// + /// + /// This is the entry point for plugging arbitrary Microsoft.Extensions.AI backends — OpenAI, Azure + /// OpenAI, Anthropic, local OpenAI-compatible servers — into a Delibera council through the standard + /// .NET AI abstractions. + /// + /// Unique instance name used for caching. + /// The chat client to wrap. + /// Optional friendly provider name (defaults to client metadata). + /// Whether disposing the factory also disposes the client. + public ChatClientLLMProvider CreateFromChatClient( + string name, + IChatClient chatClient, + string? providerName = null, + bool ownsClient = true) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(chatClient); + + if (GetInstance(name) is ChatClientLLMProvider existing) return existing; + + var provider = new ChatClientLLMProvider(chatClient, providerName, ownsClient); + CacheInstance(name, provider); + return provider; + } + /// protected override void DisposeInstances() { diff --git a/src/Delibera.Core/README.md b/src/Delibera.Core/README.md index eb4f5f2..b515425 100644 --- a/src/Delibera.Core/README.md +++ b/src/Delibera.Core/README.md @@ -33,6 +33,7 @@ backed by **Qdrant** or **PostgreSQL/pgvector** (RAG), with **context compressio - 💉 **Dependency Injection** — `AddDelibera()` extension for `IServiceCollection` - 📋 **Execution Logging** — `ExecutionLog` with `LogLevel` for Chairman, KK, Compression & participants - 📁 **Separate File Output** — export `result.md`, `statistics.md`, `logs.md` independently +- 🤝 **Microsoft.Extensions.AI** — first-class `IChatClient` / `IEmbeddingGenerator` interop with logging & function-invocation middleware - 🔌 **Interface-First** — clean abstractions for providers, factories, builders and executors - 🧱 **Modern C# 12** — file-scoped namespaces, records, init-only properties, global usings @@ -341,6 +342,7 @@ in `Providers:ApiKey`. | `Qdrant.Client` | Qdrant vector DB gRPC client | | `Npgsql` / `Pgvector` | PostgreSQL/pgvector support | | `ModelContextProtocol` | MCP client for the Operator role | +| `Microsoft.Extensions.AI`| `IChatClient` / `IEmbeddingGenerator` abstractions & middleware | | `Microsoft.Extensions.*` | Configuration, DI and Options | --- diff --git a/tests/Delibera.Core.Tests/ChatClientLLMProviderTests.cs b/tests/Delibera.Core.Tests/ChatClientLLMProviderTests.cs new file mode 100644 index 0000000..98a34c3 --- /dev/null +++ b/tests/Delibera.Core.Tests/ChatClientLLMProviderTests.cs @@ -0,0 +1,119 @@ +using Delibera.Core.Providers.LLM; +using Delibera.Core.Tests.Fakes; +using Microsoft.Extensions.AI; + +namespace Delibera.Core.Tests; + +public class ChatClientLLMProviderTests +{ + [Fact] + public void ProviderName_TakenFromClientMetadata_WhenNotSpecified() + { + using var client = new FakeChatClient(providerName: "Contoso"); + using var provider = new ChatClientLLMProvider(client); + + Assert.Equal("Contoso", provider.ProviderName); + } + + [Fact] + public void ProviderName_ExplicitOverridesMetadata() + { + using var client = new FakeChatClient(providerName: "Contoso"); + using var provider = new ChatClientLLMProvider(client, "MyName"); + + Assert.Equal("MyName", provider.ProviderName); + } + + [Fact] + public async Task ChatAsync_ReturnsClientText_AndForwardsModelAndTemperature() + { + var client = new FakeChatClient(reply: "hello world"); + using var provider = new ChatClientLLMProvider(client); + + var answer = await provider.ChatAsync("gpt-test", "system", "user", temperature: 0.42f); + + Assert.Equal("hello world", answer); + Assert.Equal("gpt-test", client.LastOptions?.ModelId); + Assert.Equal(0.42f, client.LastOptions?.Temperature); + } + + [Fact] + public async Task ChatAsync_PutsSystemAndUserMessagesInOrder() + { + var client = new FakeChatClient(); + using var provider = new ChatClientLLMProvider(client); + + await provider.ChatAsync("m", "the-system", "the-user"); + + Assert.NotNull(client.LastMessages); + Assert.Equal(2, client.LastMessages!.Count); + Assert.Equal(ChatRole.System, client.LastMessages[0].Role); + Assert.Equal("the-system", client.LastMessages[0].Text); + Assert.Equal(ChatRole.User, client.LastMessages[1].Role); + Assert.Equal("the-user", client.LastMessages[1].Text); + } + + [Fact] + public async Task ChatAsync_OmitsSystemMessage_WhenSystemPromptEmpty() + { + var client = new FakeChatClient(); + using var provider = new ChatClientLLMProvider(client); + + await provider.ChatAsync("m", "", "only-user"); + + Assert.Single(client.LastMessages!); + Assert.Equal(ChatRole.User, client.LastMessages![0].Role); + } + + [Fact] + public async Task ChatStreamAsync_YieldsChunks_ThatConcatenateToFullReply() + { + var client = new FakeChatClient(reply: "alpha beta gamma"); + using var provider = new ChatClientLLMProvider(client); + + var chunks = new List(); + await foreach (var chunk in provider.ChatStreamAsync("m", "s", "u")) + chunks.Add(chunk); + + Assert.True(chunks.Count > 1, "Streaming should produce multiple chunks."); + Assert.Equal("alpha beta gamma", string.Concat(chunks).Trim()); + } + + [Fact] + public async Task ChatAsync_Throws_OnEmptyReply() + { + var client = new FakeChatClient(reply: " "); + using var provider = new ChatClientLLMProvider(client); + + await Assert.ThrowsAsync(() => provider.ChatAsync("m", "s", "u")); + } + + [Fact] + public void Dispose_DisposesClient_WhenOwned() + { + var client = new FakeChatClient(); + var provider = new ChatClientLLMProvider(client, ownsClient: true); + provider.Dispose(); + Assert.True(client.Disposed); + } + + [Fact] + public void Dispose_LeavesClient_WhenNotOwned() + { + var client = new FakeChatClient(); + var provider = new ChatClientLLMProvider(client, ownsClient: false); + provider.Dispose(); + Assert.False(client.Disposed); + } + + [Fact] + public async Task ListModelsAsync_ReturnsDefaultModel() + { + using var client = new FakeChatClient(defaultModel: "default-x"); + using var provider = new ChatClientLLMProvider(client); + + var models = await provider.ListModelsAsync(); + + Assert.Contains("default-x", models); + } +} diff --git a/tests/Delibera.Core.Tests/Delibera.Core.Tests.csproj b/tests/Delibera.Core.Tests/Delibera.Core.Tests.csproj new file mode 100644 index 0000000..edddd78 --- /dev/null +++ b/tests/Delibera.Core.Tests/Delibera.Core.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Delibera.Core.Tests/EmbeddingAndBridgeTests.cs b/tests/Delibera.Core.Tests/EmbeddingAndBridgeTests.cs new file mode 100644 index 0000000..8f5305d --- /dev/null +++ b/tests/Delibera.Core.Tests/EmbeddingAndBridgeTests.cs @@ -0,0 +1,142 @@ +using Delibera.Core.Extensions; +using Delibera.Core.Interfaces; +using Delibera.Core.Providers; +using Delibera.Core.Providers.LLM; +using Delibera.Core.Tests.Fakes; +using Microsoft.Extensions.AI; + +namespace Delibera.Core.Tests; + +public class EmbeddingGeneratorProviderTests +{ + [Fact] + public void VectorSize_ReadFromMetadata() + { + using var gen = new FakeEmbeddingGenerator(dimensions: 8); + using var provider = new EmbeddingGeneratorProvider(gen); + + Assert.Equal(8, provider.VectorSize); + Assert.Equal("fake-embed", provider.EmbeddingModelName); + } + + [Fact] + public async Task EmbedAsync_ReturnsVectorOfCorrectSize() + { + using var gen = new FakeEmbeddingGenerator(dimensions: 5); + using var provider = new EmbeddingGeneratorProvider(gen); + + var vector = await provider.EmbedAsync("hello"); + + Assert.Equal(5, vector.Length); + } + + [Fact] + public async Task EmbedBatchAsync_ReturnsOneVectorPerInput() + { + using var gen = new FakeEmbeddingGenerator(dimensions: 3); + using var provider = new EmbeddingGeneratorProvider(gen); + + var vectors = await provider.EmbedBatchAsync(["a", "bb", "ccc"]); + + Assert.Equal(3, vectors.Count); + Assert.All(vectors, v => Assert.Equal(3, v.Length)); + } + + [Fact] + public async Task EmbedBatchAsync_EmptyInput_ReturnsEmpty() + { + using var gen = new FakeEmbeddingGenerator(); + using var provider = new EmbeddingGeneratorProvider(gen); + + var vectors = await provider.EmbedBatchAsync([]); + + Assert.Empty(vectors); + } +} + +public class MicrosoftAIExtensionsTests +{ + [Fact] + public void AsLLMProvider_WrapsChatClient() + { + using var client = new FakeChatClient(providerName: "Z"); + using var provider = client.AsLLMProvider(); + + Assert.IsType(provider); + Assert.Equal("Z", provider.ProviderName); + } + + [Fact] + public void AsChatClient_OnChatClientProvider_ReturnsUnderlyingClient() + { + using var client = new FakeChatClient(); + using var provider = new ChatClientLLMProvider(client); + + var roundTrip = provider.AsChatClient(); + + Assert.Same(client, roundTrip); + } + + [Fact] + public async Task AsChatClient_OnArbitraryProvider_BridgesCalls() + { + // Use a custom provider that is NOT a ChatClientLLMProvider to force the adapter path. + ILLMProvider custom = new EchoProvider(); + var chatClient = custom.AsChatClient("echo-model"); + + var response = await chatClient.GetResponseAsync([new ChatMessage(ChatRole.User, "ping")]); + + Assert.Equal("echo:ping", response.Text); + Assert.Equal("echo-model", response.ModelId); + } + + [Fact] + public async Task AsChatClient_Streaming_BridgesChunks() + { + ILLMProvider custom = new EchoProvider(); + var chatClient = custom.AsChatClient("echo-model"); + + var text = ""; + await foreach (var update in chatClient.GetStreamingResponseAsync([new ChatMessage(ChatRole.User, "ping")])) + text += update.Text; + + Assert.Equal("echo:ping", text); + } + + /// Minimal provider that echoes the user prompt, to exercise the bridge adapter. + private sealed class EchoProvider : ILLMProvider + { + public string ProviderName => "Echo"; + public Task IsAvailableAsync(CancellationToken ct = default) => Task.FromResult(true); + public Task> ListModelsAsync(CancellationToken ct = default) => Task.FromResult>([]); + public Task ChatAsync(string model, string systemPrompt, string userPrompt, float temperature = 0.7f, CancellationToken ct = default) + => Task.FromResult($"echo:{userPrompt}"); + public void Dispose() { } + } +} + +public class ProviderFactoryChatClientTests +{ + [Fact] + public void CreateFromChatClient_CachesByName() + { + using var factory = new ProviderFactory(); + using var client = new FakeChatClient(); + + var a = factory.CreateFromChatClient("x", client); + var b = factory.CreateFromChatClient("x", client); + + Assert.Same(a, b); + } + + [Fact] + public void CreateFromChatClient_ReturnsChatClientProvider() + { + using var factory = new ProviderFactory(); + using var client = new FakeChatClient(providerName: "P"); + + var provider = factory.CreateFromChatClient("y", client, "Custom"); + + Assert.Equal("Custom", provider.ProviderName); + } +} diff --git a/tests/Delibera.Core.Tests/Fakes/FakeChatClient.cs b/tests/Delibera.Core.Tests/Fakes/FakeChatClient.cs new file mode 100644 index 0000000..7aaddb6 --- /dev/null +++ b/tests/Delibera.Core.Tests/Fakes/FakeChatClient.cs @@ -0,0 +1,55 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.AI; + +namespace Delibera.Core.Tests.Fakes; + +/// +/// In-memory used to test the Microsoft.Extensions.AI integration +/// without any network access. +/// +public sealed class FakeChatClient(string reply = "fake-reply", string providerName = "Fake", string? defaultModel = "fake-model") + : IChatClient +{ + private readonly ChatClientMetadata _metadata = new(providerName, defaultModelId: defaultModel); + + public bool Disposed { get; private set; } + + /// Records the options of the most recent request for assertions. + public ChatOptions? LastOptions { get; private set; } + + /// Records the messages of the most recent request for assertions. + public IList? LastMessages { get; private set; } + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + LastMessages = messages.ToList(); + LastOptions = options; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, reply))); + } + + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + LastMessages = messages.ToList(); + LastOptions = options; + // Emit one update per word to simulate token streaming. + foreach (var word in reply.Split(' ')) + { + await Task.Yield(); + yield return new ChatResponseUpdate(ChatRole.Assistant, word + " "); + } + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + ArgumentNullException.ThrowIfNull(serviceType); + return serviceKey is null && serviceType.IsInstanceOfType(_metadata) ? _metadata : null; + } + + public void Dispose() => Disposed = true; +} diff --git a/tests/Delibera.Core.Tests/Fakes/FakeEmbeddingGenerator.cs b/tests/Delibera.Core.Tests/Fakes/FakeEmbeddingGenerator.cs new file mode 100644 index 0000000..7fe6b70 --- /dev/null +++ b/tests/Delibera.Core.Tests/Fakes/FakeEmbeddingGenerator.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.AI; + +namespace Delibera.Core.Tests.Fakes; + +/// +/// In-memory producing deterministic vectors. +/// +public sealed class FakeEmbeddingGenerator(int dimensions = 4, string modelName = "fake-embed") : IEmbeddingGenerator> +{ + private readonly EmbeddingGeneratorMetadata _metadata = new("Fake", defaultModelId: modelName, defaultModelDimensions: dimensions); + + public bool Disposed { get; private set; } + + public Task>> GenerateAsync( + IEnumerable values, + EmbeddingGenerationOptions? options = null, + CancellationToken cancellationToken = default) + { + var embeddings = new GeneratedEmbeddings>(); + foreach (var value in values) + { + var vector = new float[dimensions]; + for (var i = 0; i < dimensions; i++) + vector[i] = (value.Length + i) % 7 / 7f; + embeddings.Add(new Embedding(vector)); + } + + return Task.FromResult(embeddings); + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + ArgumentNullException.ThrowIfNull(serviceType); + return serviceKey is null && serviceType.IsInstanceOfType(_metadata) ? _metadata : null; + } + + public void Dispose() => Disposed = true; +} From 749df13f5f84ce128be8b548338b328a062a5ff6 Mon Sep 17 00:00:00 2001 From: Victor Buzin Date: Thu, 18 Jun 2026 18:31:16 +0200 Subject: [PATCH 3/5] Refactor: expand expression-bodied members to block bodies Refactored expression-bodied members to full block-bodied methods for improved consistency and maintainability. Updated method/property implementations, reordered some fields, and enhanced property clarity. Restored or moved obsolete/compatibility methods. Improved disposal patterns in factories and clarified null/empty handling. No changes to core business logic; focus is on code style and API consistency. --- .../Compression/CompressedContextFactory.cs | 16 ++-- .../Compression/CompressionCache.cs | 7 +- .../Compression/CompressionFactory.cs | 33 +++++---- .../Compression/CompressionService.cs | 14 ++-- .../Compression/DeduplicationCompressor.cs | 9 ++- .../Compression/HybridCompressor.cs | 2 +- .../Compression/SemanticCompressor.cs | 2 +- .../Compression/SummarizationCompressor.cs | 2 +- src/Delibera.Core/Compression/TokenCounter.cs | 7 +- src/Delibera.Core/Council/Chairman.cs | 46 ++++++++---- src/Delibera.Core/Council/CouncilBuilder.cs | 74 +++++++++++-------- src/Delibera.Core/Council/CouncilExecutor.cs | 13 +++- src/Delibera.Core/Council/KnowledgeKeeper.cs | 24 +++--- src/Delibera.Core/Council/Operator.cs | 69 ++++++++++------- src/Delibera.Core/Debate/ConsensusDebate.cs | 2 +- src/Delibera.Core/Debate/CritiqueDebate.cs | 2 +- .../Debate/DebateResultBuilder.cs | 53 ++++++++----- src/Delibera.Core/Debate/DebateScenario.cs | 45 ++++++----- src/Delibera.Core/Debate/StandardDebate.cs | 2 +- .../DependencyInjection/CouncilOptions.cs | 16 ++-- .../ServiceCollectionExtensions.cs | 7 +- .../Extensions/MicrosoftAIExtensions.cs | 39 +++++++--- .../Interfaces/IContextCompressor.cs | 2 +- .../Interfaces/ICouncilBuilder.cs | 18 ++++- src/Delibera.Core/Interfaces/ILLMProvider.cs | 4 +- src/Delibera.Core/Interfaces/IMcpClient.cs | 2 +- src/Delibera.Core/Interfaces/IOperator.cs | 2 +- src/Delibera.Core/Interfaces/IVectorStore.cs | 2 +- .../Knowledge/MarkdownKnowledgeBase.cs | 7 +- src/Delibera.Core/Models/CompressionLog.cs | 2 +- src/Delibera.Core/Models/CouncilMember.cs | 21 +++--- src/Delibera.Core/Models/DebateResult.cs | 19 +++-- src/Delibera.Core/Models/DebateRound.cs | 2 +- src/Delibera.Core/Models/ExecutionLog.cs | 27 +++++-- src/Delibera.Core/Models/OperatorModels.cs | 20 +++-- src/Delibera.Core/Models/PromptContext.cs | 12 +-- src/Delibera.Core/Models/TokenStatistics.cs | 23 +++--- .../Providers/LLM/ChatClientLLMProvider.cs | 52 +++++++------ .../LLM/EmbeddingGeneratorProvider.cs | 39 +++++----- .../Providers/LLM/OllamaEmbeddingProvider.cs | 2 +- .../Providers/LLM/OllamaProvider.cs | 42 +++++++---- .../Providers/Mcp/McpClientAdapter.cs | 8 +- .../Providers/ProviderFactory.cs | 56 +++++++++----- .../Providers/RAG/BaseRagProvider.cs | 25 ++++--- .../Providers/RAG/PgVectorRagProvider.cs | 2 +- .../Providers/RAG/PgVectorStore.cs | 14 +++- .../Providers/RAG/QdrantRagProvider.cs | 2 +- .../Providers/RAG/QdrantVectorStore.cs | 20 +++-- .../Providers/RAG/RagProviderFactory.cs | 33 +++++---- .../Providers/RAG/TextChunker.cs | 2 +- 50 files changed, 584 insertions(+), 360 deletions(-) diff --git a/src/Delibera.Core/Compression/CompressedContextFactory.cs b/src/Delibera.Core/Compression/CompressedContextFactory.cs index 702b85b..fb79287 100644 --- a/src/Delibera.Core/Compression/CompressedContextFactory.cs +++ b/src/Delibera.Core/Compression/CompressedContextFactory.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace Delibera.Core.Compression; /// @@ -12,8 +10,9 @@ internal static class CompressedContextFactory /// /// Builds a result describing text that was passed through unchanged. /// - public static CompressedContext PassThrough(string text, int tokens, string strategyName, TimeSpan duration) => - new() + public static CompressedContext PassThrough(string text, int tokens, string strategyName, TimeSpan duration) + { + return new CompressedContext { Text = text, OriginalLength = text.Length, @@ -23,6 +22,7 @@ public static CompressedContext PassThrough(string text, int tokens, string stra StrategyUsed = strategyName, Duration = duration }; + } /// /// Builds a result describing text that was actually compressed. @@ -33,8 +33,9 @@ public static CompressedContext Compressed( int originalTokens, int compressedTokens, string strategyName, - TimeSpan duration) => - new() + TimeSpan duration) + { + return new CompressedContext { Text = compressed, OriginalLength = original.Length, @@ -44,4 +45,5 @@ public static CompressedContext Compressed( StrategyUsed = strategyName, Duration = duration }; -} + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/CompressionCache.cs b/src/Delibera.Core/Compression/CompressionCache.cs index a619f81..52e112b 100644 --- a/src/Delibera.Core/Compression/CompressionCache.cs +++ b/src/Delibera.Core/Compression/CompressionCache.cs @@ -105,7 +105,10 @@ public void Clear() /// /// Returns a formatted summary of cache performance. /// - public string GetSummary() => $"Cache: {Count} entries, {HitCount} hits / {MissCount} misses ({HitRate:P1} hit rate)"; + public string GetSummary() + { + return $"Cache: {Count} entries, {HitCount} hits / {MissCount} misses ({HitRate:P1} hit rate)"; + } // ────────────────────────────────────────────── @@ -145,4 +148,4 @@ private sealed class CacheEntry public required CompressedContext Context { get; init; } public DateTime LastAccessed { get; set; } } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/CompressionFactory.cs b/src/Delibera.Core/Compression/CompressionFactory.cs index 2e61113..c7773d8 100644 --- a/src/Delibera.Core/Compression/CompressionFactory.cs +++ b/src/Delibera.Core/Compression/CompressionFactory.cs @@ -24,18 +24,21 @@ public static IContextCompressor Create( CompressionStrategy strategy, ILLMProvider? llmProvider = null, string? modelName = null, - IEmbeddingProvider? embeddingProvider = null) => strategy switch + IEmbeddingProvider? embeddingProvider = null) { - CompressionStrategy.None => PassThroughCompressor.Instance, - CompressionStrategy.Semantic => new SemanticCompressor( - embeddingProvider ?? throw new ArgumentException("Semantic compression requires an IEmbeddingProvider.", nameof(embeddingProvider))), - CompressionStrategy.Deduplication => new DeduplicationCompressor(embeddingProvider), - CompressionStrategy.Summarization => new SummarizationCompressor( - llmProvider ?? throw new ArgumentException("Summarization compression requires an ILLMProvider.", nameof(llmProvider)), - modelName ?? throw new ArgumentException("Summarization compression requires a modelName.", nameof(modelName))), - CompressionStrategy.Hybrid => new HybridCompressor(llmProvider, modelName, embeddingProvider), - _ => throw new ArgumentOutOfRangeException(nameof(strategy), $"Unknown compression strategy: {strategy}") - }; + return strategy switch + { + CompressionStrategy.None => PassThroughCompressor.Instance, + CompressionStrategy.Semantic => new SemanticCompressor( + embeddingProvider ?? throw new ArgumentException("Semantic compression requires an IEmbeddingProvider.", nameof(embeddingProvider))), + CompressionStrategy.Deduplication => new DeduplicationCompressor(embeddingProvider), + CompressionStrategy.Summarization => new SummarizationCompressor( + llmProvider ?? throw new ArgumentException("Summarization compression requires an ILLMProvider.", nameof(llmProvider)), + modelName ?? throw new ArgumentException("Summarization compression requires a modelName.", nameof(modelName))), + CompressionStrategy.Hybrid => new HybridCompressor(llmProvider, modelName, embeddingProvider), + _ => throw new ArgumentOutOfRangeException(nameof(strategy), $"Unknown compression strategy: {strategy}") + }; + } /// /// Creates a compressor from a strategy name string (case-insensitive). @@ -55,12 +58,14 @@ public static IContextCompressor Create( /// /// No-op compressor — passes text through unchanged. Used when compression is disabled. -/// +/// internal sealed class PassThroughCompressor : IContextCompressor { public static readonly PassThroughCompressor Instance = new(); - private PassThroughCompressor() { } + private PassThroughCompressor() + { + } /// public string StrategyName => "None"; @@ -82,4 +87,4 @@ public Task CompressBatchAsync(IReadOnlyList texts, C var merged = string.Join("\n\n", texts); return CompressAsync(merged, options, ct); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/CompressionService.cs b/src/Delibera.Core/Compression/CompressionService.cs index 873fc34..f68d2d9 100644 --- a/src/Delibera.Core/Compression/CompressionService.cs +++ b/src/Delibera.Core/Compression/CompressionService.cs @@ -15,14 +15,18 @@ public IContextCompressor Create( CompressionStrategy strategy, ILLMProvider? llmProvider = null, string? modelName = null, - IEmbeddingProvider? embeddingProvider = null) => - CompressionFactory.Create(strategy, llmProvider, modelName, embeddingProvider); + IEmbeddingProvider? embeddingProvider = null) + { + return CompressionFactory.Create(strategy, llmProvider, modelName, embeddingProvider); + } /// public IContextCompressor Create( string strategyName, ILLMProvider? llmProvider = null, string? modelName = null, - IEmbeddingProvider? embeddingProvider = null) => - CompressionFactory.Create(strategyName, llmProvider, modelName, embeddingProvider); -} + IEmbeddingProvider? embeddingProvider = null) + { + return CompressionFactory.Create(strategyName, llmProvider, modelName, embeddingProvider); + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/DeduplicationCompressor.cs b/src/Delibera.Core/Compression/DeduplicationCompressor.cs index 84c3bdd..bf56bb0 100644 --- a/src/Delibera.Core/Compression/DeduplicationCompressor.cs +++ b/src/Delibera.Core/Compression/DeduplicationCompressor.cs @@ -35,7 +35,7 @@ public async Task CompressAsync(string text, CompressionOptio if (sentences.Count <= 1) return CompressedContextFactory.PassThrough(text, originalTokens, StrategyName, sw.Elapsed); - List unique = _embeddingProvider is not null + var unique = _embeddingProvider is not null ? await DeduplicateWithEmbeddingsAsync(sentences, options.DeduplicationThreshold, ct) : DeduplicateWithHeuristics(sentences, options.DeduplicationThreshold); @@ -140,5 +140,8 @@ private static double ComputeTextSimilarity(string a, string b) : 0; } - private static string NormalizeText(string text) => text.Trim().ToLowerInvariant(); -} + private static string NormalizeText(string text) + { + return text.Trim().ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/HybridCompressor.cs b/src/Delibera.Core/Compression/HybridCompressor.cs index 641b08a..d49b8de 100644 --- a/src/Delibera.Core/Compression/HybridCompressor.cs +++ b/src/Delibera.Core/Compression/HybridCompressor.cs @@ -109,4 +109,4 @@ public async Task CompressBatchAsync(IReadOnlyList te var merged = string.Join("\n\n", texts); return await CompressAsync(merged, options, ct); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/SemanticCompressor.cs b/src/Delibera.Core/Compression/SemanticCompressor.cs index cbbc3fb..082c66a 100644 --- a/src/Delibera.Core/Compression/SemanticCompressor.cs +++ b/src/Delibera.Core/Compression/SemanticCompressor.cs @@ -201,4 +201,4 @@ internal static double CosineSimilarity(ReadOnlySpan a, ReadOnlySpanSentence with computed relevance score. private readonly record struct ScoredSentence(int Index, double Score, string Text); -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/SummarizationCompressor.cs b/src/Delibera.Core/Compression/SummarizationCompressor.cs index 3575b12..cd8e4fe 100644 --- a/src/Delibera.Core/Compression/SummarizationCompressor.cs +++ b/src/Delibera.Core/Compression/SummarizationCompressor.cs @@ -114,4 +114,4 @@ 5. Output ONLY the compressed text sw.Stop(); return CompressedContextFactory.Compressed(merged, summary, originalTokens, compressedTokens, StrategyName, sw.Elapsed); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Compression/TokenCounter.cs b/src/Delibera.Core/Compression/TokenCounter.cs index 6f7a1dc..b31eb64 100644 --- a/src/Delibera.Core/Compression/TokenCounter.cs +++ b/src/Delibera.Core/Compression/TokenCounter.cs @@ -82,7 +82,10 @@ public int EstimateTokens(IEnumerable texts) /// /// Returns true if the text exceeds the specified token limit. /// - public bool ExceedsLimit(string text, int tokenLimit) => EstimateTokens(text) > tokenLimit; + public bool ExceedsLimit(string text, int tokenLimit) + { + return EstimateTokens(text) > tokenLimit; + } /// /// Truncates text to approximately fit within the specified token limit. @@ -125,4 +128,4 @@ private static int CountWords(ReadOnlySpan text) return count; } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Council/Chairman.cs b/src/Delibera.Core/Council/Chairman.cs index d7e49f8..41432a7 100644 --- a/src/Delibera.Core/Council/Chairman.cs +++ b/src/Delibera.Core/Council/Chairman.cs @@ -12,36 +12,44 @@ public static class Chairman // ────────────────────────────────────────────── /// Creates a standard, neutral Chairman. - public static CouncilMember CreateStandard(string modelName, ILLMProvider provider) => - new(modelName, provider, "Chairman", + public static CouncilMember CreateStandard(string modelName, ILLMProvider provider) + { + return new CouncilMember(modelName, provider, "Chairman", """ You are the Chairman of an AI Council debate — neutral, objective and fair. You organise the debate process, ensure every participant is heard, synthesise diverse viewpoints and produce clear, actionable verdicts. Prioritise accuracy, completeness and practical usefulness. """); + } /// Creates a strict, evidence-focused Chairman. - public static CouncilMember CreateStrict(string modelName, ILLMProvider provider) => - new(modelName, provider, "Strict Chairman", + public static CouncilMember CreateStrict(string modelName, ILLMProvider provider) + { + return new CouncilMember(modelName, provider, "Strict Chairman", """ You are a strict, evidence-focused Chairman. You demand factual accuracy, reject unsupported claims and penalise logical fallacies. If no participant provides a satisfactory answer, say so clearly. """); + } /// Creates a creative Chairman who encourages novel thinking. - public static CouncilMember CreateCreative(string modelName, ILLMProvider provider) => - new(modelName, provider, "Creative Chairman", + public static CouncilMember CreateCreative(string modelName, ILLMProvider provider) + { + return new CouncilMember(modelName, provider, "Creative Chairman", """ You are a creative Chairman who looks beyond conventional answers. While maintaining accuracy, you seek innovative connections between ideas, encourage thinking outside the box, and surface unexpected insights. """); + } /// Creates a Chairman with a custom persona prompt. - public static CouncilMember CreateCustom(string modelName, ILLMProvider provider, string personaPrompt) => - new(modelName, provider, "Chairman", personaPrompt); + public static CouncilMember CreateCustom(string modelName, ILLMProvider provider, string personaPrompt) + { + return new CouncilMember(modelName, provider, "Chairman", personaPrompt); + } // ────────────────────────────────────────────── // Specialised Chairman actions (each has its own prompt) @@ -278,14 +286,26 @@ public static async Task SynthesizeVerdictAsync( public static class Moderator { /// - public static CouncilMember CreateStandard(string modelName, ILLMProvider provider) => Chairman.CreateStandard(modelName, provider); + public static CouncilMember CreateStandard(string modelName, ILLMProvider provider) + { + return Chairman.CreateStandard(modelName, provider); + } /// - public static CouncilMember CreateStrict(string modelName, ILLMProvider provider) => Chairman.CreateStrict(modelName, provider); + public static CouncilMember CreateStrict(string modelName, ILLMProvider provider) + { + return Chairman.CreateStrict(modelName, provider); + } /// - public static CouncilMember CreateCreative(string modelName, ILLMProvider provider) => Chairman.CreateCreative(modelName, provider); + public static CouncilMember CreateCreative(string modelName, ILLMProvider provider) + { + return Chairman.CreateCreative(modelName, provider); + } /// - public static CouncilMember CreateCustom(string modelName, ILLMProvider provider, string personaPrompt) => Chairman.CreateCustom(modelName, provider, personaPrompt); -} + public static CouncilMember CreateCustom(string modelName, ILLMProvider provider, string personaPrompt) + { + return Chairman.CreateCustom(modelName, provider, personaPrompt); + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Council/CouncilBuilder.cs b/src/Delibera.Core/Council/CouncilBuilder.cs index 37fa7bc..aefb2ce 100644 --- a/src/Delibera.Core/Council/CouncilBuilder.cs +++ b/src/Delibera.Core/Council/CouncilBuilder.cs @@ -16,11 +16,11 @@ public sealed class CouncilBuilder : ICouncilBuilder private IContextCompressor? _compressor; private IKnowledgeBase? _knowledgeBase; private KnowledgeKeeper? _knowledgeKeeper; + private int _maxRounds = 4; private Operator? _operator; - private bool _operatorReuseCompression; private CouncilMember? _operatorModel; + private bool _operatorReuseCompression; private IReadOnlyList? _operatorServers; - private int _maxRounds = 4; private string? _outputPath; private IDebateStrategy _strategy = new StandardDebate(); private string _systemPrompt = "You are a helpful AI assistant participating in a council debate."; @@ -62,15 +62,6 @@ public ICouncilBuilder SetChairman(string modelName, ILLMProvider provider, stri return this; } - /// Backward-compatible alias for . - [Obsolete("Use SetChairman instead.")] - public ICouncilBuilder SetModerator(CouncilMember moderator) => SetChairman(moderator); - - /// Backward-compatible alias for . - [Obsolete("Use SetChairman instead.")] - public ICouncilBuilder SetModerator(string modelName, ILLMProvider provider, string? persona = null) - => SetChairman(modelName, provider, persona); - // ── Knowledge Keeper ── /// @@ -80,20 +71,6 @@ public ICouncilBuilder WithKnowledgeKeeper(KnowledgeKeeper knowledgeKeeper) return this; } - /// Creates and attaches a Knowledge Keeper from a RAG provider, model and collection. - public ICouncilBuilder WithKnowledgeKeeper( - IRagProvider ragProvider, - string modelName, - ILLMProvider llmProvider, - string collectionName = "council_knowledge") - { - ArgumentNullException.ThrowIfNull(ragProvider); - ArgumentNullException.ThrowIfNull(llmProvider); - var member = new CouncilMember(modelName, llmProvider, "Knowledge Keeper"); - _knowledgeKeeper = new KnowledgeKeeper(ragProvider, member, collectionName); - return this; - } - // ── Operator (MCP tool micro-agent) ── /// @@ -217,6 +194,40 @@ public ICouncilBuilder SaveResultTo(string outputPath) return this; } + /// + ICouncilExecutor ICouncilBuilder.Build() + { + return Build(); + } + + /// Backward-compatible alias for . + [Obsolete("Use SetChairman instead.")] + public ICouncilBuilder SetModerator(CouncilMember moderator) + { + return SetChairman(moderator); + } + + /// Backward-compatible alias for . + [Obsolete("Use SetChairman instead.")] + public ICouncilBuilder SetModerator(string modelName, ILLMProvider provider, string? persona = null) + { + return SetChairman(modelName, provider, persona); + } + + /// Creates and attaches a Knowledge Keeper from a RAG provider, model and collection. + public ICouncilBuilder WithKnowledgeKeeper( + IRagProvider ragProvider, + string modelName, + ILLMProvider llmProvider, + string collectionName = "council_knowledge") + { + ArgumentNullException.ThrowIfNull(ragProvider); + ArgumentNullException.ThrowIfNull(llmProvider); + var member = new CouncilMember(modelName, llmProvider, "Knowledge Keeper"); + _knowledgeKeeper = new KnowledgeKeeper(ragProvider, member, collectionName); + return this; + } + // ── Build ── /// @@ -247,8 +258,12 @@ public CouncilExecutor Build() @operator = new Operator( _operatorModel, clients, - _operatorReuseCompression ? _compressor : null, - _operatorReuseCompression ? _compressionOptions : null); + _operatorReuseCompression + ? _compressor + : null, + _operatorReuseCompression + ? _compressionOptions + : null); } return new CouncilExecutor( @@ -265,7 +280,4 @@ public CouncilExecutor Build() _compressionCache, @operator); } - - /// - ICouncilExecutor ICouncilBuilder.Build() => Build(); -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Council/CouncilExecutor.cs b/src/Delibera.Core/Council/CouncilExecutor.cs index cc2823a..05e8240 100644 --- a/src/Delibera.Core/Council/CouncilExecutor.cs +++ b/src/Delibera.Core/Council/CouncilExecutor.cs @@ -250,8 +250,13 @@ public string GetInfo() return sb.ToString(); } - private void Log(ExecutionLog entry) => _executionLogs.Add(entry); + private void Log(ExecutionLog entry) + { + _executionLogs.Add(entry); + } - private static string Truncate(string text, int max) => - string.IsNullOrEmpty(text) ? "(empty)" : text.Length <= max ? text : text[..max] + "…"; -} + private static string Truncate(string text, int max) + { + return string.IsNullOrEmpty(text) ? "(empty)" : text.Length <= max ? text : text[..max] + "…"; + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Council/KnowledgeKeeper.cs b/src/Delibera.Core/Council/KnowledgeKeeper.cs index 23ac08c..280c886 100644 --- a/src/Delibera.Core/Council/KnowledgeKeeper.cs +++ b/src/Delibera.Core/Council/KnowledgeKeeper.cs @@ -7,9 +7,9 @@ namespace Delibera.Core.Council; /// public sealed class KnowledgeKeeper(IRagProvider ragProvider, CouncilMember model, string collectionName) { - private readonly IRagProvider _ragProvider = ragProvider ?? throw new ArgumentNullException(nameof(ragProvider)); - private readonly CouncilMember _model = model ?? throw new ArgumentNullException(nameof(model)); private readonly List _interactions = []; + private readonly CouncilMember _model = model ?? throw new ArgumentNullException(nameof(model)); + private readonly IRagProvider _ragProvider = ragProvider ?? throw new ArgumentNullException(nameof(ragProvider)); /// RAG collection used for searching. public string CollectionName { get; } = collectionName ?? throw new ArgumentNullException(nameof(collectionName)); @@ -28,8 +28,10 @@ public sealed class KnowledgeKeeper(IRagProvider ragProvider, CouncilMember mode /// Cancellation token. /// Scored search results from the vector store. public Task> SearchKnowledgeAsync( - string query, int limit = 5, CancellationToken ct = default) => - _ragProvider.SearchAsync(CollectionName, query, limit, ct: ct); + string query, int limit = 5, CancellationToken ct = default) + { + return _ragProvider.SearchAsync(CollectionName, query, limit, ct: ct); + } /// /// Generates an answer to a question using RAG context. @@ -221,8 +223,10 @@ public Task IndexDocumentAsync( Dictionary? metadata = null, int chunkSize = 500, int chunkOverlap = 50, - CancellationToken ct = default) => - _ragProvider.IndexDocumentAsync(CollectionName, documentText, metadata, chunkSize, chunkOverlap, ct); + CancellationToken ct = default) + { + return _ragProvider.IndexDocumentAsync(CollectionName, documentText, metadata, chunkSize, chunkOverlap, ct); + } /// /// Indexes a file into the Knowledge Keeper's collection. @@ -231,6 +235,8 @@ public Task IndexFileAsync( string filePath, int chunkSize = 500, int chunkOverlap = 50, - CancellationToken ct = default) => - _ragProvider.IndexFileAsync(CollectionName, filePath, chunkSize, chunkOverlap, ct); -} + CancellationToken ct = default) + { + return _ragProvider.IndexFileAsync(CollectionName, filePath, chunkSize, chunkOverlap, ct); + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Council/Operator.cs b/src/Delibera.Core/Council/Operator.cs index d9385d5..883f80e 100644 --- a/src/Delibera.Core/Council/Operator.cs +++ b/src/Delibera.Core/Council/Operator.cs @@ -101,7 +101,9 @@ public string GetToolCatalog() sb.AppendLine($"\n• Server \"{byServer.Key}\":"); foreach (var tool in byServer) { - var desc = string.IsNullOrWhiteSpace(tool.Description) ? "(no description)" : tool.Description.Trim(); + var desc = string.IsNullOrWhiteSpace(tool.Description) + ? "(no description)" + : tool.Description.Trim(); sb.AppendLine($" - {tool.QualifiedName}: {desc}"); } } @@ -167,6 +169,20 @@ public async Task ExecuteTaskAsync( return operatorResult; } + /// + public async ValueTask DisposeAsync() + { + foreach (var client in _mcpClients.Values) + try + { + await client.DisposeAsync(); + } + catch + { + // ignore disposal errors + } + } + // ────────────────────────────────────────────── // Micro-agent internals // ────────────────────────────────────────────── @@ -227,8 +243,12 @@ private static IReadOnlyList ParsePlan(string raw) var calls = new List(); foreach (var callEl in callsEl.EnumerateArray()) { - var server = callEl.TryGetProperty("server", out var s) ? s.GetString() : null; - var tool = callEl.TryGetProperty("tool", out var t) ? t.GetString() : null; + var server = callEl.TryGetProperty("server", out var s) + ? s.GetString() + : null; + var tool = callEl.TryGetProperty("tool", out var t) + ? t.GetString() + : null; if (string.IsNullOrWhiteSpace(server) || string.IsNullOrWhiteSpace(tool)) continue; @@ -323,32 +343,25 @@ private static string ExtractJson(string raw) var start = raw.IndexOf('{'); var end = raw.LastIndexOf('}'); - return start >= 0 && end > start ? raw[start..(end + 1)] : string.Empty; + return start >= 0 && end > start + ? raw[start..(end + 1)] + : string.Empty; } - private static object? JsonElementToObject(JsonElement el) => el.ValueKind switch + private static object? JsonElementToObject(JsonElement el) { - JsonValueKind.String => el.GetString(), - JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Null => null, - JsonValueKind.Array => el.EnumerateArray().Select(JsonElementToObject).ToList(), - JsonValueKind.Object => el.EnumerateObject().ToDictionary(p => p.Name, p => JsonElementToObject(p.Value)), - _ => el.GetRawText() - }; - - /// - public async ValueTask DisposeAsync() - { - foreach (var client in _mcpClients.Values) - try - { - await client.DisposeAsync(); - } - catch - { - // ignore disposal errors - } + return el.ValueKind switch + { + JsonValueKind.String => el.GetString(), + JsonValueKind.Number => el.TryGetInt64(out var l) + ? l + : el.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Array => el.EnumerateArray().Select(JsonElementToObject).ToList(), + JsonValueKind.Object => el.EnumerateObject().ToDictionary(p => p.Name, p => JsonElementToObject(p.Value)), + _ => el.GetRawText() + }; } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Debate/ConsensusDebate.cs b/src/Delibera.Core/Debate/ConsensusDebate.cs index 65e8069..7dbde2e 100644 --- a/src/Delibera.Core/Debate/ConsensusDebate.cs +++ b/src/Delibera.Core/Debate/ConsensusDebate.cs @@ -150,4 +150,4 @@ private static DebateResult BuildAndComplete(DebateResultBuilder builder) builder.MarkCompleted(); return builder.Build(); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Debate/CritiqueDebate.cs b/src/Delibera.Core/Debate/CritiqueDebate.cs index 3f39690..335b463 100644 --- a/src/Delibera.Core/Debate/CritiqueDebate.cs +++ b/src/Delibera.Core/Debate/CritiqueDebate.cs @@ -150,4 +150,4 @@ private static DebateResult BuildAndComplete(DebateResultBuilder builder) builder.MarkCompleted(); return builder.Build(); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Debate/DebateResultBuilder.cs b/src/Delibera.Core/Debate/DebateResultBuilder.cs index cf518ec..c50cc68 100644 --- a/src/Delibera.Core/Debate/DebateResultBuilder.cs +++ b/src/Delibera.Core/Debate/DebateResultBuilder.cs @@ -16,32 +16,47 @@ internal sealed class DebateResultBuilder( Operator? @operator = null) { private readonly List _rounds = []; - private string? _openingStatement; - private string? _finalVerdict; private DateTime? _completedAt; + private string? _finalVerdict; + private string? _openingStatement; public string StrategyName => strategy.StrategyName; public IReadOnlyList Rounds => _rounds; - public void SetOpeningStatement(string? statement) => _openingStatement = statement; + public void SetOpeningStatement(string? statement) + { + _openingStatement = statement; + } - public void AddRound(DebateRound round) => _rounds.Add(round); + public void AddRound(DebateRound round) + { + _rounds.Add(round); + } - public void SetFinalVerdict(string? verdict) => _finalVerdict = verdict; + public void SetFinalVerdict(string? verdict) + { + _finalVerdict = verdict; + } - public void MarkCompleted() => _completedAt = DateTime.UtcNow; + public void MarkCompleted() + { + _completedAt = DateTime.UtcNow; + } - public DebateResult Build() => new() + public DebateResult Build() { - StrategyName = strategy.StrategyName, - Context = context, - Participants = members.Select(m => m.DisplayName).ToList(), - ChairmanName = chairman?.DisplayName, - KnowledgeKeeperName = knowledgeKeeper?.DisplayName, - OperatorName = @operator?.DisplayName, - OpeningStatement = _openingStatement, - Rounds = _rounds, - FinalVerdict = _finalVerdict, - CompletedAt = _completedAt - }; -} + return new DebateResult + { + StrategyName = strategy.StrategyName, + Context = context, + Participants = members.Select(m => m.DisplayName).ToList(), + ChairmanName = chairman?.DisplayName, + KnowledgeKeeperName = knowledgeKeeper?.DisplayName, + OperatorName = @operator?.DisplayName, + OpeningStatement = _openingStatement, + Rounds = _rounds, + FinalVerdict = _finalVerdict, + CompletedAt = _completedAt + }; + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Debate/DebateScenario.cs b/src/Delibera.Core/Debate/DebateScenario.cs index c0f7fa5..bf09067 100644 --- a/src/Delibera.Core/Debate/DebateScenario.cs +++ b/src/Delibera.Core/Debate/DebateScenario.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using Delibera.Core.Council; namespace Delibera.Core.Debate; @@ -9,6 +10,20 @@ namespace Delibera.Core.Debate; /// public abstract class DebateScenario : IDebateStrategy { + // ────────────────────────────────────────────── + // Operator helpers + // ────────────────────────────────────────────── + + /// + /// Marker participants use to delegate a task to the Operator, e.g.: + /// [[OPERATOR: search the web for the latest .NET 10 release notes]]. + /// + private static readonly Regex OperatorRequestRegex = + new(@"\[\[\s*OPERATOR\s*:\s*(?.+?)\]\]", + RegexOptions.Singleline | + RegexOptions.IgnoreCase | + RegexOptions.Compiled); + /// public abstract string StrategyName { get; } @@ -72,8 +87,10 @@ protected static string FormatRoundResponses(DebateRound round) } /// Formats all rounds into a single text block. - protected static string FormatAllRounds(IReadOnlyList rounds) => - string.Join("\n\n", rounds.Select(FormatRoundResponses)); + protected static string FormatAllRounds(IReadOnlyList rounds) + { + return string.Join("\n\n", rounds.Select(FormatRoundResponses)); + } /// Creates a completed debate round. protected static DebateRound CreateRound( @@ -83,8 +100,9 @@ protected static DebateRound CreateRound( Dictionary responses, string? prompt = null, IReadOnlyList? knowledgeInteractions = null, - IReadOnlyList? operatorInteractions = null) => - new() + IReadOnlyList? operatorInteractions = null) + { + return new DebateRound { RoundNumber = number, RoundName = name, @@ -95,6 +113,7 @@ protected static DebateRound CreateRound( OperatorInteractions = operatorInteractions ?? [], CompletedAt = DateTime.UtcNow }; + } /// /// Optionally queries the Knowledge Keeper for context relevant to the debate topic. @@ -159,20 +178,6 @@ protected static DebateRound CreateRound( } } - // ────────────────────────────────────────────── - // Operator helpers - // ────────────────────────────────────────────── - - /// - /// Marker participants use to delegate a task to the Operator, e.g.: - /// [[OPERATOR: search the web for the latest .NET 10 release notes]]. - /// - private static readonly System.Text.RegularExpressions.Regex OperatorRequestRegex = - new(@"\[\[\s*OPERATOR\s*:\s*(?.+?)\]\]", - System.Text.RegularExpressions.RegexOptions.Singleline | - System.Text.RegularExpressions.RegexOptions.IgnoreCase | - System.Text.RegularExpressions.RegexOptions.Compiled); - /// /// Builds an "Operator briefing" describing the Operator's tools and how to delegate /// tasks to it. Appended to the participants' system prompt so they know what is available. @@ -216,7 +221,7 @@ protected static async Task> ProcessOperatorR { if (string.IsNullOrWhiteSpace(response)) continue; - foreach (System.Text.RegularExpressions.Match match in OperatorRequestRegex.Matches(response)) + foreach (Match match in OperatorRequestRegex.Matches(response)) { var task = match.Groups["task"].Value.Trim(); if (string.IsNullOrWhiteSpace(task)) continue; @@ -259,4 +264,4 @@ protected static string FormatOperatorInteractions(IReadOnlyList McpServers { get; set; } = []; /// Materialises the configured MCP servers into instances. - public IReadOnlyList ToServerConfigs() => - McpServers.Select(s => s.ToConfig()).ToList(); + public IReadOnlyList ToServerConfigs() + { + return McpServers.Select(s => s.ToConfig()).ToList(); + } } /// @@ -165,18 +167,20 @@ public sealed class McpServerOptions public Dictionary AdditionalHeaders { get; set; } = []; /// Converts these options into a . - public Models.McpServerConfig ToConfig() => - string.Equals(Transport, "Http", StringComparison.OrdinalIgnoreCase) - ? Models.McpServerConfig.Http( + public McpServerConfig ToConfig() + { + return string.Equals(Transport, "Http", StringComparison.OrdinalIgnoreCase) + ? McpServerConfig.Http( Name, new Uri(Endpoint ?? throw new InvalidOperationException($"MCP server '{Name}' uses Http transport but has no Endpoint.")), AdditionalHeaders) - : Models.McpServerConfig.Stdio( + : McpServerConfig.Stdio( Name, Command ?? throw new InvalidOperationException($"MCP server '{Name}' uses Stdio transport but has no Command."), Arguments, EnvironmentVariables, WorkingDirectory); + } } /// diff --git a/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs index 6bd81d5..ec5fbe0 100644 --- a/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Delibera.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Delibera.Core.Compression; using Delibera.Core.Council; -using Delibera.Core.Interfaces; using Delibera.Core.Providers; using Delibera.Core.Providers.LLM; using Delibera.Core.Providers.RAG; @@ -101,7 +100,7 @@ public static IServiceCollection AddDeliberaChatClient( services.TryAddSingleton(chatClientFactory); // The DI container owns the IChatClient lifetime, so the provider must not dispose it. services.TryAddSingleton(sp => - new ChatClientLLMProvider(sp.GetRequiredService(), providerName, ownsClient: false)); + new ChatClientLLMProvider(sp.GetRequiredService(), providerName, false)); return services; } @@ -145,8 +144,8 @@ public static IServiceCollection AddDeliberaEmbeddingGenerator( sp.GetRequiredService>>(), modelName, vectorSize, - ownsGenerator: false)); + false)); return services; } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs b/src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs index 66cfe27..906b577 100644 --- a/src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs +++ b/src/Delibera.Core/Extensions/MicrosoftAIExtensions.cs @@ -1,6 +1,4 @@ using System.Runtime.CompilerServices; -using System.Text; -using Delibera.Core.Interfaces; using Delibera.Core.Providers.LLM; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; @@ -15,8 +13,14 @@ namespace Delibera.Core.Extensions; /// /// These extensions let the two worlds interoperate in either direction: /// -/// Adopt any Microsoft.Extensions.AI client as a Delibera provider via / . -/// Expose a Delibera provider as a Microsoft.Extensions.AI via , so it can participate in the standard middleware pipeline. +/// +/// Adopt any Microsoft.Extensions.AI client as a Delibera provider via / +/// . +/// +/// +/// Expose a Delibera provider as a Microsoft.Extensions.AI via +/// , so it can participate in the standard middleware pipeline. +/// /// Compose middleware (function invocation, logging) with . /// /// @@ -29,7 +33,9 @@ public static class MicrosoftAIExtensions /// Optional friendly provider name (defaults to client metadata). /// Whether disposing the provider also disposes the client. public static ILLMProvider AsLLMProvider(this IChatClient chatClient, string? providerName = null, bool ownsClient = true) - => new ChatClientLLMProvider(chatClient, providerName, ownsClient); + { + return new ChatClientLLMProvider(chatClient, providerName, ownsClient); + } /// /// Adopts a Microsoft.Extensions.AI as a @@ -40,7 +46,9 @@ public static IEmbeddingProvider AsEmbeddingProvider( string? modelName = null, int? vectorSize = null, bool ownsGenerator = true) - => new EmbeddingGeneratorProvider(generator, modelName, vectorSize, ownsGenerator); + { + return new EmbeddingGeneratorProvider(generator, modelName, vectorSize, ownsGenerator); + } /// /// Exposes a Delibera as a Microsoft.Extensions.AI . @@ -121,10 +129,17 @@ public async IAsyncEnumerable GetStreamingResponseAsync( return null; } - public void Dispose() => provider.Dispose(); + public void Dispose() + { + provider.Dispose(); + } - private string ResolveModel(ChatOptions? options) => - options?.ModelId is { Length: > 0 } m ? m : defaultModel ?? string.Empty; + private string ResolveModel(ChatOptions? options) + { + return options?.ModelId is { Length: > 0 } m + ? m + : defaultModel ?? string.Empty; + } private static (string System, string User) SplitMessages(IEnumerable messages) { @@ -132,7 +147,9 @@ private static (string System, string User) SplitMessages(IEnumerable 0) target.Append('\n'); target.Append(message.Text); } @@ -140,4 +157,4 @@ private static (string System, string User) SplitMessages(IEnumerablePercentage of tokens saved. public double TokensSavedPercent => (1.0 - CompressionRatio) * 100.0; -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Interfaces/ICouncilBuilder.cs b/src/Delibera.Core/Interfaces/ICouncilBuilder.cs index 6f8cf80..c3cb4a6 100644 --- a/src/Delibera.Core/Interfaces/ICouncilBuilder.cs +++ b/src/Delibera.Core/Interfaces/ICouncilBuilder.cs @@ -1,4 +1,5 @@ using Delibera.Core.Council; +using Delibera.Core.Debate; namespace Delibera.Core.Interfaces; @@ -73,13 +74,22 @@ ICouncilBuilder WithOperator( ICouncilBuilder WithStrategy(IDebateStrategy strategy); /// Uses the standard 4-round debate strategy. - ICouncilBuilder WithStandardDebate() => WithStrategy(new Debate.StandardDebate()); + ICouncilBuilder WithStandardDebate() + { + return WithStrategy(new StandardDebate()); + } /// Uses the adversarial critique debate strategy. - ICouncilBuilder WithCritiqueDebate() => WithStrategy(new Debate.CritiqueDebate()); + ICouncilBuilder WithCritiqueDebate() + { + return WithStrategy(new CritiqueDebate()); + } /// Uses the consensus-building debate strategy. - ICouncilBuilder WithConsensusDebate() => WithStrategy(new Debate.ConsensusDebate()); + ICouncilBuilder WithConsensusDebate() + { + return WithStrategy(new ConsensusDebate()); + } /// Attaches a legacy knowledge base for prompt injection. /// Knowledge base instance. @@ -147,4 +157,4 @@ ICouncilBuilder WithCompression( /// Configured council executor ready for debate execution. /// When required configuration is missing. ICouncilExecutor Build(); -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Interfaces/ILLMProvider.cs b/src/Delibera.Core/Interfaces/ILLMProvider.cs index 0c9e300..d58e65c 100644 --- a/src/Delibera.Core/Interfaces/ILLMProvider.cs +++ b/src/Delibera.Core/Interfaces/ILLMProvider.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace Delibera.Core.Interfaces; /// @@ -52,7 +54,7 @@ async IAsyncEnumerable ChatStreamAsync( string systemPrompt, string userPrompt, float temperature = 0.7f, - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default) + [EnumeratorCancellation] CancellationToken ct = default) { yield return await ChatAsync(model, systemPrompt, userPrompt, temperature, ct); } diff --git a/src/Delibera.Core/Interfaces/IMcpClient.cs b/src/Delibera.Core/Interfaces/IMcpClient.cs index bd4649b..791ee24 100644 --- a/src/Delibera.Core/Interfaces/IMcpClient.cs +++ b/src/Delibera.Core/Interfaces/IMcpClient.cs @@ -38,4 +38,4 @@ Task CallToolAsync( string toolName, IReadOnlyDictionary arguments, CancellationToken ct = default); -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Interfaces/IOperator.cs b/src/Delibera.Core/Interfaces/IOperator.cs index 5340091..dd8a23a 100644 --- a/src/Delibera.Core/Interfaces/IOperator.cs +++ b/src/Delibera.Core/Interfaces/IOperator.cs @@ -48,4 +48,4 @@ Task ExecuteTaskAsync( string requesterName, string task, CancellationToken ct = default); -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Interfaces/IVectorStore.cs b/src/Delibera.Core/Interfaces/IVectorStore.cs index 9101205..527d677 100644 --- a/src/Delibera.Core/Interfaces/IVectorStore.cs +++ b/src/Delibera.Core/Interfaces/IVectorStore.cs @@ -70,4 +70,4 @@ Task> SearchAsync( /// Returns the total number of points in a collection. Task CountAsync(string collectionName, CancellationToken ct = default); -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Knowledge/MarkdownKnowledgeBase.cs b/src/Delibera.Core/Knowledge/MarkdownKnowledgeBase.cs index f3efd7a..8d4fecc 100644 --- a/src/Delibera.Core/Knowledge/MarkdownKnowledgeBase.cs +++ b/src/Delibera.Core/Knowledge/MarkdownKnowledgeBase.cs @@ -86,7 +86,10 @@ public IReadOnlyList Search(string query, int maxResults = 5) } /// - public IReadOnlyList GetLoadedSources() => _documents.Keys.ToList().AsReadOnly(); + public IReadOnlyList GetLoadedSources() + { + return _documents.Keys.ToList().AsReadOnly(); + } /// Loads all matching files from a directory. public async Task LoadDirectoryAsync(string directoryPath, string pattern = "*.md") @@ -97,4 +100,4 @@ public async Task LoadDirectoryAsync(string directoryPath, string pattern = "*.m await LoadManyAsync(Directory.GetFiles(full, pattern, SearchOption.AllDirectories)); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/CompressionLog.cs b/src/Delibera.Core/Models/CompressionLog.cs index 5361d06..42531c7 100644 --- a/src/Delibera.Core/Models/CompressionLog.cs +++ b/src/Delibera.Core/Models/CompressionLog.cs @@ -33,4 +33,4 @@ public sealed record CompressionLog public double Ratio => OriginalTokens > 0 ? (double)CompressedTokens / OriginalTokens : 1.0; -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/CouncilMember.cs b/src/Delibera.Core/Models/CouncilMember.cs index 03b9398..297cc63 100644 --- a/src/Delibera.Core/Models/CouncilMember.cs +++ b/src/Delibera.Core/Models/CouncilMember.cs @@ -5,6 +5,10 @@ namespace Delibera.Core.Models; /// public sealed class CouncilMember(string modelName, ILLMProvider provider, string? role = null, string? personaPrompt = null) { + private readonly string? _personaPrompt = personaPrompt; + + private string _role = role ?? "Expert"; + /// Unique participant identifier. public string Id { get; } = $"{provider.ProviderName}:{modelName}:{Guid.NewGuid():N}".ToLowerInvariant(); @@ -17,8 +21,6 @@ public sealed class CouncilMember(string modelName, ILLMProvider provider, strin /// LLM provider that serves this model. public ILLMProvider Provider { get; } = provider ?? throw new ArgumentNullException(nameof(provider)); - private string _role = role ?? "Expert"; - /// Role in the debate (Expert, Critic, Chairman, etc.). public string Role { @@ -26,14 +28,8 @@ public string Role set => field = value ?? "Expert"; } - private string? _personaPrompt = personaPrompt; - /// Optional persona system-prompt that personalises the model's behaviour. - public string? PersonaPrompt - { - get => field; - set => field = value; - } + public string? PersonaPrompt { get; set; } /// Sends a request to the underlying model. public Task AskAsync( @@ -53,5 +49,8 @@ public Task AskAsync( } /// - public override string ToString() => DisplayName; -} + public override string ToString() + { + return DisplayName; + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/DebateResult.cs b/src/Delibera.Core/Models/DebateResult.cs index 927aefd..fa429bc 100644 --- a/src/Delibera.Core/Models/DebateResult.cs +++ b/src/Delibera.Core/Models/DebateResult.cs @@ -1,5 +1,3 @@ -using System.Text; - namespace Delibera.Core.Models; /// @@ -272,19 +270,28 @@ public string ToLogsMarkdown() /// Saves the debate result (rounds and verdict) to a Markdown file. /// /// Path for the result Markdown file. - public Task SaveToMarkdownAsync(string filePath) => WriteAllTextAsync(filePath, ToMarkdown()); + public Task SaveToMarkdownAsync(string filePath) + { + return WriteAllTextAsync(filePath, ToMarkdown()); + } /// /// Saves token statistics and compression logs to a Markdown file. /// /// Path for the statistics Markdown file. - public Task SaveStatisticsAsync(string filePath) => WriteAllTextAsync(filePath, ToStatisticsMarkdown()); + public Task SaveStatisticsAsync(string filePath) + { + return WriteAllTextAsync(filePath, ToStatisticsMarkdown()); + } /// /// Saves execution logs to a Markdown file. /// /// Path for the logs Markdown file. - public Task SaveLogsAsync(string filePath) => WriteAllTextAsync(filePath, ToLogsMarkdown()); + public Task SaveLogsAsync(string filePath) + { + return WriteAllTextAsync(filePath, ToLogsMarkdown()); + } /// /// Saves all three files (result.md, statistics.md, logs.md) to the specified directory. @@ -385,4 +392,4 @@ public static void WriteTokenStatistics(StringBuilder sb, TokenStatistics stats) sb.AppendLine(); } } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/DebateRound.cs b/src/Delibera.Core/Models/DebateRound.cs index 3a075c5..33ca3ef 100644 --- a/src/Delibera.Core/Models/DebateRound.cs +++ b/src/Delibera.Core/Models/DebateRound.cs @@ -66,4 +66,4 @@ public sealed record DebateRound /// Round duration. public TimeSpan Duration => (CompletedAt ?? DateTime.UtcNow) - StartedAt; -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/ExecutionLog.cs b/src/Delibera.Core/Models/ExecutionLog.cs index ebf8a0f..700bc4f 100644 --- a/src/Delibera.Core/Models/ExecutionLog.cs +++ b/src/Delibera.Core/Models/ExecutionLog.cs @@ -39,25 +39,40 @@ public sealed record ExecutionLog( /// /// Creates an with and the current UTC time. /// - public static ExecutionLog Info(string source, string message) => new(LogLevel.Info, source, message, DateTime.UtcNow); + public static ExecutionLog Info(string source, string message) + { + return new ExecutionLog(LogLevel.Info, source, message, DateTime.UtcNow); + } /// /// Creates an with and the current UTC time. /// - public static ExecutionLog Trace(string source, string message) => new(LogLevel.Trace, source, message, DateTime.UtcNow); + public static ExecutionLog Trace(string source, string message) + { + return new ExecutionLog(LogLevel.Trace, source, message, DateTime.UtcNow); + } /// /// Creates an with and the current UTC time. /// - public static ExecutionLog Warn(string source, string message) => new(LogLevel.Warning, source, message, DateTime.UtcNow); + public static ExecutionLog Warn(string source, string message) + { + return new ExecutionLog(LogLevel.Warning, source, message, DateTime.UtcNow); + } /// /// Creates an with and the current UTC time. /// - public static ExecutionLog Error(string source, string message) => new(LogLevel.Error, source, message, DateTime.UtcNow); + public static ExecutionLog Error(string source, string message) + { + return new ExecutionLog(LogLevel.Error, source, message, DateTime.UtcNow); + } /// /// Formats the log entry as a single-line string. /// - public override string ToString() => $"[{Timestamp:HH:mm:ss.fff}] [{Level,-7}] [{Source}] {Message}"; -} + public override string ToString() + { + return $"[{Timestamp:HH:mm:ss.fff}] [{Level,-7}] [{Source}] {Message}"; + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/OperatorModels.cs b/src/Delibera.Core/Models/OperatorModels.cs index fa163f8..e9a71ea 100644 --- a/src/Delibera.Core/Models/OperatorModels.cs +++ b/src/Delibera.Core/Models/OperatorModels.cs @@ -69,8 +69,9 @@ public static McpServerConfig Stdio( string command, IReadOnlyList? arguments = null, IReadOnlyDictionary? environmentVariables = null, - string? workingDirectory = null) => - new() + string? workingDirectory = null) + { + return new McpServerConfig { Name = name, TransportType = McpTransportType.Stdio, @@ -79,19 +80,22 @@ public static McpServerConfig Stdio( EnvironmentVariables = environmentVariables ?? new Dictionary(), WorkingDirectory = workingDirectory }; + } /// Creates an HTTP/SSE MCP server configuration. public static McpServerConfig Http( string name, Uri endpoint, - IReadOnlyDictionary? additionalHeaders = null) => - new() + IReadOnlyDictionary? additionalHeaders = null) + { + return new McpServerConfig { Name = name, TransportType = McpTransportType.Http, Endpoint = endpoint, AdditionalHeaders = additionalHeaders ?? new Dictionary() }; + } } /// @@ -171,6 +175,8 @@ public sealed record OperatorResult( bool Compressed) { /// Converts this result into a loggable . - public OperatorInteraction ToInteraction() => - new(RequesterName, Task, Answer, ToolCalls, Compressed, DateTime.UtcNow); -} + public OperatorInteraction ToInteraction() + { + return new OperatorInteraction(RequesterName, Task, Answer, ToolCalls, Compressed, DateTime.UtcNow); + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/PromptContext.cs b/src/Delibera.Core/Models/PromptContext.cs index cd5d385..adba2b6 100644 --- a/src/Delibera.Core/Models/PromptContext.cs +++ b/src/Delibera.Core/Models/PromptContext.cs @@ -31,11 +31,11 @@ public string GetFullUserPrompt() return UserPrompt; return $$""" - ### Context (Knowledge Base): - {{KnowledgeContent}} + ### Context (Knowledge Base): + {{KnowledgeContent}} - ### Question: - {{UserPrompt}} - """; + ### Question: + {{UserPrompt}} + """; } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Models/TokenStatistics.cs b/src/Delibera.Core/Models/TokenStatistics.cs index cec8871..d7413b9 100644 --- a/src/Delibera.Core/Models/TokenStatistics.cs +++ b/src/Delibera.Core/Models/TokenStatistics.cs @@ -52,13 +52,16 @@ public sealed record TokenStatistics /// /// Formats the statistics as a human-readable summary. /// - public string ToSummary() => $""" - 📊 Token Statistics: - Original tokens: {TotalOriginalTokens:N0} - Compressed tokens: {TotalCompressedTokens:N0} - Response tokens: {TotalResponseTokens:N0} - Tokens saved: {TokensSaved:N0} ({SavedPercent:F1}%) - Compression ratio: {OverallCompressionRatio:P1} - Grand total: {GrandTotal:N0} - """; -} + public string ToSummary() + { + return $""" + 📊 Token Statistics: + Original tokens: {TotalOriginalTokens:N0} + Compressed tokens: {TotalCompressedTokens:N0} + Response tokens: {TotalResponseTokens:N0} + Tokens saved: {TokensSaved:N0} ({SavedPercent:F1}%) + Compression ratio: {OverallCompressionRatio:P1} + Grand total: {GrandTotal:N0} + """; + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs b/src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs index 76c5629..6287fd8 100644 --- a/src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs +++ b/src/Delibera.Core/Providers/LLM/ChatClientLLMProvider.cs @@ -1,4 +1,4 @@ -using Delibera.Core.Interfaces; +using System.Runtime.CompilerServices; using Microsoft.Extensions.AI; namespace Delibera.Core.Providers.LLM; @@ -23,7 +23,6 @@ namespace Delibera.Core.Providers.LLM; /// public sealed class ChatClientLLMProvider : ILLMProvider { - private readonly IChatClient _chatClient; private readonly bool _ownsClient; private bool _disposed; @@ -41,12 +40,14 @@ public sealed class ChatClientLLMProvider : ILLMProvider /// public ChatClientLLMProvider(IChatClient chatClient, string? providerName = null, bool ownsClient = true) { - _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + ChatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); _ownsClient = ownsClient; var metadata = chatClient.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata; - ProviderName = providerName - ?? (string.IsNullOrWhiteSpace(metadata?.ProviderName) ? "ChatClient" : metadata!.ProviderName!); + ProviderName = providerName ?? + (string.IsNullOrWhiteSpace(metadata?.ProviderName) + ? "ChatClient" + : metadata!.ProviderName!); DefaultModelId = metadata?.DefaultModelId; } @@ -54,7 +55,7 @@ public ChatClientLLMProvider(IChatClient chatClient, string? providerName = null public string? DefaultModelId { get; } /// Exposes the wrapped for advanced scenarios and middleware composition. - public IChatClient ChatClient => _chatClient; + public IChatClient ChatClient { get; } /// public string ProviderName { get; } @@ -95,7 +96,7 @@ public async Task ChatAsync( try { - var response = await _chatClient.GetResponseAsync(messages, options, ct); + var response = await ChatClient.GetResponseAsync(messages, options, ct); var text = response.Text?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(text)) throw new InvalidOperationException($"Empty response from model '{model}' ({ProviderName})."); @@ -118,14 +119,14 @@ public async IAsyncEnumerable ChatStreamAsync( string systemPrompt, string userPrompt, float temperature = 0.7f, - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default) + [EnumeratorCancellation] CancellationToken ct = default) { ArgumentException.ThrowIfNullOrWhiteSpace(userPrompt); var messages = BuildMessages(systemPrompt, userPrompt); var options = BuildOptions(model, temperature); - await foreach (var update in _chatClient.GetStreamingResponseAsync(messages, options, ct)) + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages, options, ct)) { var text = update.Text; if (!string.IsNullOrEmpty(text)) @@ -133,6 +134,15 @@ public async IAsyncEnumerable ChatStreamAsync( } } + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + if (_ownsClient) ChatClient.Dispose(); + GC.SuppressFinalize(this); + } + private static List BuildMessages(string systemPrompt, string userPrompt) { var messages = new List(2); @@ -142,19 +152,15 @@ private static List BuildMessages(string systemPrompt, string userP return messages; } - private ChatOptions BuildOptions(string model, float temperature) => new() + private ChatOptions BuildOptions(string model, float temperature) { - // Prefer the explicitly requested model; fall back to the client's default. - ModelId = string.IsNullOrWhiteSpace(model) ? DefaultModelId : model, - Temperature = temperature - }; - - /// - public void Dispose() - { - if (_disposed) return; - _disposed = true; - if (_ownsClient) _chatClient.Dispose(); - GC.SuppressFinalize(this); + return new ChatOptions + { + // Prefer the explicitly requested model; fall back to the client's default. + ModelId = string.IsNullOrWhiteSpace(model) + ? DefaultModelId + : model, + Temperature = temperature + }; } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs b/src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs index 4de303f..dba685f 100644 --- a/src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs +++ b/src/Delibera.Core/Providers/LLM/EmbeddingGeneratorProvider.cs @@ -1,4 +1,3 @@ -using Delibera.Core.Interfaces; using Microsoft.Extensions.AI; namespace Delibera.Core.Providers.LLM; @@ -14,7 +13,6 @@ namespace Delibera.Core.Providers.LLM; /// public sealed class EmbeddingGeneratorProvider : IEmbeddingProvider, IDisposable { - private readonly IEmbeddingGenerator> _generator; private readonly bool _ownsGenerator; private int? _cachedVectorSize; private bool _disposed; @@ -40,17 +38,28 @@ public EmbeddingGeneratorProvider( int? vectorSize = null, bool ownsGenerator = true) { - _generator = generator ?? throw new ArgumentNullException(nameof(generator)); + Generator = generator ?? throw new ArgumentNullException(nameof(generator)); _ownsGenerator = ownsGenerator; var metadata = generator.GetService(typeof(EmbeddingGeneratorMetadata)) as EmbeddingGeneratorMetadata; - EmbeddingModelName = modelName - ?? (string.IsNullOrWhiteSpace(metadata?.DefaultModelId) ? "embedding" : metadata!.DefaultModelId!); + EmbeddingModelName = modelName ?? + (string.IsNullOrWhiteSpace(metadata?.DefaultModelId) + ? "embedding" + : metadata!.DefaultModelId!); _cachedVectorSize = vectorSize ?? metadata?.DefaultModelDimensions; } /// Exposes the wrapped generator for advanced scenarios and middleware composition. - public IEmbeddingGenerator> Generator => _generator; + public IEmbeddingGenerator> Generator { get; } + + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + if (_ownsGenerator) Generator.Dispose(); + GC.SuppressFinalize(this); + } /// public string EmbeddingModelName { get; } @@ -65,9 +74,8 @@ public async Task EmbedAsync(string text, CancellationToken ct = defaul { ArgumentException.ThrowIfNullOrWhiteSpace(text); - var result = await _generator.GenerateAsync([text], cancellationToken: ct); - var embedding = result.FirstOrDefault() - ?? throw new InvalidOperationException($"No embedding returned by model '{EmbeddingModelName}'."); + var result = await Generator.GenerateAsync([text], cancellationToken: ct); + var embedding = result.FirstOrDefault() ?? throw new InvalidOperationException($"No embedding returned by model '{EmbeddingModelName}'."); var vector = embedding.Vector.ToArray(); _cachedVectorSize ??= vector.Length; @@ -80,7 +88,7 @@ public async Task> EmbedBatchAsync(IReadOnlyList ArgumentNullException.ThrowIfNull(texts); if (texts.Count == 0) return []; - var result = await _generator.GenerateAsync(texts, cancellationToken: ct); + var result = await Generator.GenerateAsync(texts, cancellationToken: ct); var vectors = new List(texts.Count); foreach (var embedding in result) @@ -92,13 +100,4 @@ public async Task> EmbedBatchAsync(IReadOnlyList return vectors.AsReadOnly(); } - - /// - public void Dispose() - { - if (_disposed) return; - _disposed = true; - if (_ownsGenerator) _generator.Dispose(); - GC.SuppressFinalize(this); - } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/LLM/OllamaEmbeddingProvider.cs b/src/Delibera.Core/Providers/LLM/OllamaEmbeddingProvider.cs index 9f1c74e..d16acff 100644 --- a/src/Delibera.Core/Providers/LLM/OllamaEmbeddingProvider.cs +++ b/src/Delibera.Core/Providers/LLM/OllamaEmbeddingProvider.cs @@ -81,4 +81,4 @@ public async Task> EmbedBatchAsync(IReadOnlyList return result.AsReadOnly(); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/LLM/OllamaProvider.cs b/src/Delibera.Core/Providers/LLM/OllamaProvider.cs index 726dde9..22a2d55 100644 --- a/src/Delibera.Core/Providers/LLM/OllamaProvider.cs +++ b/src/Delibera.Core/Providers/LLM/OllamaProvider.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.AI; using OllamaSharp; using OllamaSharp.Models; using OllamaSharp.Models.Chat; +using ChatRole = OllamaSharp.Models.Chat.ChatRole; namespace Delibera.Core.Providers.LLM; @@ -41,21 +43,6 @@ public OllamaProvider(string endpoint, string apiKey = "") /// public string ProviderName => "Ollama"; - /// - /// Exposes the underlying OllamaSharp client as a Microsoft.Extensions.AI . - /// - /// - /// natively implements , so this lets the - /// Ollama provider plug into the standard Microsoft.Extensions.AI middleware pipeline - /// (function invocation, caching, telemetry). - /// - public Microsoft.Extensions.AI.IChatClient AsChatClient() => Client; - - /// - /// Exposes the underlying OllamaSharp client as a Microsoft.Extensions.AI embedding generator. - /// - public Microsoft.Extensions.AI.IEmbeddingGenerator> AsEmbeddingGenerator() => Client; - /// public async Task IsAvailableAsync(CancellationToken ct = default) { @@ -141,4 +128,27 @@ public void Dispose() _disposed = true; GC.SuppressFinalize(this); } -} + + /// + /// Exposes the underlying OllamaSharp client as a Microsoft.Extensions.AI + /// . + /// + /// + /// natively implements , so this lets + /// the + /// Ollama provider plug into the standard Microsoft.Extensions.AI middleware pipeline + /// (function invocation, caching, telemetry). + /// + public IChatClient AsChatClient() + { + return Client; + } + + /// + /// Exposes the underlying OllamaSharp client as a Microsoft.Extensions.AI embedding generator. + /// + public IEmbeddingGenerator> AsEmbeddingGenerator() + { + return Client; + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs b/src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs index a4484e1..9190dce 100644 --- a/src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs +++ b/src/Delibera.Core/Providers/Mcp/McpClientAdapter.cs @@ -112,13 +112,15 @@ private static string SchemaToJson(McpClientTool tool) } } - private static IClientTransport CreateTransport(McpServerConfig config) => - config.TransportType switch + private static IClientTransport CreateTransport(McpServerConfig config) + { + return config.TransportType switch { McpTransportType.Stdio => CreateStdioTransport(config), McpTransportType.Http => CreateHttpTransport(config), _ => throw new NotSupportedException($"Unsupported MCP transport: {config.TransportType}") }; + } private static StdioClientTransport CreateStdioTransport(McpServerConfig config) { @@ -159,4 +161,4 @@ private static HttpClientTransport CreateHttpTransport(McpServerConfig config) return new HttpClientTransport(options); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/ProviderFactory.cs b/src/Delibera.Core/Providers/ProviderFactory.cs index 28a1455..4a46425 100644 --- a/src/Delibera.Core/Providers/ProviderFactory.cs +++ b/src/Delibera.Core/Providers/ProviderFactory.cs @@ -20,11 +20,29 @@ public abstract class CachingFactory /// Registered provider type names (read-only view). public IReadOnlyCollection RegisteredTypes => _builders.Keys.ToList().AsReadOnly(); + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + DisposeInstances(); + ClearInstances(); + GC.SuppressFinalize(this); + } + /// All currently cached provider instances. - public IReadOnlyDictionary GetAllInstances() => _instances; + public IReadOnlyDictionary GetAllInstances() + { + return _instances; + } /// Returns a cached instance by name, or null. - public TInstance? GetInstance(string name) => _instances.TryGetValue(name, out var p) ? p : null; + public TInstance? GetInstance(string name) + { + return _instances.TryGetValue(name, out var p) + ? p + : null; + } /// Registers a builder for a new provider type (e.g., "OpenAI", "YandexGPT"). public CachingFactory RegisterBuilder(string providerType, TBuilder builder) @@ -61,19 +79,15 @@ protected TInstance CacheInstance(string name, TInstance instance) } /// Iterates every cached instance — used by the derived Dispose methods. - protected IEnumerable EnumerateInstances() => _instances.Values; + protected IEnumerable EnumerateInstances() + { + return _instances.Values; + } /// Clears the cache without disposing anything. - protected void ClearInstances() => _instances.Clear(); - - /// - public void Dispose() + protected void ClearInstances() { - if (_disposed) return; - _disposed = true; - DisposeInstances(); - ClearInstances(); - GC.SuppressFinalize(this); + _instances.Clear(); } /// Disposes every cached instance. Subclasses override to call the correct dispose method. @@ -107,14 +121,22 @@ ILLMProviderFactory ILLMProviderFactory.RegisterBuilder(string providerType, Fun /// /// Creates (or returns a cached) provider from configuration. /// - public ILLMProvider Create(string name, string providerType, IConfigurationSection config) => - GetOrCreate(name, providerType, b => b(config)); + public ILLMProvider Create(string name, string providerType, IConfigurationSection config) + { + return GetOrCreate(name, providerType, b => b(config)); + } /// Returns a cached provider by name, or null. - public ILLMProvider? GetProvider(string name) => GetInstance(name); + public ILLMProvider? GetProvider(string name) + { + return GetInstance(name); + } /// All created provider instances. - public IReadOnlyDictionary GetAllProviders() => GetAllInstances(); + public IReadOnlyDictionary GetAllProviders() + { + return GetAllInstances(); + } /// Creates an Ollama provider with direct parameters. public OllamaProvider CreateOllama(string endpoint, string apiKey = "") @@ -161,4 +183,4 @@ protected override void DisposeInstances() { foreach (var p in EnumerateInstances()) p.Dispose(); } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/RAG/BaseRagProvider.cs b/src/Delibera.Core/Providers/RAG/BaseRagProvider.cs index 9c613b2..f21ec07 100644 --- a/src/Delibera.Core/Providers/RAG/BaseRagProvider.cs +++ b/src/Delibera.Core/Providers/RAG/BaseRagProvider.cs @@ -8,15 +8,6 @@ namespace Delibera.Core.Providers.RAG; /// public abstract class BaseRagProvider : IRagProvider { - /// Abstract implementations describe themselves. - public abstract string ProviderName { get; } - - /// - public IVectorStore VectorStore { get; } - - /// - public IEmbeddingProvider EmbeddingProvider { get; } - /// /// Initialises a base RAG provider with the supplied vector store and embedding provider. /// @@ -26,6 +17,15 @@ protected BaseRagProvider(IVectorStore vectorStore, IEmbeddingProvider embedding EmbeddingProvider = embeddingProvider ?? throw new ArgumentNullException(nameof(embeddingProvider)); } + /// Abstract implementations describe themselves. + public abstract string ProviderName { get; } + + /// + public IVectorStore VectorStore { get; } + + /// + public IEmbeddingProvider EmbeddingProvider { get; } + /// public virtual async Task IndexDocumentAsync( string collectionName, @@ -124,5 +124,8 @@ public virtual async Task GetContextAsync( } /// - public virtual ValueTask DisposeAsync() => VectorStore.DisposeAsync(); -} + public virtual ValueTask DisposeAsync() + { + return VectorStore.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/RAG/PgVectorRagProvider.cs b/src/Delibera.Core/Providers/RAG/PgVectorRagProvider.cs index 903e8e3..ef10cfc 100644 --- a/src/Delibera.Core/Providers/RAG/PgVectorRagProvider.cs +++ b/src/Delibera.Core/Providers/RAG/PgVectorRagProvider.cs @@ -36,4 +36,4 @@ public PgVectorRagProvider(IEmbeddingProvider embeddingProvider, string connecti /// public override string ProviderName => "PgVectorRAG"; -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/RAG/PgVectorStore.cs b/src/Delibera.Core/Providers/RAG/PgVectorStore.cs index b118fb5..40cba43 100644 --- a/src/Delibera.Core/Providers/RAG/PgVectorStore.cs +++ b/src/Delibera.Core/Providers/RAG/PgVectorStore.cs @@ -110,8 +110,12 @@ public async Task UpsertAsync(string collectionName, IReadOnlyList foreach (var p in points) { - var id = Guid.TryParse(p.Id, out var guid) ? guid : Guid.NewGuid(); - var metadataJson = p.Metadata is { Count: > 0 } ? JsonSerializer.Serialize(p.Metadata) : null; + var id = Guid.TryParse(p.Id, out var guid) + ? guid + : Guid.NewGuid(); + var metadataJson = p.Metadata is { Count: > 0 } + ? JsonSerializer.Serialize(p.Metadata) + : null; var cmd = new NpgsqlBatchCommand($""" INSERT INTO {tableName} (id, text, metadata, embedding) @@ -164,7 +168,9 @@ public async Task> SearchAsync( { var id = reader.GetGuid(0).ToString(); var text = reader.GetString(1); - var metaJson = reader.IsDBNull(2) ? null : reader.GetString(2); + var metaJson = reader.IsDBNull(2) + ? null + : reader.GetString(2); var score = (float)reader.GetDouble(3); Dictionary? metadata = null; @@ -229,4 +235,4 @@ private static string SanitizeTableName(string collectionName) collectionName.ToLowerInvariant(), @"[^a-z0-9_]", "_"); return $"vc_{safe}"; } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/RAG/QdrantRagProvider.cs b/src/Delibera.Core/Providers/RAG/QdrantRagProvider.cs index 17c20b1..bb7e832 100644 --- a/src/Delibera.Core/Providers/RAG/QdrantRagProvider.cs +++ b/src/Delibera.Core/Providers/RAG/QdrantRagProvider.cs @@ -32,4 +32,4 @@ public QdrantRagProvider( /// public override string ProviderName => "QdrantRAG"; -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/RAG/QdrantVectorStore.cs b/src/Delibera.Core/Providers/RAG/QdrantVectorStore.cs index aee80ac..f6be109 100644 --- a/src/Delibera.Core/Providers/RAG/QdrantVectorStore.cs +++ b/src/Delibera.Core/Providers/RAG/QdrantVectorStore.cs @@ -83,14 +83,18 @@ public async Task> SearchAsync( collectionName, queryVector, limit: (ulong)limit, - scoreThreshold: scoreThreshold > 0 ? scoreThreshold : null, + scoreThreshold: scoreThreshold > 0 + ? scoreThreshold + : null, cancellationToken: ct); var results = new List(scored.Count); foreach (var s in scored) { - var text = s.Payload.TryGetValue("text", out var tv) ? tv.StringValue : string.Empty; + var text = s.Payload.TryGetValue("text", out var tv) + ? tv.StringValue + : string.Empty; var meta = new Dictionary(); foreach (var (k, v) in s.Payload) @@ -101,15 +105,19 @@ public async Task> SearchAsync( s.Id.Uuid, text, s.Score, - meta.Count > 0 ? meta : null)); + meta.Count > 0 + ? meta + : null)); } return results.AsReadOnly(); } /// - public Task DeleteCollectionAsync(string collectionName, CancellationToken ct = default) => - _client.DeleteCollectionAsync(collectionName, cancellationToken: ct); + public Task DeleteCollectionAsync(string collectionName, CancellationToken ct = default) + { + return _client.DeleteCollectionAsync(collectionName, cancellationToken: ct); + } /// public async Task CountAsync(string collectionName, CancellationToken ct = default) @@ -124,4 +132,4 @@ public ValueTask DisposeAsync() _client.Dispose(); return ValueTask.CompletedTask; } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/RAG/RagProviderFactory.cs b/src/Delibera.Core/Providers/RAG/RagProviderFactory.cs index 90df63b..cb5e060 100644 --- a/src/Delibera.Core/Providers/RAG/RagProviderFactory.cs +++ b/src/Delibera.Core/Providers/RAG/RagProviderFactory.cs @@ -16,7 +16,9 @@ public RagProviderFactory() RegisterBuilder("Qdrant", (config, embeddings) => { var host = config["Host"] ?? "localhost"; - var port = int.TryParse(config["Port"], out var p) ? p : 6334; + var port = int.TryParse(config["Port"], out var p) + ? p + : 6334; var https = bool.TryParse(config["Https"], out var h) && h; var apiKey = config["ApiKey"]; @@ -42,11 +44,24 @@ IRagProviderFactory IRagProviderFactory.RegisterBuilder( /// /// Creates (or returns cached) a RAG provider instance. /// - public IRagProvider Create(string name, string providerType, IConfigurationSection config, IEmbeddingProvider embeddingProvider) => - GetOrCreate(name, providerType, b => b(config, embeddingProvider)); + public IRagProvider Create(string name, string providerType, IConfigurationSection config, IEmbeddingProvider embeddingProvider) + { + return GetOrCreate(name, providerType, b => b(config, embeddingProvider)); + } /// Returns a cached provider by name. - public IRagProvider? GetProvider(string name) => GetInstance(name); + public IRagProvider? GetProvider(string name) + { + return GetInstance(name); + } + + /// + public async ValueTask DisposeAsync() + { + foreach (var p in EnumerateInstances()) + await p.DisposeAsync(); + ClearInstances(); + } /// /// Creates a Qdrant RAG provider with direct parameters. @@ -85,12 +100,4 @@ protected override void DisposeInstances() foreach (var p in EnumerateInstances()) p.DisposeAsync().AsTask().GetAwaiter().GetResult(); } - - /// - public async ValueTask DisposeAsync() - { - foreach (var p in EnumerateInstances()) - await p.DisposeAsync(); - ClearInstances(); - } -} +} \ No newline at end of file diff --git a/src/Delibera.Core/Providers/RAG/TextChunker.cs b/src/Delibera.Core/Providers/RAG/TextChunker.cs index 0b6cda5..761b165 100644 --- a/src/Delibera.Core/Providers/RAG/TextChunker.cs +++ b/src/Delibera.Core/Providers/RAG/TextChunker.cs @@ -59,4 +59,4 @@ public static List SplitIntoChunks(string text, int chunkSize, int chunk return chunks; } -} +} \ No newline at end of file From 6613a3ad9856231304bd23a1f00b7402368318c8 Mon Sep 17 00:00:00 2001 From: Victor Buzin Date: Thu, 18 Jun 2026 19:04:17 +0200 Subject: [PATCH 4/5] Revamp README: expand docs, update samples, clarify config Major overhaul of README.md for clarity and completeness: - Expanded intro, features, and quick start with prerequisites - Improved code samples and formatting throughout - Added detailed sections for DI, Microsoft.Extensions.AI interop, context compression, RAG, Operator, and output files - Provided a more comprehensive appsettings.json example - Updated NuGet dependencies table and all repo links - README.md no longer included in NuGet package in .csproj - Numerous minor language, formatting, and consistency fixes --- README.md | 2 +- src/Delibera.Core/Delibera.Core.csproj | 1 - src/Delibera.Core/README.md | 364 +++++++++++++++---------- 3 files changed, 225 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 15c220d..d180f50 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![.NET 10](https://img.shields.io/badge/.NET-10.0-1F2937.svg)](https://dotnet.microsoft.com) [![C# 15](https://img.shields.io/badge/C%23-15.0--preview-239120.svg)](https://learn.microsoft.com/dotnet/csharp/) -🇷🇺 [Русская версия (README-RU.md)](README-RU.md) +[EN](README.md) / [RU](README-RU.md) diff --git a/src/Delibera.Core/Delibera.Core.csproj b/src/Delibera.Core/Delibera.Core.csproj index 2471c5b..727d501 100644 --- a/src/Delibera.Core/Delibera.Core.csproj +++ b/src/Delibera.Core/Delibera.Core.csproj @@ -33,7 +33,6 @@ - diff --git a/src/Delibera.Core/README.md b/src/Delibera.Core/README.md index b515425..5c2d0fd 100644 --- a/src/Delibera.Core/README.md +++ b/src/Delibera.Core/README.md @@ -1,25 +1,27 @@ -
- -# Delibera +# [Delibera](https://www.nuget.org/packages/Delibera.Core) ### ⚖️ Thoughtful AI Decisions **Collective decision making through structured AI deliberation — with RAG, pgvector, Knowledge Keeper, Operator (MCP tools), Chairman, Context Compression, Dependency Injection & Execution Logging.** [![NuGet](https://img.shields.io/nuget/v/Delibera.Core.svg)](https://www.nuget.org/packages/Delibera.Core) -[![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](../LICENSE) +[![License: MIT](https://img.shields.io/badge/License-MIT-10B981.svg)](https://github.com/techbuzzz/Delibera/blob/develop/LICENSE) [![.NET 10](https://img.shields.io/badge/.NET-10.0-1F2937.svg)](https://dotnet.microsoft.com) - -
+[![C# 15](https://img.shields.io/badge/C%23-15.0--preview-239120.svg)](https://learn.microsoft.com/dotnet/csharp/) --- ## What is Delibera? **Delibera** is a C# / .NET 10 framework that orchestrates **multi-model deliberations** between LLMs. -Multiple models reason through a question across structured rounds, critique each other, and a -**Chairman** synthesises a balanced final verdict — optionally enriched by a **Knowledge Keeper** -backed by **Qdrant** or **PostgreSQL/pgvector** (RAG), with **context compression** to minimise tokens. +Multiple AI models reason through a question across structured rounds, critique each other's answers, +and a **Chairman** weighs the arguments to synthesise a balanced final verdict — enriched by a +**Knowledge Keeper** backed by **Qdrant** or **PostgreSQL/pgvector** (RAG), with **intelligent context +compression** to minimise token usage. + +The name comes from *deliberation* — the careful weighing of evidence and viewpoints before reaching +a decision. Delibera brings that discipline to AI, helping teams reach **thoughtful, well-reasoned +outcomes** rather than single-model guesses. --- @@ -28,14 +30,15 @@ backed by **Qdrant** or **PostgreSQL/pgvector** (RAG), with **context compressio - 🏛️ **Multi-Model Councils** — orchestrate any number of LLM participants across structured debate rounds - ⚖️ **Chairman Synthesis** — a dedicated moderator opens, regulates and synthesises the final verdict - 📚 **Knowledge Keeper (RAG)** — per-round semantic retrieval with Qdrant or pgvector +- 🐘 **Qdrant + pgvector** — pluggable vector stores (dedicated DB or your existing PostgreSQL) - 🛠️ **Operator (MCP Tools)** — a micro-agent that delegates tasks to MCP servers (web search, file system, Notion, PostgreSQL…) on demand during the debate - 🗜️ **Context Compression** — 4 strategies (Semantic, Deduplication, Summarization, Hybrid) save 30–70% of tokens -- 💉 **Dependency Injection** — `AddDelibera()` extension for `IServiceCollection` -- 📋 **Execution Logging** — `ExecutionLog` with `LogLevel` for Chairman, KK, Compression & participants +- 💉 **Dependency Injection** — `AddDelibera()` extension for `IServiceCollection` with full options binding +- 📋 **Execution Logging** — `ExecutionLog` model with `LogLevel` for Chairman, KK, Compression & participants - 📁 **Separate File Output** — export `result.md`, `statistics.md`, `logs.md` independently - 🤝 **Microsoft.Extensions.AI** — first-class `IChatClient` / `IEmbeddingGenerator` interop with logging & function-invocation middleware - 🔌 **Interface-First** — clean abstractions for providers, factories, builders and executors -- 🧱 **Modern C# 12** — file-scoped namespaces, records, init-only properties, global usings +- 🧱 **Modern C# 15** — file-scoped namespaces, records, init-only properties, global usings --- @@ -47,7 +50,12 @@ backed by **Qdrant** or **PostgreSQL/pgvector** (RAG), with **context compressio dotnet add package Delibera.Core ``` -### 2. Pull the recommended models +### 2. Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- A running [Ollama](https://ollama.com) instance — local (`ollama serve`) or [Ollama Cloud](https://ollama.com/cloud) (just an API key, no install). + +### 3. Pull the recommended models ```bash ollama pull llama3.2:3b # council member @@ -56,9 +64,9 @@ ollama pull nomic-embed-text # embeddings (RAG) ``` > ☁️ To use **Ollama Cloud** instead, just pass an API key — no local models required. -> See the [Configuration section](#-dependency-injection) below. +> See the [Configuration section](#-configuration-appsettingsjson) below. -### 3. Run your first deliberation +### 4. Run your first deliberation ```csharp using Delibera.Core.Council; @@ -68,16 +76,16 @@ using var factory = new ProviderFactory(); var ollama = factory.CreateOllama("http://localhost:11434"); var result = await new CouncilBuilder() - .AddMember("llama3.2:3b", ollama, "Analyst") - .AddMember("qwen2.5:7b", ollama, "Strategist") - .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) - .WithStandardDebate() - .WithSystemPrompt("You are a software architecture expert.") - .WithUserPrompt("Microservices vs Monolith for a 5-person startup?") - .WithMaxRounds(4) - .SaveResultTo("./deliberation.md") - .Build() - .ExecuteAsync(); + .AddMember("llama3.2:3b", ollama, "Analyst") + .AddMember("qwen2.5:7b", ollama, "Strategist") + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + .WithStandardDebate() + .WithSystemPrompt("You are a software architecture expert.") + .WithUserPrompt("Microservices vs Monolith for a 5-person startup?") + .WithMaxRounds(4) + .SaveResultTo("./deliberation.md") + .Build() + .ExecuteAsync(); Console.WriteLine(result.FinalVerdict); ``` @@ -86,19 +94,23 @@ Console.WriteLine(result.FinalVerdict); ## 💉 Dependency Injection +Register all Delibera services in one line: + ```csharp using Delibera.Core.DependencyInjection; -// A: with configuration binding +// A: with configuration binding (binds the "Delibera" section) services.AddDelibera(configuration, "Delibera"); // B: with options delegate services.AddDelibera(options => { - options.Strategy = "Standard"; - options.MaxRounds = 4; - options.Compression.Enabled = true; - options.Compression.Strategy = "Hybrid"; + options.Strategy = "Standard"; + options.MaxRounds = 4; + options.Temperature = 0.7f; + options.Compression.Enabled = true; + options.Compression.Strategy = "Hybrid"; + options.Compression.TargetRatio = 0.5; }); // C: defaults only @@ -116,8 +128,54 @@ Resolved services: --- +## 🤝 Microsoft.Extensions.AI Interop + +Delibera integrates with [`Microsoft.Extensions.AI`](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai) +through `ChatClientLLMProvider` and `EmbeddingGeneratorProvider`. Adapt **any** `IChatClient` / +`IEmbeddingGenerator` (OpenAI, Azure OpenAI, Ollama, custom…) and use the full middleware pipeline +(logging, function invocation, telemetry). + +```csharp +using Delibera.Core.Providers.LLM; +using Microsoft.Extensions.AI; +using OpenAI; + +var openAi = new OpenAIClient(apiKey); + +// IChatClient — works with any MEAI-compatible client +var chatClient = openAi.GetChatClient("gpt-4o").AsIChatClient(); +var llm = new ChatClientLLMProvider(chatClient); + +// IEmbeddingGenerator — for Knowledge Keeper / compression +var embeddingGen = openAi.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); +var embeddings = new EmbeddingGeneratorProvider(embeddingGen); + +var result = await new CouncilBuilder() + .AddMember("gpt-4o", llm, "Analyst") + .SetChairman(Chairman.CreateStandard("gpt-4o", llm)) + .WithStandardDebate() + .WithUserPrompt("Should we adopt event sourcing?") + .Build() + .ExecuteAsync(); +``` + +Streaming via `ChatStreamAsync` and DI helpers (`AddDeliberaChatClient`) are also included. Fully +backward compatible with the existing `OllamaProvider` / `OllamaEmbeddingProvider` APIs. + +--- + ## 🗜️ Context Compression +Automatically compress context between deliberation rounds — save **30–70% of tokens** without losing meaning. + +| Strategy | How It Works | Best For | +| ----------------- | ---------------------------------------------------------- | -------------------------------- | +| **Semantic** | Embeds sentences, ranks by relevance to topic, keeps top-N | Large knowledge contexts | +| **Deduplication** | Removes semantically similar sentences across participants | Multi-model debates with overlap | +| **Summarization** | LLM produces a concise summary preserving key facts | Maximum compression ratio | +| **Hybrid** | Dedup → Semantic → Summarize pipeline | Best overall quality | +| **None** | Pass-through (when disabled) | Debugging | + ```csharp using Delibera.Core.Compression; using Delibera.Core.Providers.LLM; @@ -126,26 +184,27 @@ var ollama = new OllamaProvider("http://localhost:11434"); var embeddings = new OllamaEmbeddingProvider(ollama, "nomic-embed-text"); var result = await new CouncilBuilder() - .AddMember("llama3.2:3b", ollama) - .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) - .WithCompression(CompressionStrategy.Hybrid, - llmProvider: ollama, - modelName: "llama3.2:3b", - embeddingProvider: embeddings) - .WithCompressionCache() - .WithUserPrompt("Analyze our architecture options...") - .Build() - .ExecuteAsync(); + .AddMember("llama3.2:3b", ollama) + .SetChairman(Chairman.CreateStandard("qwen2.5:7b", ollama)) + .WithCompression(CompressionStrategy.Hybrid, + llmProvider: ollama, + modelName: "llama3.2:3b", + embeddingProvider: embeddings) + .WithCompressionOptions(new CompressionOptions { TargetRatio = 0.5 }) + .WithCompressionCache() + .WithUserPrompt("Analyze our architecture options...") + .Build() + .ExecuteAsync(); Console.WriteLine(result.TokenStats?.ToSummary()); ``` -Strategies: **Semantic**, **Deduplication**, **Summarization**, **Hybrid** (Dedup → Semantic → Summarize). - --- ## 📚 RAG Integration +Use a dedicated **Qdrant** instance or your existing **PostgreSQL/pgvector** database as a vector store. + ```csharp using Delibera.Core.Council; using Delibera.Core.Models; @@ -155,22 +214,27 @@ using Delibera.Core.Providers.RAG; var ollama = new OllamaProvider("http://localhost:11434"); var embeddings = new OllamaEmbeddingProvider(ollama, "nomic-embed-text"); -// pgvector — point at your existing PostgreSQL -var rag = new RagProviderFactory().CreatePgVector( - embeddings, - "Host=localhost;Database=council_vectors;Username=postgres;Password=postgres"); - -// …or Qdrant -// var rag = new RagProviderFactory().CreateQdrant(embeddings, "localhost", 6334); +// pgvector — just add a connection string +var ragFactory = new RagProviderFactory(); +var rag = ragFactory.CreatePgVector( + embeddings, + "Host=localhost;Database=council_vectors;Username=postgres;Password=postgres"); -var keeper = new KnowledgeKeeper( - rag, - new CouncilMember("llama3.2:3b", ollama, "Knowledge Keeper"), - "my_knowledge"); +await rag.IndexDocumentAsync("my_collection", documentText); +var results = await rag.SearchAsync("my_collection", "query", limit: 5); +// Wire into a Knowledge Keeper +var kkMember = new CouncilMember("llama3.2:3b", ollama, "Knowledge Keeper"); +var keeper = new KnowledgeKeeper(rag, kkMember, "my_knowledge"); await keeper.IndexFileAsync("./docs/architecture.md"); ``` +…or use **Qdrant**: + +```csharp +var rag = ragFactory.CreateQdrant(embeddings, "localhost", 6334); +``` + --- ## 🛠️ Operator (MCP Tools) @@ -184,16 +248,16 @@ to the debate participants. 1. The Operator connects to one or more MCP servers and discovers their tools on `InitializeAsync`. 2. Participants are told (in their system prompt) what the Operator can do, and can delegate a task at - **any moment** during the debate by writing a marker in their message: + **any moment** during the debate by writing a marker in their message: - ``` - [[OPERATOR: search the web for the latest EU AI Act timeline and save it to Notion]] - ``` + ``` + [[OPERATOR: search the web for the latest EU AI Act timeline and save it to Notion]] + ``` 3. The Operator interprets the request with its **own (cheaper) LLM model**, picks and calls the right - MCP tools, interprets the results, and returns a concise answer that is injected into the next round. + MCP tools, interprets the results, and returns a concise answer that is injected into the next round. 4. If the council uses **context compression**, the Operator can reuse the same strategy to compress - large tool outputs before they re-enter the debate. + large tool outputs before they re-enter the debate. All Operator interactions are recorded per round and rendered in the final Markdown report under a **🛠️ Operator Interactions** block. @@ -210,32 +274,30 @@ var ollama = new OllamaProvider("http://localhost:11434"); // Define the MCP servers the Operator may use var servers = new[] { - // stdio transport — launches a local MCP server process - McpServerConfig.Stdio( - name: "everything", - command: "npx", - arguments: new[] { "-y", "@modelcontextprotocol/server-everything" }), - - McpServerConfig.Stdio( - name: "filesystem", - command: "npx", - arguments: new[] { "-y", "@modelcontextprotocol/server-filesystem", "/data" }), - - // …or an HTTP/SSE transport for a remote MCP server - // McpServerConfig.Http( - // name: "remote", - // endpoint: "https://my-mcp-host.example.com/mcp", - // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }), + McpServerConfig.Stdio( + name: "everything", + command: "npx", + arguments: new[] { "-y", "@modelcontextprotocol/server-everything" }), + + McpServerConfig.Stdio( + name: "filesystem", + command: "npx", + arguments: new[] { "-y", "@modelcontextprotocol/server-filesystem", "/data" }), + + // McpServerConfig.Http( + // name: "remote", + // endpoint: "https://my-mcp-host.example.com/mcp", + // additionalHeaders: new Dictionary { ["Authorization"] = "Bearer " }), }; var council = new CouncilBuilder() - .AddMember("gpt-oss:20b", ollama, "Optimist") - .AddMember("llama3.1:8b", ollama, "Skeptic") - .WithChairman("gpt-oss:20b", ollama) - // Operator uses its own cheaper model; reuseCompression shares the council's compressor - .WithOperator("llama3.2:3b", ollama, servers, reuseCompression: true) - .WithTopic("Should we migrate the data pipeline to Kafka?") - .Build(); + .AddMember("gpt-oss:20b", ollama, "Optimist") + .AddMember("llama3.1:8b", ollama, "Skeptic") + .WithChairman("gpt-oss:20b", ollama) + // Operator uses its own cheaper model; reuseCompression shares the council's compressor + .WithOperator("llama3.2:3b", ollama, servers, reuseCompression: true) + .WithTopic("Should we migrate the data pipeline to Kafka?") + .Build(); var result = await council.ExecuteAsync(); ``` @@ -244,22 +306,17 @@ Prefer to build the `Operator` yourself? Pass a pre-built instance: ```csharp var @operator = new Operator( - new CouncilMember("llama3.2:3b", ollama, "Operator"), - new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }, - compressor: null, // optional IContextCompressor - compressionOptions: null); // optional CompressionOptions + new CouncilMember("llama3.2:3b", ollama, "Operator"), + new IMcpClient[] { new McpClientAdapter(servers[0]), new McpClientAdapter(servers[1]) }, + compressor: null, // optional IContextCompressor + compressionOptions: null); // optional CompressionOptions var council = new CouncilBuilder() - /* …members… */ - .WithOperator(@operator) - .Build(); + /* …members… */ + .WithOperator(@operator) + .Build(); ``` -### Dependency Injection - -Configure the Operator declaratively in `appsettings.json` under `Delibera:Operator` (see the -configuration section below), then build the council from `CouncilOptions` as usual. - --- ## 🗣️ Debate Strategies @@ -270,10 +327,16 @@ configuration section below), then build the council from `CouncilOptions` as us | **CritiqueDebate** | Position → Attack → Defence → Judge | Hypothesis testing | | **ConsensusDebate** | Perspectives → Common Ground → Consensus → Facilitator | Optimal solution search | +Each strategy is implemented as an `IDebateStrategy` — combine with `Builder`, `Template Method` +(`DebateScenario`) and `Factory` patterns (`ProviderFactory`, `RagProviderFactory`, `CompressionFactory`, +`Chairman`) to compose custom flows. + --- ## 📁 Output Files +Each deliberation can be exported as a single file or as three separate Markdown documents: + ```csharp var result = await executor.ExecuteAsync(); @@ -287,6 +350,12 @@ var (resultPath, statsPath, logsPath) = await result.SaveAllAsync("./output"); // → debate__logs.md (ExecutionLog) ``` +| File | Contents | +| ----------------- | ---------------------------------------------------------------------------- | +| `*_result.md` | Full deliberation transcript, rounds, and the Chairman's final verdict | +| `*_statistics.md` | Token usage statistics with per-round breakdown | +| `*_logs.md` | Execution logs (`ExecutionLog`) for Chairman, KK, compression & participants | + --- ## 📦 Configuration (`appsettings.json`) @@ -294,37 +363,53 @@ var (resultPath, statsPath, logsPath) = await result.SaveAllAsync("./output"); ```json { "Delibera": { - "Strategy": "Standard", - "MaxRounds": 4, - "Temperature": 0.7, - "Providers": { - "DefaultType": "Ollama", - "DefaultEndpoint": "http://localhost:11434", - "ApiKey": "", - "EmbeddingModel": "nomic-embed-text" - }, - "Compression": { "Enabled": true, "Strategy": "Hybrid", "TargetRatio": 0.5 }, - "Rag": { "Enabled": false, "ProviderType": "Qdrant", "Host": "localhost", "Port": 6334 }, - "Operator": { - "Enabled": true, - "ModelName": "llama3.2:3b", - "ReuseCompression": true, - "McpServers": [ - { - "Name": "filesystem", - "Transport": "Stdio", - "Command": "npx", - "Arguments": [ "-y", "@modelcontextprotocol/server-filesystem", "/data" ] - }, - { - "Name": "remote", - "Transport": "Http", - "Endpoint": "https://my-mcp-host.example.com/mcp", - "AdditionalHeaders": { "Authorization": "Bearer " } - } - ] - }, - "Output": { "Directory": "./debate_results", "SeparateFiles": true } + "Strategy": "Standard", + "MaxRounds": 4, + "Temperature": 0.7, + "SystemPrompt": "You are a knowledgeable AI expert participating in a council debate.", + "Providers": { + "DefaultType": "Ollama", + "DefaultEndpoint": "http://localhost:11434", + "ApiKey": "", + "EmbeddingModel": "nomic-embed-text" + }, + "Compression": { + "Enabled": true, + "Strategy": "Hybrid", + "TargetRatio": 0.5, + "EnableCache": true, + "MaxCacheEntries": 256 + }, + "Rag": { + "Enabled": false, + "ProviderType": "Qdrant", + "Host": "localhost", + "Port": 6334, + "CollectionName": "council_knowledge" + }, + "Operator": { + "Enabled": true, + "ModelName": "llama3.2:3b", + "ReuseCompression": true, + "McpServers": [ + { + "Name": "filesystem", + "Transport": "Stdio", + "Command": "npx", + "Arguments": [ "-y", "@modelcontextprotocol/server-filesystem", "/data" ] + }, + { + "Name": "remote", + "Transport": "Http", + "Endpoint": "https://my-mcp-host.example.com/mcp", + "AdditionalHeaders": { "Authorization": "Bearer " } + } + ] + }, + "Output": { + "Directory": "./debate_results", + "SeparateFiles": true + } } } ``` @@ -336,31 +421,30 @@ in `Providers:ApiKey`. ## 📦 NuGet Dependencies -| Package | Purpose | -| ------------------------ | -------------------------------- | -| `OllamaSharp` | Ollama API client | -| `Qdrant.Client` | Qdrant vector DB gRPC client | -| `Npgsql` / `Pgvector` | PostgreSQL/pgvector support | -| `ModelContextProtocol` | MCP client for the Operator role | -| `Microsoft.Extensions.AI`| `IChatClient` / `IEmbeddingGenerator` abstractions & middleware | -| `Microsoft.Extensions.*` | Configuration, DI and Options | +| Package | Purpose | +| ------------------------- | ----------------------------------------------------------- | +| `Microsoft.Extensions.AI` | `IChatClient` / `IEmbeddingGenerator` abstractions & middleware | +| `OllamaSharp` | Ollama API client | +| `Qdrant.Client` | Qdrant vector DB gRPC client | +| `Npgsql` / `Pgvector` | PostgreSQL/pgvector support | +| `ModelContextProtocol` | MCP client for the Operator role | +| `Microsoft.Extensions.*` | Configuration, DI and Options | --- ## 📚 Learn More -- 📖 Full README (architecture, design patterns, console app examples) → [github.com/delibera/Delibera](https://github.com/delibera/Delibera) -- 📄 Step-by-step walkthrough → [docs/QuickStart.md](https://github.com/delibera/Delibera/blob/main/docs/QuickStart.md) -- 🤝 [CONTRIBUTING.md](https://github.com/delibera/Delibera/blob/main/CONTRIBUTING.md) +- 📖 Full README (architecture, design patterns, console app examples) → [github.com/techbuzzz/Delibera](https://github.com/techbuzzz/Delibera) +- 📄 Step-by-step walkthrough → [docs/QuickStart.md](https://github.com/techbuzzz/Delibera/blob/develop/docs/QuickStart.md) +- 💻 Console app examples → [src/Delibera.ConsoleApp](https://github.com/techbuzzz/Delibera/tree/develop/src/Delibera.ConsoleApp) +- 🤝 [CONTRIBUTING.md](https://github.com/techbuzzz/Delibera/blob/develop/CONTRIBUTING.md) --- ## 📄 License -[MIT](../LICENSE) — Copyright © 2026 Delibera Project. - -
+[MIT](https://github.com/techbuzzz/Delibera/blob/develop/LICENSE) — Copyright © 2026 Delibera Project. **⚖️ Delibera — Thoughtful AI Decisions** -
+*Built with care for AI-powered collective intelligence* \ No newline at end of file From 2ce7385e0bc92ef51f12758b37e5bff7f78bebad Mon Sep 17 00:00:00 2001 From: Victor Buzin Date: Fri, 19 Jun 2026 11:04:17 +0200 Subject: [PATCH 5/5] Add NuGet icon and include README.md in package Added icon.png as the NuGet package icon via and ensured README.md is packed with the package for better documentation. --- src/Delibera.Core/Delibera.Core.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Delibera.Core/Delibera.Core.csproj b/src/Delibera.Core/Delibera.Core.csproj index 727d501..2d3a366 100644 --- a/src/Delibera.Core/Delibera.Core.csproj +++ b/src/Delibera.Core/Delibera.Core.csproj @@ -21,6 +21,7 @@ Copyright © 2026 Techbuzzz llm;ai;debate;council;deliberation;rag;qdrant;pgvector;ollama;openai;vector-search;embeddings;dependency-injection README.md + icon.png MIT https://github.com/techbuzzz/Delibera git @@ -34,6 +35,7 @@ +