Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
root = true

[*]
indent_style = space
indent_size = 3
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{cs,vb}]
# Delibera's public API uses domain acronyms such as "LLM" in their conventional
# all-caps form (ILLMProvider, ChatClientLLMProvider, AsLLMProvider,
# LLMProviderChatClient). The .NET naming analyzers (IDE1006) want these
# lowercased after the first letter (IllmProvider, ChatClientLlmProvider, ...),
# which is a breaking API change for the published NuGet package *and* reads
# worse. There is no built-in capitalisation mode that permits all-caps acronyms
# mixed with PascalCase, so we disable the naming rules that fire on these
# identifiers while keeping the rest of the naming convention enforcement.
dotnet_diagnostic.IDE1006.severity = none
94 changes: 94 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,100 @@ 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.2.2] - 2026

### Added — Polly v8 resilience via Microsoft.Extensions.Http.Resilience

- **Microsoft.Extensions.Http.Resilience 10.7.0** dependency (transitively brings Polly v8
`Polly.Core` + `Microsoft.Extensions.Http`). Hand-rolled retry loops in `OllamaProvider`,
`YandexGptProvider`, and `McpClientAdapter` have been **removed** in favour of named
Polly v8 pipelines registered through DI.

- **`ResilienceOptions`** (bound from `Delibera:Resilience` configuration section)
configures: `MaxRetryAttempts`, `BaseDelay`, `MaxDelay`, `UseJitter`, `BackoffType`
(`"Exponential"` / `"Linear"` / `"Constant"`), `RetryableStatusCodes`, `AttemptTimeout`,
master `Enabled` flag. All values are live-tracked through `IOptionsMonitor<>` so option
changes are honoured without rebuilding the container.

- **`IDeliberaResiliencePipelineProvider`** — central registry of named Polly v8
pipelines with three built-in keys:
- `Delibera.Local` — retries connection-level failures only (no status code); used by
`OllamaConnectionMode.Local`.
- `Delibera.Cloud` — retries transient HTTP responses (configurable allow-list,
default `[408, 429, 500, 502, 503, 504, 524]`) plus `HttpRequestException` /
`TaskCanceledException`; used by `OllamaConnectionMode.Cloud`, `YandexGptProvider`,
and `McpClientAdapter`'s HTTP transport.
- `Delibera.Default` — alias for the more permissive of the two; used when a consumer
does not specify a pipeline key.

- **`AddDeliberaResilience(IServiceCollection, Action<ResilienceOptions>?)`** —
one-call DI setup. Registers `IDeliberaResiliencePipelineProvider`, plus three named
`HttpClient` entries (`Delibera.Ollama.Local`, `Delibera.Ollama.Cloud`,
`Delibera.YandexGPT`) each wired with `AddResilienceHandler` so retries apply to the
HttpClient handler chain (the standard `Microsoft.Extensions.Http.Resilience` pattern).

- **`AddDeliberaResiliencePipeline(name, build)`** — register custom Polly v8
pipelines under arbitrary keys. The pipeline registry merges them into its lookup
table alongside the built-ins.

- **`AddDeliberaHttpClient(name, pipelineName, configure)`** — register an arbitrary
named HttpClient whose handler pipeline is decorated with the chosen
Polly v8 pipeline. Useful for additional HTTP integrations beyond Delibera's
built-in providers.

- **`OllamaProvider` DI path** — `OllamaProvider.ForLocal(endpoint, IHttpClientFactory,
IDeliberaResiliencePipelineProvider, ...)` and `ForCloud(...)` overloads construct
the provider from the factory's named HttpClient and the operation-level pipeline
(`GetOperationPipeline`). The hand-rolled `for` loop and `IsTransientHttp` helper
have been deleted; transient failures are now retried by the configured pipeline.

- **`YandexGptProvider` DI path** — new constructor accepts `IHttpClientFactory?` +
`IDeliberaResiliencePipelineProvider?`. The original `(apiKey, folderId, ...)`
constructor remains for backward compatibility — providers constructed without DI
still work, just without retry.

- **`McpClientAdapter` DI path** — new constructor accepts `IHttpClientFactory?` +
logical client name. When wired, the adapter injects the factory-managed HttpClient
into the ModelContextProtocol `HttpClientTransport` so retries apply to MCP HTTP/SSE
traffic.

- **`ResilientHttpClientExtensions.SendAsync(http, pipeline, request, ...)`** —
extension helper that runs an `HttpClient` request through a typed
`ResiliencePipeline<HttpResponseMessage>`. Handles request cloning between attempts
(HttpClient disposes the request after the first attempt).

- **Console example** `ResilienceExample` (run with `--resilience`) showing how to
wire up `AddDeliberaResilience`, register a custom pipeline, and read the live
configuration through `IOptionsMonitor`.

- **Tests** — 9 new unit tests in `ResilienceTests` covering `ResilienceOptions`
defaults, the pipeline registry, `AddDeliberaResilience` DI wiring, configuration
binding from `Delibera:Resilience`, and custom-pipeline registration.

### Changed

- Bumped `Delibera.Core` package version `10.2.0` → `10.3.0`.
- Bumped console app dependencies to `Microsoft.Extensions.Http` 10.0.9 +
`Microsoft.Extensions.Http.Resilience` 10.7.0.
- `OllamaProvider.Client` is now constructed eagerly inside the provider's
constructor (was lazy before). `OllamaEmbeddingProvider`'s constructor still reads
`ollamaProvider.Client` synchronously, so the eager construction preserves the
existing pattern.
- `ResilienceOptions` is bound from the `Delibera:Resilience` configuration section
inside `AddDelibera(IConfiguration, ...)` so `IOptionsMonitor<ResilienceOptions>`
flows through DI transparently.

### Compatibility

- **No breaking changes for non-DI consumers.** The legacy `OllamaProvider(endpoint,
apiKey, ...)`, `YandexGptProvider(apiKey, folderId, ...)`, and `McpClientAdapter(config)`
constructors remain in place and behave exactly as before — without retries (the
behaviour before v10.2.2). Consumers that want retry semantics opt in by using the
new DI-aware overloads.
- **No breaking changes for DI consumers either** unless they previously relied on
the hand-rolled retry semantics in `OllamaProvider.ChatAsync` (not exposed
publicly; the loop was internal).

## [10.2.0] - 2026

### Added — Microsoft.Extensions.Logging, response-language enforcement, parallel Operator
Expand Down
2 changes: 2 additions & 0 deletions src/Delibera.ConsoleApp/Delibera.ConsoleApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Delibera.ConsoleApp.Examples;
/// Demonstrates Delibera usage with Dependency Injection.
/// Shows how to register services, resolve interfaces, and build a council via DI.
/// </summary>
public static class DIExample
public static class DependencyInjectionExample
{
/// <summary>
/// Runs the DI example — builds a service provider, resolves council services,
Expand Down
21 changes: 10 additions & 11 deletions src/Delibera.ConsoleApp/Examples/MicrosoftExtensionsAiExample.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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;
Expand Down Expand Up @@ -37,12 +36,12 @@ public static async Task RunAsync()

// 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
var pipeline = ollama
.AsChatClient()
.WithMiddleware(enableFunctionInvocation: true);
.WithMiddleware(true);

// Expose the decorated client back to Delibera as a normal provider.
ILLMProvider msaiProvider = pipeline.AsLLMProvider("Ollama (via Microsoft.Extensions.AI)");
var msaiProvider = pipeline.AsLLMProvider("Ollama (via Microsoft.Extensions.AI)");
Console.WriteLine($" ✦ Provider name: {msaiProvider.ProviderName}");

// ──────────────────────────────────────────────────────────────
Expand All @@ -53,8 +52,8 @@ public static async Task RunAsync()
{
await foreach (var chunk in msaiProvider.ChatStreamAsync(
model,
systemPrompt: "You are a concise assistant.",
userPrompt: "In one sentence, what is Microsoft.Extensions.AI?"))
"You are a concise assistant.",
"In one sentence, what is Microsoft.Extensions.AI?"))
Console.Write(chunk);
Console.WriteLine();
}
Expand All @@ -70,7 +69,7 @@ public static async Task RunAsync()
Console.WriteLine("\n 🧮 Embedding via IEmbeddingGenerator:");
try
{
IEmbeddingProvider embeddings = ollama
var embeddings = ollama
.AsEmbeddingGenerator()
.AsEmbeddingProvider("nomic-embed-text");

Expand All @@ -88,9 +87,9 @@ public static async Task RunAsync()
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");
"msai",
ollama.AsChatClient(),
"Ollama");

var executor = new CouncilBuilder()
.AddMember(model, councilProvider, "Architect")
Expand Down Expand Up @@ -127,4 +126,4 @@ private static void PrintTips(Exception ex)
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().");
}
}
}
26 changes: 14 additions & 12 deletions src/Delibera.ConsoleApp/Examples/OperatorExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ public static async Task RunAsync()
// (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"]);
"demo",
"npx",
["-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()]);
"files",
"npx",
["-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.
Expand All @@ -55,9 +55,9 @@ public static async Task RunAsync()
.AddMember("llama2", ollama, "Analyst")
.SetChairman(Chairman.CreateStandard("qwen2.5", ollama))
.WithOperator(
modelName: "llama3.2", // cheaper model for tool orchestration
provider: ollama,
servers: servers)
"llama3.2", // cheaper model for tool orchestration
ollama,
servers)
.WithStandardDebate()
.WithSystemPrompt("You are a meticulous research council. Delegate factual lookups to the Operator.")
.WithUserPrompt(
Expand Down Expand Up @@ -106,6 +106,8 @@ public static async Task RunAsync()
}
}

private static string Preview(string text, int max) =>
string.IsNullOrEmpty(text) ? "(empty)" : text.Length <= max ? text : text[..max] + "…";
}
private static string Preview(string text, int max)
{
return string.IsNullOrEmpty(text) ? "(empty)" : text.Length <= max ? text : text[..max] + "…";
}
}
60 changes: 37 additions & 23 deletions src/Delibera.ConsoleApp/Examples/OperatorMcpToolsExample.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using Delibera.Core.Council;
using Delibera.Core.Interfaces;
using Delibera.Core.Models;
Expand Down Expand Up @@ -31,15 +32,19 @@ namespace Delibera.ConsoleApp.Examples;
/// Пример показывает <b>оба</b> способа использования Operator:
/// </para>
/// <list type="number">
/// <item><description>Прямой вызов <see cref="Operator.ExecuteTaskAsync" /> (раздел A).</description></item>
/// <item><description>Делегирование внутри совета через маркер <c>[[OPERATOR: ...]]</c> (раздел B).</description></item>
/// <item>
/// <description>Прямой вызов <see cref="Operator.ExecuteTaskAsync" /> (раздел A).</description>
/// </item>
/// <item>
/// <description>Делегирование внутри совета через маркер <c>[[OPERATOR: ...]]</c> (раздел B).</description>
/// </item>
/// </list>
/// </summary>
public static class OperatorMcpToolsExample
{
public static async Task RunAsync()
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("🛠️ Operator + MCP-инструменты (браузер · Marp-презентации)\n");

// ── LLM-провайдер ──
Expand All @@ -56,9 +61,9 @@ public static async Task RunAsync()
// Предоставляет инструменты управления реальным браузером.
// Запускается как дочерний процесс через npx (нужен Node.js).
var browserServer = McpServerConfig.Stdio(
name: "browser",
command: "npx",
arguments: ["-y", "@playwright/mcp@latest", "--headless"]);
"browser",
"npx",
["-y", "@playwright/mcp@latest", "--headless"]);

// (2) 🎯 Marp — генерация презентаций из Markdown.
// Каталог вывода монтируется через рабочую директорию; готовые файлы
Expand All @@ -67,9 +72,9 @@ public static async Task RunAsync()
Directory.CreateDirectory(presentationsDir);

var marpServer = McpServerConfig.Stdio(
name: "marp",
command: "npx",
arguments: ["-y", "@marp-team/marp-cli", "--server", presentationsDir],
"marp",
"npx",
["-y", "@marp-team/marp-cli", "--server", presentationsDir],
workingDirectory: presentationsDir);

// (3) 🌐 HTTP-вариант (закомментирован): удалённый MCP-сервер браузера/поиска.
Expand Down Expand Up @@ -120,15 +125,15 @@ private static async Task RunDirectOperatorAsync(
// A.1 🌐 Браузерная задача — Operator сам выберет browser_navigate + browser_snapshot.
Console.WriteLine("\n▶ Задача 1 (браузер): прочитать заголовок и краткое содержание страницы.");
var browseResult = await @operator.ExecuteTaskAsync(
requesterName: "Аналитик",
task: "Открой https://modelcontextprotocol.io и кратко перескажи, что такое MCP, в 3 предложениях.");
"Аналитик",
"Открой 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.");
"Докладчик",
"Создай Marp-презентацию из 3 слайдов о преимуществах .NET 10 и сохрани её как deck.html.");
PrintResult(deckResult);
}
catch (Exception ex)
Expand All @@ -151,9 +156,9 @@ private static async Task RunCouncilWithOperatorAsync(
.AddMember("llama2", ollama, "Архитектор")
.SetChairman(Chairman.CreateStandard("qwen2.5", ollama))
.WithOperator(
modelName: "llama3.2", // дешёвая модель для оркестрации инструментов
provider: ollama,
servers: servers)
"llama3.2", // дешёвая модель для оркестрации инструментов
ollama,
servers)
.WithStandardDebate()
.WithSystemPrompt(
"Вы — совет по технологической стратегии. Когда нужны свежие факты из интернета " +
Expand Down Expand Up @@ -206,15 +211,22 @@ private static async Task RunCouncilWithOperatorAsync(
// ──────────────────────────────────────────────────────────────────

/// <summary>Создаёт MCP-клиент для конфигурации сервера.</summary>
private static IMcpClient IMcpClientFor(McpServerConfig config) => new McpClientAdapter(config);
private static IMcpClient IMcpClientFor(McpServerConfig config)
{
return new McpClientAdapter(config);
}

private static void PrintResult(OperatorResult result)
{
Console.WriteLine($" Вызвано инструментов: {result.ToolCalls.Count}" +
(result.Compressed ? " (ответ сжат)" : string.Empty));
(result.Compressed
? " (ответ сжат)"
: string.Empty));
foreach (var call in result.ToolCalls)
{
var status = call.IsError ? "❌" : "✓";
var status = call.IsError
? "❌"
: "✓";
Console.WriteLine($" {status} [{call.ServerName}] {call.ToolName}");
}

Expand All @@ -231,7 +243,9 @@ private static void PrintFailureTips(Exception ex)
Console.WriteLine(" • Свои MCP-серверы подключаются через McpServerConfig.Stdio / .Http.");
}

private static string Preview(string? text, int max) =>
string.IsNullOrEmpty(text) ? "(пусто)" :
text.Length <= max ? text : text[..max] + "…";
}
private static string Preview(string? text, int max)
{
return string.IsNullOrEmpty(text) ? "(пусто)" :
text.Length <= max ? text : text[..max] + "…";
}
}
3 changes: 1 addition & 2 deletions src/Delibera.ConsoleApp/Examples/PgVectorExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ of loosely coupled services. Each service is fine-grained and implements a singl
collectionName,
sampleDocument,
new Dictionary<string, string> { ["source"] = "architecture_guide.md" },
300,
50);
300);

Console.WriteLine($" ✅ Indexed {chunks} chunks into '{collectionName}'\n");

Expand Down
Loading
Loading