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 <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-01-16 20:03:46 +00:00
parent ddb0de0757
commit 8242de5289
2 changed files with 143 additions and 0 deletions

View file

@ -5,6 +5,7 @@ across multiple models, building context, and managing discussion flow.
""" """
import asyncio import asyncio
import json
import logging import logging
from moai.core.ai_client import get_ai_client from moai.core.ai_client import get_ai_client
@ -25,6 +26,33 @@ Guidelines:
- Focus on practical, actionable insights - Focus on practical, actionable insights
- If you reach agreement with others, state it clearly""" - 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( async def query_models_parallel(
models: list[str], models: list[str],
@ -227,3 +255,71 @@ async def run_discussion_round(
context_messages.append({"role": "user", "content": formatted}) context_messages.append({"role": "user", "content": formatted})
return responses 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),
}

View file

@ -8,6 +8,7 @@ from sqlalchemy.orm import selectinload
from moai.core.database import get_session from moai.core.database import get_session
from moai.core.models import ( from moai.core.models import (
Consensus,
Discussion, Discussion,
DiscussionStatus, DiscussionStatus,
DiscussionType, 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) select(Message).where(Message.round_id == round_id).order_by(Message.timestamp)
) )
return list(result.scalars().all()) 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()