From 8242de5289906a3fae2dfe712685389e1b7761b0 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 16 Jan 2026 20:03:46 +0000 Subject: [PATCH] feat(06-01): add consensus generation to orchestrator and service - Add CONSENSUS_PROMPT constant for AI consensus analysis - Add generate_consensus() function that builds context and calls AI - Add save_consensus() and get_consensus() to discussion service - Import json module and Consensus model Co-Authored-By: Claude Opus 4.5 --- src/moai/core/orchestrator.py | 96 ++++++++++++++++++++++++++++ src/moai/core/services/discussion.py | 47 ++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/src/moai/core/orchestrator.py b/src/moai/core/orchestrator.py index bddd60b..e7e360f 100644 --- a/src/moai/core/orchestrator.py +++ b/src/moai/core/orchestrator.py @@ -5,6 +5,7 @@ across multiple models, building context, and managing discussion flow. """ import asyncio +import json import logging from moai.core.ai_client import get_ai_client @@ -25,6 +26,33 @@ Guidelines: - Focus on practical, actionable insights - If you reach agreement with others, state it clearly""" +# Prompt for generating consensus summary +CONSENSUS_PROMPT = """Analyze the discussion above and provide a JSON summary with: +1. "agreements" - A list of strings, each being a point all participants agreed on +2. "disagreements" - A list of objects, each with: + - "topic": The topic of disagreement + - "positions": An object mapping model names to their positions + +Only include items where there was clear agreement or disagreement. +Respond with ONLY valid JSON, no markdown formatting or explanation. + +Example format: +{ + "agreements": [ + "Python is a great language for beginners", + "Documentation is important" + ], + "disagreements": [ + { + "topic": "Best web framework", + "positions": { + "claude": "FastAPI for its modern async support", + "gpt": "Django for its batteries-included approach" + } + } + ] +}""" + async def query_models_parallel( models: list[str], @@ -227,3 +255,71 @@ async def run_discussion_round( context_messages.append({"role": "user", "content": formatted}) return responses + + +async def generate_consensus(discussion: Discussion, model: str = "claude") -> dict: + """Generate a consensus summary from a discussion. + + Analyzes all rounds and messages in the discussion to identify + agreements and disagreements among the participating AI models. + + Args: + discussion: Discussion object with eager-loaded rounds and messages. + model: Model short name to use for generating the consensus (default: "claude"). + + Returns: + Dict with "agreements" (list of strings) and "disagreements" + (list of {topic, positions} objects). + """ + client = get_ai_client() + + # Build context from the discussion + context_messages = build_context(discussion) + + # Add the consensus prompt as the final user message + context_messages.append({"role": "user", "content": CONSENSUS_PROMPT}) + + # System prompt for consensus generation + system_prompt = """You are an impartial analyst summarizing a multi-model AI discussion. +Your task is to identify clear agreements and disagreements from the conversation. +Be precise and only include items where there was genuine consensus or difference of opinion. +Respond with valid JSON only.""" + + try: + response = await client.complete( + model=model, + messages=context_messages, + system_prompt=system_prompt, + ) + logger.info("Consensus generated successfully by %s", model) + + # Parse JSON response, handling potential markdown code blocks + json_str = response.strip() + if json_str.startswith("```"): + # Strip markdown code block + lines = json_str.split("\n") + json_str = "\n".join(lines[1:-1]) if lines[-1] == "```" else "\n".join(lines[1:]) + json_str = json_str.strip() + + result = json.loads(json_str) + + # Ensure required keys exist with defaults + return { + "agreements": result.get("agreements", []), + "disagreements": result.get("disagreements", []), + } + + except json.JSONDecodeError as e: + logger.error("Failed to parse consensus JSON: %s", e) + return { + "agreements": [], + "disagreements": [], + "error": f"Failed to parse AI response: {e}", + } + except Exception as e: + logger.error("Consensus generation failed: %s", e) + return { + "agreements": [], + "disagreements": [], + "error": str(e), + } diff --git a/src/moai/core/services/discussion.py b/src/moai/core/services/discussion.py index bf84885..348a3f6 100644 --- a/src/moai/core/services/discussion.py +++ b/src/moai/core/services/discussion.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import selectinload from moai.core.database import get_session from moai.core.models import ( + Consensus, Discussion, DiscussionStatus, DiscussionType, @@ -217,3 +218,49 @@ async def get_round_messages(round_id: str) -> list[Message]: select(Message).where(Message.round_id == round_id).order_by(Message.timestamp) ) return list(result.scalars().all()) + + +async def save_consensus( + discussion_id: str, + agreements: list, + disagreements: list, + generated_by: str, +) -> Consensus: + """Save a consensus summary for a discussion. + + Args: + discussion_id: The discussion's UUID. + agreements: List of agreement strings. + disagreements: List of disagreement dicts with topic and positions. + generated_by: The model that generated the consensus. + + Returns: + The created Consensus object. + """ + async with get_session() as session: + consensus = Consensus( + discussion_id=discussion_id, + agreements=agreements, + disagreements=disagreements, + generated_by=generated_by, + ) + session.add(consensus) + await session.flush() + await session.refresh(consensus) + return consensus + + +async def get_consensus(discussion_id: str) -> Consensus | None: + """Get the consensus for a discussion if it exists. + + Args: + discussion_id: The discussion's UUID. + + Returns: + The Consensus object if found, None otherwise. + """ + async with get_session() as session: + result = await session.execute( + select(Consensus).where(Consensus.discussion_id == discussion_id) + ) + return result.scalar_one_or_none()