Skip to content

Commit 0f20c3c

Browse files
Use XML for the model response; add examples
1 parent 2d27169 commit 0f20c3c

6 files changed

Lines changed: 306 additions & 30 deletions

File tree

docker-compose.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
services:
3+
consensus:
4+
image: ghcr.io/yetanotherchris/consensus:latest
5+
container_name: consensus
6+
ports:
7+
- "8585:8080"
8+
# Set an env var of CONSENSUS_APIKEY with your key value first.
9+
environment:
10+
- Consensus__ApiEndpoint=https://openrouter.ai/api/v1
11+
- Consensus__ApiKey=${CONSENSUS__APIKEY}
12+
- Consensus__Models__0=openai/gpt-4
13+
- Consensus__Models__1=anthropic/claude-3-opus
14+
- Consensus__Models__2=microsoft/phi-4
15+
- Consensus__Models__3=google/gemini-2.5-flash
16+
volumes:
17+
- ./output:/app/output
18+
restart: unless-stopped

docs/README.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,50 @@ ReactJS and Tailwind for the frontend, .NET 9 with Microsoft Agent Framework and
66

77
The app builds prompts for building a consensus using [prompt templates](https://github.com/yetanotherchris/consensus/tree/main/src/Consensus.Core/Templates) and then the first LLM as the judge.
88

9-
## Quickstart
10-
11-
`docker run todo`
12-
13-
## Docs
14-
- [Example prompt and model costs](./prompt-model-costs.md)
15-
169
## Screenshot
1710
<img width="773" height="331" alt="image" src="https://github.com/user-attachments/assets/2dc34183-d185-4294-b4d5-f9125fab44ea" />
1811

12+
## Quickstart
13+
14+
```
15+
docker run -d \
16+
-p 8080:8080 \
17+
-e Consensus__ApiEndpoint="https://openrouter.ai/api/v1" \
18+
-e Consensus__ApiKey="your-api-key-here" \
19+
-e Consensus__Models__0="openai/gpt-4" \
20+
-e Consensus__Models__1="anthropic/claude-3-opus" \
21+
-e Consensus__Models__2="microsoft/phi-4" \
22+
-e Consensus__Models__3="google/gemini-2.5-flash" \
23+
-v $(pwd)/output:/app/output \
24+
--name consensus \
25+
ghcr.io/yetanotherchris/consensus:latest
26+
```
27+
28+
Now go to http://localhost:8585/
29+
30+
Or use Docker compose:
31+
32+
```
33+
services:
34+
consensus:
35+
image: ghcr.io/yetanotherchris/consensus:latest
36+
container_name: consensus
37+
ports:
38+
- "8085:8080"
39+
environment:
40+
- Consensus__ApiEndpoint=https://openrouter.ai/api/v1
41+
- Consensus__ApiKey=your-api-key-here
42+
- Consensus__Models__0=openai/gpt-4
43+
- Consensus__Models__1=anthropic/claude-3-opus
44+
- Consensus__Models__2=google/gemini-pro
45+
- Consensus__Models__3=microsoft/phi-4
46+
volumes:
47+
- ./output:/app/output
48+
restart: unless-stopped
49+
```
50+
51+
[Example prompt and model costs](./prompt-model-costs.md)
52+
1953

2054
## Local development
2155

docs/prompt-model-costs.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
# Example prompt
22

3+
This prompt should have a disagreement (by Microsoft Phi) with the top 5 list, where it thinks Paracetamol is in the list.
4+
5+
```
36
What are the top 5 prescribed drugs in the UK?
4-
Factor in the typical length of time a single prescription lasts, e.g. 2 weeks supply. Factor in this and the number of prescriptions.
7+
Factor in the typical length of time a single prescription lasts,
8+
e.g. 2 weeks supply. Factor this in with the number of prescriptions given.
9+
10+
Include information about drugs prescribed for mental health, and format using a numbered list.
11+
```
512

6-
Include information about drugs for mental health.
13+
**Another example**
14+
15+
```
16+
Find research for links between paracetamol (acetaminophen) usage during pregnancy and autism. Decide which viewpoint has stronger evidence
17+
```
718

819
# Model costs
920
## Cheap models

src/Consensus.Core/Services/SynthesizerService.cs

Lines changed: 189 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.RegularExpressions;
2+
using System.Xml.Linq;
23
using Consensus.Channels;
34
using Consensus.Configuration;
45
using Consensus.Models;
@@ -71,17 +72,63 @@ public async Task<ConsensusResult> SynthesizeAsync(
7172
}
7273

7374
private ConsensusResult ParseSynthesisResponse(string synthesisResponse, List<ModelResponse> originalResponses, string originalPrompt, string runId)
75+
{
76+
// Try XML parsing first
77+
try
78+
{
79+
return ParseXmlSynthesisResponse(synthesisResponse, originalResponses, originalPrompt, runId);
80+
}
81+
catch (Exception ex)
82+
{
83+
_runTracker.WriteLog(runId, $"XML parsing failed: {ex.Message}, falling back to text parsing");
84+
}
85+
86+
// Fall back to text-based parsing for backwards compatibility
87+
return ParseTextSynthesisResponse(synthesisResponse, originalResponses, originalPrompt, runId);
88+
}
89+
90+
private ConsensusResult ParseXmlSynthesisResponse(string synthesisResponse, List<ModelResponse> originalResponses, string originalPrompt, string runId)
91+
{
92+
// Extract the <synthesis> block if it exists
93+
var synthesisMatch = Regex.Match(synthesisResponse, @"<synthesis>(.+?)</synthesis>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
94+
string xmlContent = synthesisMatch.Success ? $"<synthesis>{synthesisMatch.Groups[1].Value}</synthesis>" : synthesisResponse;
95+
96+
var doc = XDocument.Parse(xmlContent);
97+
var synthesis = doc.Root ?? throw new Exception("No root element found");
98+
99+
var result = new ConsensusResult
100+
{
101+
SynthesizedAnswer = synthesis.Element("synthesized_answer")?.Value.Trim() ?? string.Empty,
102+
SynthesisReasoning = synthesis.Element("reasoning")?.Value.Trim() ?? string.Empty,
103+
Summary = synthesis.Element("summary")?.Value.Trim() ?? "No summary provided",
104+
OverallConfidence = ParseConfidence(synthesis.Element("confidence")?.Value ?? "0"),
105+
ConsensusLevel = ParseConsensusLevel(synthesis.Element("consensus_level")?.Value ?? "Conflicted"),
106+
IndividualResponses = originalResponses,
107+
AgreementPoints = ParseXmlAgreementPoints(synthesis.Element("agreement_points")),
108+
Disagreements = ParseXmlDisagreements(synthesis.Element("disagreements")),
109+
OriginalPrompt = originalPrompt
110+
};
111+
112+
if (string.IsNullOrWhiteSpace(result.SynthesizedAnswer))
113+
{
114+
throw new Exception("Synthesized answer is empty");
115+
}
116+
117+
return result;
118+
}
119+
120+
private ConsensusResult ParseTextSynthesisResponse(string synthesisResponse, List<ModelResponse> originalResponses, string originalPrompt, string runId)
74121
{
75122
// Extract summary from XML tags
76123
string summary = "No summary provided by model";
77-
var summaryMatch = Regex.Match(synthesisResponse, @"<summary>(.+?)</summary>",
124+
var summaryMatch = Regex.Match(synthesisResponse, @"<summary>(.+?)</summary>",
78125
RegexOptions.Singleline | RegexOptions.IgnoreCase);
79-
126+
80127
if (summaryMatch.Success)
81128
{
82129
summary = summaryMatch.Groups[1].Value.Trim();
83130
}
84-
131+
85132
// Extract structured sections from the synthesis response
86133
var result = new ConsensusResult
87134
{
@@ -108,15 +155,24 @@ private ConsensusResult ParseSynthesisResponse(string synthesisResponse, List<Mo
108155

109156
private string ExtractSection(string response, string sectionName)
110157
{
111-
// Try to extract content after "SECTION_NAME:"
158+
// Try format 1: "SECTION_NAME:" followed by content until next section or end
112159
var pattern = $@"{Regex.Escape(sectionName)}:\s*(.+?)(?=\n[A-Z\s]+:|$)";
113160
var match = Regex.Match(response, pattern, RegexOptions.Singleline | RegexOptions.IgnoreCase);
114-
161+
115162
if (match.Success)
116163
{
117164
return match.Groups[1].Value.Trim();
118165
}
119166

167+
// Try format 2: Markdown header "## SECTION_NAME" or "# SECTION_NAME"
168+
var markdownPattern = $@"^#{1,3}\s+{Regex.Escape(sectionName)}\s*$\n(.+?)(?=^#{1,3}\s+|\Z)";
169+
var markdownMatch = Regex.Match(response, markdownPattern, RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Multiline);
170+
171+
if (markdownMatch.Success)
172+
{
173+
return markdownMatch.Groups[1].Value.Trim();
174+
}
175+
120176
return string.Empty;
121177
}
122178

@@ -188,11 +244,134 @@ private List<Disagreement> ParseDisagreements(string disagreementText)
188244
if (string.IsNullOrWhiteSpace(disagreementText))
189245
return disagreements;
190246

191-
// Only add disagreement if there's actual content to display
192-
// The empty disagreement with no views was causing "Model Disagreements" to show with no content
193-
// For now, we'll return an empty list since we don't have a sophisticated parser yet
194-
// A more sophisticated parser could extract multiple disagreements and their views
195-
247+
// Check if explicitly marked as "None"
248+
if (disagreementText.Trim().Equals("None", StringComparison.OrdinalIgnoreCase))
249+
return disagreements;
250+
251+
// Parse structured format:
252+
// - TOPIC: [topic description]
253+
// MODEL: [model name] - [their position]
254+
// MODEL: [model name] - [their position]
255+
var lines = disagreementText.Split('\n', StringSplitOptions.RemoveEmptyEntries);
256+
257+
Disagreement? currentDisagreement = null;
258+
259+
foreach (var line in lines)
260+
{
261+
var trimmedLine = line.Trim();
262+
263+
// Check for topic line (starts with - TOPIC:)
264+
if (trimmedLine.StartsWith("- TOPIC:", StringComparison.OrdinalIgnoreCase))
265+
{
266+
// Save previous disagreement if exists
267+
if (currentDisagreement != null && currentDisagreement.Views.Any())
268+
{
269+
disagreements.Add(currentDisagreement);
270+
}
271+
272+
// Start new disagreement
273+
var topic = trimmedLine.Substring("- TOPIC:".Length).Trim();
274+
currentDisagreement = new Disagreement
275+
{
276+
Topic = topic,
277+
Views = new List<DissentingView>()
278+
};
279+
}
280+
// Check for model view line (starts with MODEL:)
281+
else if (trimmedLine.StartsWith("MODEL:", StringComparison.OrdinalIgnoreCase) && currentDisagreement != null)
282+
{
283+
var viewText = trimmedLine.Substring("MODEL:".Length).Trim();
284+
285+
// Split on " - " to separate model name from position
286+
var parts = viewText.Split(new[] { " - " }, 2, StringSplitOptions.None);
287+
if (parts.Length == 2)
288+
{
289+
currentDisagreement.Views.Add(new DissentingView
290+
{
291+
ModelName = parts[0].Trim(),
292+
Position = parts[1].Trim()
293+
});
294+
}
295+
}
296+
}
297+
298+
// Add the last disagreement if exists
299+
if (currentDisagreement != null && currentDisagreement.Views.Any())
300+
{
301+
disagreements.Add(currentDisagreement);
302+
}
303+
304+
return disagreements;
305+
}
306+
307+
private List<ConsensusPoint> ParseXmlAgreementPoints(XElement? agreementPointsElement)
308+
{
309+
var points = new List<ConsensusPoint>();
310+
311+
if (agreementPointsElement == null)
312+
return points;
313+
314+
foreach (var pointElement in agreementPointsElement.Elements("point"))
315+
{
316+
var pointText = pointElement.Value.Trim();
317+
if (!string.IsNullOrWhiteSpace(pointText))
318+
{
319+
points.Add(new ConsensusPoint
320+
{
321+
Point = pointText,
322+
SupportingModels = 0,
323+
ModelNames = new List<string>()
324+
});
325+
}
326+
}
327+
328+
return points;
329+
}
330+
331+
private List<Disagreement> ParseXmlDisagreements(XElement? disagreementsElement)
332+
{
333+
var disagreements = new List<Disagreement>();
334+
335+
if (disagreementsElement == null)
336+
return disagreements;
337+
338+
foreach (var disagreementElement in disagreementsElement.Elements("disagreement"))
339+
{
340+
var topic = disagreementElement.Element("topic")?.Value.Trim();
341+
if (string.IsNullOrWhiteSpace(topic))
342+
continue;
343+
344+
var disagreement = new Disagreement
345+
{
346+
Topic = topic,
347+
Views = new List<DissentingView>()
348+
};
349+
350+
var viewsElement = disagreementElement.Element("views");
351+
if (viewsElement != null)
352+
{
353+
foreach (var viewElement in viewsElement.Elements("view"))
354+
{
355+
var modelName = viewElement.Element("model")?.Value.Trim();
356+
var position = viewElement.Element("position")?.Value.Trim();
357+
358+
if (!string.IsNullOrWhiteSpace(modelName) && !string.IsNullOrWhiteSpace(position))
359+
{
360+
disagreement.Views.Add(new DissentingView
361+
{
362+
ModelName = modelName,
363+
Position = position
364+
});
365+
}
366+
}
367+
}
368+
369+
if (disagreement.Views.Any())
370+
{
371+
disagreements.Add(disagreement);
372+
}
373+
}
374+
196375
return disagreements;
197376
}
198377
}

src/Consensus.Core/Templates/ConsensusOutput.html.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@
405405
{{ if .HasViews }}
406406
<ul style="margin-top: 10px; padding-left: 30px;">
407407
{{ range .Views }}
408-
<li>{{ . }}</li>
408+
<li><strong>{{ .ModelName }}:</strong> {{ .Position }}</li>
409409
{{ end }}
410410
</ul>
411411
{{ end }}

0 commit comments

Comments
 (0)