11using System . Text . RegularExpressions ;
2+ using System . Xml . Linq ;
23using Consensus . Channels ;
34using Consensus . Configuration ;
45using 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}
0 commit comments