Overview
Add a full gRPC transport layer to Delibera.Server as a new project Delibera.Grpc.
Parallel to the existing REST+SSE API, gRPC provides:
- Strongly-typed contract (
.proto as the single source of truth)
- Bi-directional streaming for live debate rounds (replaces SSE for gRPC clients)
- Auto-generated C# client (
Delibera.Grpc.Client NuGet) for SDK consumers
- Better performance for high-frequency integrations (enterprise BPM, CI pipelines)
Branch: feature/p03-grpc
Project Structure
src/
├── Delibera.Server/ # existing — add Grpc service registrations
├── Delibera.Grpc/ # NEW — proto files + generated stubs
│ ├── Protos/
│ │ └── delibera.proto
│ ├── Services/
│ │ ├── DebateGrpcService.cs
│ │ ├── ScenarioGrpcService.cs
│ │ └── CorpusGrpcService.cs
│ └── Delibera.Grpc.csproj
└── Delibera.Grpc.Client/ # NEW — client NuGet package
├── DeliberaGrpcClient.cs
└── Delibera.Grpc.Client.csproj
Proto Schema — delibera.proto
Derived from existing contracts:
ScenarioRequest / ScenarioMember / ScenarioChairman → src/Delibera.Server/Api/Contracts/ScenarioRequest.cs
DebateResponse / VerdictDto / VotingResultDto / TokenStatsDto → DebateResponse.cs, VerdictDto.cs
- SSE events
debate-round / debate-completed → SseDebateStreamWriter.cs
syntax = "proto3";
option csharp_namespace = "Delibera.Grpc";
package delibera.v1;
import "google/protobuf/timestamp.proto";
import "google/protobuf/struct.proto";
// ─────────────────────────────────────────────────────────────────────────────
// SERVICE: DebateService
// ─────────────────────────────────────────────────────────────────────────────
service DebateService {
// Unary: get debate by ID
rpc GetDebate (GetDebateRequest) returns (DebateResponse);
// Server streaming: stream live rounds (mirrors SSE /stream)
rpc StreamDebate (StreamDebateRequest) returns (stream DebateEvent);
// Bi-directional: client sends control signals, server streams rounds
rpc RunDebate (stream DebateControl) returns (stream DebateEvent);
// Unary: cancel a running debate
rpc CancelDebate (CancelDebateRequest) returns (CancelDebateResponse);
// Unary: list all debates
rpc ListDebates (ListDebatesRequest) returns (ListDebatesResponse);
}
// ─────────────────────────────────────────────────────────────────────────────
// SERVICE: ScenarioService
// ─────────────────────────────────────────────────────────────────────────────
service ScenarioService {
// Unary: run a fully self-describing scenario (mirrors POST /api/v1/scenarios)
rpc RunScenario (ScenarioRequest) returns (DebateResponse);
// Server streaming: run scenario and stream rounds live
rpc RunScenarioStream (ScenarioRequest) returns (stream DebateEvent);
}
// ─────────────────────────────────────────────────────────────────────────────
// SERVICE: TemplateService
// ─────────────────────────────────────────────────────────────────────────────
service TemplateService {
rpc ListTemplates (ListTemplatesRequest) returns (ListTemplatesResponse);
rpc RunTemplate (RunTemplateRequest) returns (DebateResponse);
rpc RunTemplateStream (RunTemplateRequest) returns (stream DebateEvent);
}
// ─────────────────────────────────────────────────────────────────────────────
// SERVICE: CorpusService
// ─────────────────────────────────────────────────────────────────────────────
service CorpusService {
rpc CreateCorpus (CreateCorpusRequest) returns (CorpusResponse);
rpc GetCorpus (GetCorpusRequest) returns (CorpusResponse);
rpc DeleteCorpus (DeleteCorpusRequest) returns (DeleteCorpusResponse);
rpc ListCorpora (ListCorporaRequest) returns (ListCorporaResponse);
}
// ─────────────────────────────────────────────────────────────────────────────
// MESSAGES: Scenario / Council composition
// ─────────────────────────────────────────────────────────────────────────────
message ScenarioRequest {
string question = 1; // required
repeated Member members = 2; // required, min 1
string strategy = 3; // Standard | Critique | Consensus
Chairman chairman = 4;
string voting_strategy = 5; // Majority | BordaCount | Weighted | ""
map<string, float> member_weights = 6; // used when voting_strategy = Weighted
int32 max_rounds = 7; // default 3
float temperature = 8; // default 0.7
string system_prompt = 9;
string knowledge_text = 10;
repeated string corpus_ids = 11;
string output_schema = 12; // JSON Schema string
string compression_strategy = 13; // Hybrid | Semantic | Dedup | Summarization | None
google.protobuf.Struct input_data = 14; // free-form JSON
string label = 15;
string response_language = 16;
int32 max_degree_of_parallelism = 17;
}
message Member {
string role = 1; // required
string model = 2;
string provider = 3; // Ollama | OpenAI | AzureOpenAI | Anthropic
string persona = 4;
float weight = 5; // default 1.0
string capabilities = 6; // Text | Vision | Both
}
message Chairman {
string model = 1;
string provider = 2;
string system_prompt = 3;
bool opening_statement = 4;
}
// ─────────────────────────────────────────────────────────────────────────────
// MESSAGES: Debate responses
// ─────────────────────────────────────────────────────────────────────────────
message DebateResponse {
string debate_id = 1;
string template_id = 2;
string tenant_id = 3;
DebateStatus status = 4;
string final_verdict = 5;
Verdict verdict = 6;
VotingResult voting = 7;
TokenStats token_stats = 8;
string error_message = 9;
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp completed_at = 11;
}
enum DebateStatus {
DEBATE_STATUS_UNSPECIFIED = 0;
PENDING = 1;
RUNNING = 2;
COMPLETED = 3;
FAILED = 4;
CANCELLED = 5;
PAUSED = 6;
}
message Verdict {
string recommendation = 1;
string risk_level = 2; // Low | Medium | High | Critical
string rationale = 3;
float confidence = 4; // 0.0 – 1.0
repeated string conditions = 5;
repeated Risk risks = 6;
string raw_json = 7;
}
message Risk {
string category = 1;
string description = 2;
string severity = 3;
string mitigation = 4;
}
message VotingResult {
string strategy = 1;
string winner = 2;
int32 total_votes = 3;
repeated Tally tally = 4;
}
message Tally {
string candidate = 1;
int32 votes = 2;
float score = 3;
}
message TokenStats {
int32 total_input_tokens = 1;
int32 total_output_tokens = 2;
int32 total_tokens = 3;
int32 saved_by_compression = 4;
}
// ─────────────────────────────────────────────────────────────────────────────
// MESSAGES: Streaming events (mirrors SSE debate-round / debate-completed)
// ─────────────────────────────────────────────────────────────────────────────
message DebateEvent {
oneof payload {
DebateRoundEvent round = 1;
DebateCompletedEvent completed = 2;
DebateErrorEvent error = 3;
DebateStatusEvent status = 4;
}
}
message DebateRoundEvent {
string debate_id = 1;
int32 round_index = 2;
string speaker = 3;
string role = 4;
string content = 5;
google.protobuf.Timestamp timestamp = 6;
}
message DebateCompletedEvent {
string debate_id = 1;
DebateStatus status = 2;
string verdict = 3;
google.protobuf.Timestamp completed_at = 4;
string error = 5;
}
message DebateErrorEvent {
string debate_id = 1;
string code = 2;
string message = 3;
}
message DebateStatusEvent {
string debate_id = 1;
DebateStatus status = 2;
}
// ─────────────────────────────────────────────────────────────────────────────
// MESSAGES: Bi-directional control (client → server in RunDebate)
// ─────────────────────────────────────────────────────────────────────────────
message DebateControl {
oneof action {
ScenarioRequest start = 1; // first message: start debate
PauseAction pause = 2;
ResumeAction resume = 3;
CancelAction cancel = 4;
}
}
message PauseAction { string debate_id = 1; }
message ResumeAction { string debate_id = 1; }
message CancelAction { string debate_id = 1; string reason = 2; }
// ─────────────────────────────────────────────────────────────────────────────
// MESSAGES: Get / List / Cancel Debate
// ─────────────────────────────────────────────────────────────────────────────
message GetDebateRequest { string debate_id = 1; }
message StreamDebateRequest { string debate_id = 1; }
message CancelDebateRequest { string debate_id = 1; string reason = 2; }
message CancelDebateResponse { bool success = 1; string message = 2; }
message ListDebatesRequest {
string tenant_id = 1;
string label = 2;
int32 page = 3;
int32 page_size = 4;
}
message ListDebatesResponse {
repeated DebateResponse debates = 1;
int32 total_count = 2;
}
// ─────────────────────────────────────────────────────────────────────────────
// MESSAGES: Templates
// ─────────────────────────────────────────────────────────────────────────────
message ListTemplatesRequest {}
message ListTemplatesResponse { repeated TemplateDto templates = 1; }
message TemplateDto {
string id = 1;
string name = 2;
string description = 3;
string vertical = 4; // Engineering | Governance | Legal | Product
}
message RunTemplateRequest {
string template_id = 1;
string question = 2;
google.protobuf.Struct input_data = 3;
repeated string corpus_ids = 4;
string label = 5;
}
// ─────────────────────────────────────────────────────────────────────────────
// MESSAGES: Corpus
// ─────────────────────────────────────────────────────────────────────────────
message CreateCorpusRequest {
string tenant_id = 1;
string name = 2;
string description = 3;
string content = 4;
}
message GetCorpusRequest { string corpus_id = 1; }
message DeleteCorpusRequest { string corpus_id = 1; }
message DeleteCorpusResponse { bool success = 1; }
message ListCorporaRequest { string tenant_id = 1; }
message CorpusResponse {
string corpus_id = 1;
string name = 2;
string description = 3;
int32 chunk_count = 4;
google.protobuf.Timestamp created_at = 5;
}
message ListCorporaResponse { repeated CorpusResponse corpora = 1; }
Delibera.Grpc.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.67.*" />
<PackageReference Include="Google.Protobuf" Version="3.29.*" />
<PackageReference Include="Grpc.Tools" Version="2.67.*" PrivateAssets="All" />
<ProjectReference Include="../Delibera.Server/Delibera.Server.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos/delibera.proto" GrpcServices="Server" />
</ItemGroup>
</Project>
Delibera.Grpc.Client.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>Delibera.Grpc.Client</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.Net.Client" Version="2.67.*" />
<PackageReference Include="Google.Protobuf" Version="3.29.*" />
<PackageReference Include="Grpc.Tools" Version="2.67.*" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="../Delibera.Grpc/Protos/delibera.proto" GrpcServices="Client" Link="Protos/delibera.proto" />
</ItemGroup>
</Project>
Program.cs — регистрация gRPC
Добавить в Delibera.Server/Program.cs:
// ── gRPC ──────────────────────────────────────────────────────────────────────
builder.Services.AddGrpc(o =>
{
o.EnableDetailedErrors = builder.Environment.IsDevelopment();
o.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16 MB
});
builder.Services.AddGrpcReflection(); // grpcurl support
// ... после app.Build():
app.MapGrpcService<DebateGrpcService>();
app.MapGrpcService<ScenarioGrpcService>();
app.MapGrpcService<TemplateGrpcService>();
app.MapGrpcService<CorpusGrpcService>();
if (app.Environment.IsDevelopment())
app.MapGrpcReflectionService();
Bi-Directional Streaming — RunDebate
RunDebate — главный метод. Клиент отправляет первым сообщением ScenarioRequest (обёрнутый в DebateControl.start), сервер запускает CouncilExecutor и стримит DebateEvent по мере выполнения раундов. Клиент может в любой момент послать PauseAction / ResumeAction / CancelAction.
// Services/DebateGrpcService.cs — скелет
public override async Task RunDebate(
IAsyncStreamReader<DebateControl> requestStream,
IServerStreamWriter<DebateEvent> responseStream,
ServerCallContext context)
{
// 1. Read first message — must be ScenarioRequest (start)
await requestStream.MoveNext(context.CancellationToken);
var start = requestStream.Current.Start
?? throw new RpcException(new Status(StatusCode.InvalidArgument, "First message must be 'start'"));
// 2. Build council from ScenarioRequest
var record = await _debateService.StartAsync(start.ToScenarioRequest(), context.CancellationToken);
// 3. Stream rounds + listen for control signals in parallel
using var cts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);
var streamTask = StreamRoundsAsync(record, responseStream, cts.Token);
var controlTask = ListenControlAsync(requestStream, record, cts);
await Task.WhenAll(streamTask, controlTask);
}
Acceptance Criteria
Branch
feature/p03-grpc
Labels
enhancement, grpc, server
Overview
Add a full gRPC transport layer to
Delibera.Serveras a new projectDelibera.Grpc.Parallel to the existing REST+SSE API, gRPC provides:
.protoas the single source of truth)Delibera.Grpc.ClientNuGet) for SDK consumersBranch:
feature/p03-grpcProject Structure
Proto Schema —
delibera.protoDerived from existing contracts:
ScenarioRequest/ScenarioMember/ScenarioChairman→src/Delibera.Server/Api/Contracts/ScenarioRequest.csDebateResponse/VerdictDto/VotingResultDto/TokenStatsDto→DebateResponse.cs,VerdictDto.csdebate-round/debate-completed→SseDebateStreamWriter.csDelibera.Grpc.csprojDelibera.Grpc.Client.csprojProgram.cs— регистрация gRPCДобавить в
Delibera.Server/Program.cs:Bi-Directional Streaming —
RunDebateRunDebate— главный метод. Клиент отправляет первым сообщениемScenarioRequest(обёрнутый вDebateControl.start), сервер запускаетCouncilExecutorи стримитDebateEventпо мере выполнения раундов. Клиент может в любой момент послатьPauseAction/ResumeAction/CancelAction.Acceptance Criteria
src/Delibera.Grpc/Protos/delibera.protoсоздан и валиден (protoc --versionбез ошибок)dotnet build src/Delibera.Grpc→ 0 errors (C# stubs сгенерированыGrpc.Tools)dotnet build src/Delibera.Grpc.Client→ 0 errors (C# клиент сгенерирован)DebateGrpcService,ScenarioGrpcService,TemplateGrpcService,CorpusGrpcServiceреализованыRunDebatebi-directional stream работает: start → rounds → completedRunScenarioStreamserver stream работает end-to-endgrpcurlвозвращает список сервисов)Delibera.Server/Program.csрегистрирует все gRPC сервисыDelibera.slnxобновлён — добавленыDelibera.GrpcиDelibera.Grpc.Clientgrpcurlи C# клиентаBranch
feature/p03-grpcLabels
enhancement,grpc,server