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:
parent
ddb0de0757
commit
8242de5289
2 changed files with 143 additions and 0 deletions
|
|
@ -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),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue