From ee9f8ca3a4b5bdba770fd619c6d77706e391aebb Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 16 Jan 2026 20:06:52 +0000 Subject: [PATCH] feat(06-01): create /consensus command handler - Add consensus_command handler with typing indicator - Add _format_consensus helper for Markdown output - Register CommandHandler("consensus") in __init__.py - Displays existing consensus or generates new one Co-Authored-By: Claude Opus 4.5 --- src/moai/bot/handlers/__init__.py | 2 + src/moai/bot/handlers/discussion.py | 125 +++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/moai/bot/handlers/__init__.py b/src/moai/bot/handlers/__init__.py index 617c715..1709b18 100644 --- a/src/moai/bot/handlers/__init__.py +++ b/src/moai/bot/handlers/__init__.py @@ -9,6 +9,7 @@ from telegram.ext import Application, CommandHandler, MessageHandler, filters from moai.bot.handlers.commands import help_command, start_command from moai.bot.handlers.discussion import ( ask_command, + consensus_command, discuss_command, mention_handler, next_command, @@ -42,6 +43,7 @@ def register_handlers(app: Application) -> None: app.add_handler(CommandHandler("discuss", discuss_command)) app.add_handler(CommandHandler("next", next_command)) app.add_handler(CommandHandler("stop", stop_command)) + app.add_handler(CommandHandler("consensus", consensus_command)) # @mention handler - MessageHandler registered AFTER CommandHandlers # Matches messages starting with @claude, @gpt, or @gemini followed by content diff --git a/src/moai/bot/handlers/discussion.py b/src/moai/bot/handlers/discussion.py index 2d6b1b2..71ebcb6 100644 --- a/src/moai/bot/handlers/discussion.py +++ b/src/moai/bot/handlers/discussion.py @@ -6,15 +6,22 @@ from telegram.ext import ContextTypes from moai.bot.handlers.projects import get_selected_project from moai.core.ai_client import MODEL_MAP, get_ai_client from moai.core.models import DiscussionType, RoundType -from moai.core.orchestrator import query_model_direct, query_models_parallel, run_discussion_round +from moai.core.orchestrator import ( + generate_consensus, + query_model_direct, + query_models_parallel, + run_discussion_round, +) from moai.core.services.discussion import ( complete_discussion, create_discussion, create_message, create_round, get_active_discussion, + get_consensus, get_current_round, get_discussion, + save_consensus, ) @@ -425,3 +432,119 @@ async def mention_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> except Exception as e: await update.message.reply_text(f"Error: {e}") + + +async def consensus_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Handle /consensus command - generate or display consensus summary. + + Requires a selected project with an active discussion. If a consensus + already exists, displays it. Otherwise generates a new one using AI. + + Examples: + /consensus + """ + # Require a selected project + project = await get_selected_project(context) + if project is None: + await update.message.reply_text("No project selected. Use /project select first.") + return + + # Check for active discussion + discussion = await get_active_discussion(project.id) + if discussion is None: + await update.message.reply_text( + "No active discussion. Start one with /open first." + ) + return + + # Check if consensus already exists + existing_consensus = await get_consensus(discussion.id) + if existing_consensus is not None: + # Display existing consensus + response_text = _format_consensus( + agreements=existing_consensus.agreements, + disagreements=existing_consensus.disagreements, + generated_by=existing_consensus.generated_by, + ) + await update.message.reply_text(response_text, parse_mode="Markdown") + return + + # Show typing indicator while generating + await update.message.chat.send_action("typing") + + try: + # Reload discussion with full context + discussion = await get_discussion(discussion.id) + + # Generate consensus + consensus_model = "claude" + result = await generate_consensus(discussion, model=consensus_model) + + # Check for errors + if "error" in result: + await update.message.reply_text(f"Failed to generate consensus: {result['error']}") + return + + # Save consensus + await save_consensus( + discussion_id=discussion.id, + agreements=result["agreements"], + disagreements=result["disagreements"], + generated_by=consensus_model, + ) + + # Format and display + response_text = _format_consensus( + agreements=result["agreements"], + disagreements=result["disagreements"], + generated_by=consensus_model, + ) + await update.message.reply_text(response_text, parse_mode="Markdown") + + except Exception as e: + await update.message.reply_text(f"Error: {e}") + + +def _format_consensus( + agreements: list, + disagreements: list, + generated_by: str, +) -> str: + """Format consensus data into a Markdown string. + + Args: + agreements: List of agreement strings. + disagreements: List of disagreement dicts with topic and positions. + generated_by: The model that generated the consensus. + + Returns: + Formatted Markdown string. + """ + lines = ["*Consensus Summary*\n"] + + # Agreements section + lines.append("*Agreements:*") + if agreements: + for point in agreements: + lines.append(f"- {point}") + else: + lines.append("- None identified") + lines.append("") + + # Disagreements section + lines.append("*Disagreements:*") + if disagreements: + for disagreement in disagreements: + topic = disagreement.get("topic", "Unknown topic") + positions = disagreement.get("positions", {}) + lines.append(f"- *{topic}*") + for model, position in positions.items(): + lines.append(f" - {model.title()}: {position}") + else: + lines.append("- None identified") + lines.append("") + + # Footer + lines.append(f"_Generated by {generated_by.title()}_") + + return "\n".join(lines)