Skip to content
Merged
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@ 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.0] - 2026

### Added — Microsoft.Extensions.Logging, response-language enforcement, parallel Operator

- **Microsoft.Extensions.Logging support** — inject your own `ILogger` / `ILoggerFactory` and every
debate event (Chairman opening, Knowledge Keeper queries, compression, Operator interactions,
participant responses, errors) is forwarded to the host's logging pipeline. New APIs:
- `ICouncilBuilder.WithLogger(ILogger?)` and `CouncilBuilder.WithLogger(...)`.
- `ICouncilExecutor.Logger` property exposing the configured `ILogger`.
- `AddDelibera(IServiceCollection, IConfiguration, ILoggerFactory, string?)` DI overload that
auto-decorates every resolved `ICouncilBuilder` with a logger.
- `DebateExecutionOptions` record bundles the logger (plus response language + parallelism)
threaded through `IDebateStrategy.ExecuteAsync(...)` via a new
`IDebateStrategyWithOptions` interface (default method on `IDebateStrategy` keeps custom
strategies working unchanged).
- **Response-language enforcement** — `ICouncilBuilder.WithResponseLanguage(string?)` and
`CouncilOptions.ResponseLanguage`. When set, Delibera injects a strict directive into every
system and user prompt so all models (participants, Chairman, Knowledge Keeper, Operator) answer
exclusively in the chosen language, regardless of the prompt or retrieved RAG context.
- **Parallel Operator requests** — `[[OPERATOR: …]]` tasks delegated by participants within a round
now run concurrently via `Parallel.ForEachAsync`, bounded by
`ICouncilBuilder.WithMaxDegreeOfParallelism(int)` / `CouncilOptions.MaxDegreeOfParallelism`
(0 = unbounded, default). Delibera-shipped strategies (`StandardDebate`, `CritiqueDebate`,
`ConsensusDebate`) opt in via the new `ExecuteAsync(..., DebateExecutionOptions, ...)` overload.

### Changed

- **Renamed `LogLevel` enum to `ExecutionLogLevel`** (in `Delibera.Core.Models`) to avoid a name
clash with `Microsoft.Extensions.Logging.LogLevel`, which is now referenced throughout the
framework. The `ExecutionLog.Level` field, `DebateResult.ToLogsMarkdown()`, and the console
demo all use the renamed enum. `ExecutionLog.ToMicrosoftLogLevel()` maps to the M.E.Logging
severity. This is a source-breaking change for consumers that referenced `LogLevel.Info` etc.
directly; replace with `ExecutionLogLevel.Info`.

## [10.1.1] - 2026

### Added — Microsoft.Extensions.AI integration
Expand Down
217 changes: 186 additions & 31 deletions src/Delibera.ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@
Console.OutputEncoding = Encoding.UTF8;
PrintBanner();

// Top-level crash guard: any unhandled exception is printed in full and the
// console is kept open so the user can read the diagnostic before it closes.
// `alreadyReported` prevents double-printing when RunAsync prints its own
// debate-specific diagnostic (with tips) before re-throwing.
var alreadyReported = false;
try
{
await RunAsync(args, () => alreadyReported = true).ConfigureAwait(false);
}
catch (Exception ex)
{
if (!alreadyReported)
PrintFatalError(ex);

WaitForKeyOnExit("Press any key to exit…", isError: true);
return;
}

WaitForKeyOnExit("\n🏁 Delibera session complete. Press any key to exit…", isError: false);
}

private static async Task RunAsync(string[] args, Action onDebateFailed)
{

// ═══════════════════════════════════════════════
// 🆕 v3.1: DI & Separate Files Examples
// ═══════════════════════════════════════════════
Expand Down Expand Up @@ -316,22 +340,26 @@
var maxRounds = debateCfg.GetValue<int?>("MaxRounds") ?? 4;
var temperature = debateCfg.GetValue<float?>("Temperature") ?? 0.7f;

var systemPrompt = cfg["Prompts:SystemPrompt"] ?? "You are a helpful AI assistant participating in a council debate.";
var userPrompt = cfg["Prompts:UserPrompt"] ?? "What is the different between Microservices vs Monolith?";

IDebateStrategy strategy = stratName.ToLowerInvariant() switch
{
"critique" => new CritiqueDebate(),
"consensus" => new ConsensusDebate(),
_ => new StandardDebate()
};

var builder = new CouncilBuilder()
.WithStrategy(strategy)
.WithSystemPrompt(systemPrompt)
.WithUserPrompt(userPrompt)
.WithMaxRounds(maxRounds)
.WithTemperature(temperature);
var systemPrompt = cfg["Prompts:SystemPrompt"] ?? "You are a helpful AI assistant participating in a council debate.";
var userPrompt = cfg["Prompts:UserPrompt"] ?? "What is the different between Microservices vs Monolith?";
var responseLanguage = debateCfg["ResponseLanguage"];
var maxDegreeOfParallelism = debateCfg.GetValue<int?>("MaxDegreeOfParallelism") ?? 0;

IDebateStrategy strategy = stratName.ToLowerInvariant() switch
{
"critique" => new CritiqueDebate(),
"consensus" => new ConsensusDebate(),
_ => new StandardDebate()
};

var builder = new CouncilBuilder()
.WithStrategy(strategy)
.WithSystemPrompt(systemPrompt)
.WithUserPrompt(userPrompt)
.WithMaxRounds(maxRounds)
.WithTemperature(temperature)
.WithResponseLanguage(responseLanguage)
.WithMaxDegreeOfParallelism(maxDegreeOfParallelism);

// Add members
foreach (var mc in cfg.GetSection("Models").GetChildren())
Expand Down Expand Up @@ -400,6 +428,21 @@
Console.WriteLine("🎯 Starting debate...\n");
Console.WriteLine(new string('═', 60));

// Stream every ExecutionLog entry live so the user can watch progress in real time.
// Stored separately so we can also dump the full transcript at the end.
var liveLogs = new List<ExecutionLog>();
executor.OnLog += entry =>
{
liveLogs.Add(entry);
WriteLogEntry(entry);
};

// Surface non-fatal internal errors (e.g. failed MCP tool call) without aborting the debate.
executor.OnError += (ex, context) =>
{
WriteErrorEntry(ex, context);
};

executor.OnRoundCompleted += round =>
{
Console.WriteLine($"\n✅ Round {round.RoundNumber}: {round.RoundName} ({round.Duration.TotalSeconds:F1}s)");
Expand Down Expand Up @@ -497,42 +540,154 @@
if (result.ExecutionLogs.Count > 0)
{
Console.WriteLine($"\n 📋 Execution Logs ({result.ExecutionLogs.Count} entries):");
foreach (var log in result.ExecutionLogs.Where(l => l.Level >= LogLevel.Info))
foreach (var log in result.ExecutionLogs.Where(l => l.Level >= ExecutionLogLevel.Info))
Console.WriteLine($" {log}");
}
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ Debate failed: {ex.Message}");
PrintFatalError(ex, header: "❌ Debate failed");
Console.WriteLine("\n💡 Tips:");
Console.WriteLine(" • Ensure Ollama Cloud API key is set in appsettings.json");
Console.WriteLine(" • Or run a local Ollama server: ollama serve");
Console.WriteLine(" • For Qdrant RAG: docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant");
Console.WriteLine(" • For pgvector RAG: PostgreSQL 15+ with CREATE EXTENSION vector;");
Console.WriteLine($"\n{ex.StackTrace}");
onDebateFailed();
throw;
}

Console.WriteLine("\n🏁 Delibera session complete.");
}

private static void PrintBanner()
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("""

██████╗ ███████╗██╗ ██╗██████╗ ███████╗██████╗ █████╗
██╔══██╗██╔════╝██║ ██║██╔══██╗██╔════╝██╔══██╗██╔══██╗
██║ ██║█████╗ ██║ ██║██████╔╝█████╗ ██████╔╝███████║
██║ ██║██╔══╝ ██║ ██║██╔══██╗██╔══╝ ██╔══██╗██╔══██║
██████╔╝███████╗███████╗██║██████╔╝███████╗██║ ██║██║ ██║
╚═════╝ ╚══════╝╚══════╝╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
██████╗ ███████╗██╗ ██╗██████╗ ███████╗██████╗ █████╗
██╔══██╗██╔════╝██║ ██║██╔══██╗██╔════╝██╔══██╗██╔══██╗
██║ ██║█████╗ ██║ ██║██████╔╝█████╗ ██████╔╝███████║
██║ ██║██╔══╝ ██║ ██║██╔══██╗██╔══╝ ██╔══██╗██╔══██║
██████╔╝███████╗███████╗██║██████╔╝███████╗██║ ██║██║ ██║
╚═════╝ ╚══════╝╚══════╝╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝

⚖️ Thoughtful AI Decisions · v3.1

RAG • pgvector • Knowledge Keeper • Chairman
Context Compression • DI • Execution Logging

""");
Console.ResetColor();
}

// ─── Console observability helpers ─────────────────────────────────────────────

/// <summary>
/// Writes a single <see cref="ExecutionLog" /> entry to the console in a
/// colour-coded, single-line format. Safe to call from the executor's
/// streaming events.
/// </summary>
private static void WriteLogEntry(ExecutionLog entry)
{
var prev = Console.ForegroundColor;
Console.ForegroundColor = entry.Level switch
{
ExecutionLogLevel.Trace => ConsoleColor.DarkGray,
ExecutionLogLevel.Info => ConsoleColor.Cyan,
ExecutionLogLevel.Warning => ConsoleColor.Yellow,
ExecutionLogLevel.Error => ConsoleColor.Red,
_ => prev
};

Console.WriteLine($" ┊ {entry}");
Console.ForegroundColor = prev;
}

⚖️ Thoughtful AI Decisions · v3.1
/// <summary>
/// Writes a non-fatal internal error to the console with a small stack-trace
/// excerpt. The debate continues; this is purely informational.
/// </summary>
private static void WriteErrorEntry(Exception ex, string context)
{
var prev = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($" ┊ ⚠ {context} error: {ex.Message}");
if (ex.StackTrace is not null)
{
var firstFrame = ex.StackTrace
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault();
if (firstLineIsMeaningful(firstFrame))
Console.WriteLine($" ┊ at {firstFrame.Trim()}");

Check warning on line 619 in src/Delibera.ConsoleApp/Program.cs

View workflow job for this annotation

GitHub Actions / build & pack

Dereference of a possibly null reference.

Check warning on line 619 in src/Delibera.ConsoleApp/Program.cs

View workflow job for this annotation

GitHub Actions / build & pack

Dereference of a possibly null reference.
}

RAG • pgvector • Knowledge Keeper • Chairman
Context Compression • DI • Execution Logging
Console.ForegroundColor = prev;
}

""");
/// <summary>
/// Prints a full diagnostic panel for a fatal exception: type, message,
/// full stack trace and the live log transcript captured so far.
/// </summary>
/// <param name="ex">The exception that aborted the run.</param>
/// <param name="header">Optional header line; defaults to a generic label.</param>
private static void PrintFatalError(Exception ex, string header = "❌ Unhandled exception")
{
var prev = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine();
Console.WriteLine(new string('═', 60));
Console.WriteLine($" {header}");
Console.WriteLine(new string('═', 60));
Console.WriteLine($" Type: {ex.GetType().FullName}");
Console.WriteLine($" Message: {ex.Message}");
Console.WriteLine();
Console.WriteLine(" ── Stack trace ──");
Console.WriteLine(ex.StackTrace);
if (ex.InnerException is not null)
{
Console.WriteLine();
Console.WriteLine(" ── Inner exception ──");
Console.WriteLine($" Type: {ex.InnerException.GetType().FullName}");
Console.WriteLine($" Message: {ex.InnerException.Message}");
Console.WriteLine(ex.InnerException.StackTrace);
}

Console.ForegroundColor = prev;
}

/// <summary>
/// Pauses the console so the user can read output before the window closes.
/// Honoured in both normal and error paths. When <paramref name="isError" />
/// is <c>true</c> the prompt is shown in red and the exit code is set to 1.
/// </summary>
private static void WaitForKeyOnExit(string prompt, bool isError)
{
if (isError)
Console.ForegroundColor = ConsoleColor.Red;

Console.WriteLine();
Console.WriteLine(prompt);
Console.ResetColor();

try
{
Console.ReadKey(intercept: true);
}
catch (InvalidOperationException)
{
// No interactive console (e.g. redirected stdin in CI) — fall back gracefully.
Console.WriteLine("(no interactive console available; exiting.)");
}

Environment.ExitCode = isError ? 1 : 0;
}

private static bool firstLineIsMeaningful(string? frame)
{
if (string.IsNullOrWhiteSpace(frame))
return false;

var trimmed = frame.Trim();
// Filter out noise from runtime/compiler-emitted frames.
return !trimmed.StartsWith("at System.", StringComparison.Ordinal)
|| trimmed.Contains("Delibera", StringComparison.Ordinal);
}
}
6 changes: 5 additions & 1 deletion src/Delibera.ConsoleApp/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"MaxRounds": 4,
"Temperature": 0.7,
"SystemPrompt": "You are a knowledgeable AI expert participating in a council debate. Provide thorough, well-reasoned answers.",
"ResponseLanguage": "Russian",
"MaxDegreeOfParallelism": 0,

"Providers": {
"DefaultType": "Ollama",
Expand Down Expand Up @@ -92,7 +94,9 @@
"Strategy": "Standard",
"MaxRounds": 4,
"Temperature": 0.7,
"OutputDirectory": "./debate_results"
"OutputDirectory": "./debate_results",
"ResponseLanguage": "Russian",
"MaxDegreeOfParallelism": 0
},

"ContextCompression": {
Expand Down
Loading
Loading