diff --git a/src/moai/bot/handlers/__init__.py b/src/moai/bot/handlers/__init__.py index d7326c4..617c715 100644 --- a/src/moai/bot/handlers/__init__.py +++ b/src/moai/bot/handlers/__init__.py @@ -4,12 +4,13 @@ This module contains handlers for Telegram bot commands including project management, discussion commands, and export functionality. """ -from telegram.ext import Application, CommandHandler +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, discuss_command, + mention_handler, next_command, open_command, stop_command, @@ -41,3 +42,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)) + + # @mention handler - MessageHandler registered AFTER CommandHandlers + # Matches messages starting with @claude, @gpt, or @gemini followed by content + app.add_handler(MessageHandler(filters.Regex(r"^@(claude|gpt|gemini)\s"), mention_handler)) diff --git a/src/moai/bot/handlers/discussion.py b/src/moai/bot/handlers/discussion.py index 28ac79b..2d6b1b2 100644 --- a/src/moai/bot/handlers/discussion.py +++ b/src/moai/bot/handlers/discussion.py @@ -6,13 +6,14 @@ 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_models_parallel, run_discussion_round +from moai.core.orchestrator import 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_current_round, get_discussion, ) @@ -343,3 +344,84 @@ async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No except Exception as e: await update.message.reply_text(f"Error: {e}") + + +async def mention_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Handle @model mention messages. + + Messages starting with @claude, @gpt, or @gemini are routed to that + specific model. If a discussion is active, includes full context. + + Examples: + @claude What do you think about this? + @gpt Can you elaborate on your previous point? + @gemini Do you agree with Claude? + """ + message_text = update.message.text + if not message_text: + return + + # Parse model name from first word (e.g., "@claude" -> "claude") + parts = message_text.split(maxsplit=1) + if not parts: + return + + model_tag = parts[0].lower() + # Strip the @ prefix + model_name = model_tag.lstrip("@") + + # Validate model + if model_name not in MODEL_MAP: + return # Not a valid model mention, ignore + + # Get the rest of the message as content + content = parts[1] if len(parts) > 1 else "" + if not content.strip(): + await update.message.reply_text( + f"Usage: @{model_name} \n\nExample: @{model_name} What do you think?" + ) + return + + # Get project context if available + project = await get_selected_project(context) + if project is None: + await update.message.reply_text("No project selected. Use /project select first.") + return + + project_name = project.name + + # Check for active discussion (for context) + discussion = await get_active_discussion(project.id) + + # Show typing indicator + await update.message.chat.send_action("typing") + + try: + # Query the model directly with optional discussion context + response = await query_model_direct( + model=model_name, + message=content, + discussion=discussion, + project_name=project_name, + ) + + # If there's an active discussion, persist the message + if discussion is not None: + # Get or create a round for this direct message + current_round = await get_current_round(discussion.id) + if current_round is not None: + await create_message( + round_id=current_round.id, + model=model_name, + content=response, + is_direct=True, + ) + + # Format response + await update.message.reply_text( + f"*@{model_name.title()} (direct):*\n{response}", + parse_mode="Markdown", + ) + + except Exception as e: + await update.message.reply_text(f"Error: {e}")