""" ThirdEye bot commands — voice intelligence. Houses cmd_voicelog and any future command handlers that don't belong in the main bot.py module. """ import logging from datetime import datetime logger = logging.getLogger("thirdeye.bot.commands") async def cmd_voicelog(update, context): """ /voicelog [filter] Audit trail of all voice note decisions, actions, and blockers in this group. Usage: /voicelog — all voice-sourced signals (last 20) /voicelog decisions — only decisions from voice notes /voicelog actions — only action items from voice notes /voicelog blockers — only blockers from voice notes /voicelog @Raj — only voice notes by Raj /voicelog search [query] — search voice note content """ from backend.db.chroma import query_signals, get_all_signals from backend.agents.voice_transcriber import format_duration chat_id = str(update.effective_chat.id) args = context.args or [] filter_type = None filter_speaker = None search_query = None if args: first = args[0].lower() if first == "decisions": filter_type = "architecture_decision" elif first == "actions": filter_type = "action_item" elif first == "blockers": filter_type = "blocker" elif first == "search" and len(args) > 1: search_query = " ".join(args[1:]) elif first.startswith("@"): filter_speaker = first[1:] await update.message.reply_text("🎤 Searching voice notes...", parse_mode="Markdown") if search_query: raw_signals = query_signals(chat_id, search_query, n_results=30) else: raw_signals = get_all_signals(chat_id) # Normalise: both query_signals and get_all_signals return # {"document": ..., "metadata": {...}, "id": ...} shaped dicts. # Flatten metadata to top-level for uniform field access below. def _flatten(s: dict) -> dict: meta = s.get("metadata", {}) flat = {**meta} flat.setdefault("id", s.get("id", "")) flat.setdefault("document", s.get("document", "")) return flat all_signals = [_flatten(s) for s in raw_signals] # Filter to voice-sourced signals only voice_signals = [ s for s in all_signals if s.get("source") == "voice" or s.get("type") == "voice_transcript" or "[Voice @" in s.get("summary", "") ] if filter_type: voice_signals = [s for s in voice_signals if s.get("type") == filter_type] if filter_speaker: voice_signals = [ s for s in voice_signals if filter_speaker.lower() in s.get("speaker", "").lower() or filter_speaker.lower() in str(s.get("entities", [])).lower() ] # Prefer structured signals; fall back to raw transcripts if none structured = [s for s in voice_signals if s.get("type") != "voice_transcript"] display_signals = structured if structured else voice_signals # Sort by timestamp descending def _ts(s): try: return datetime.fromisoformat(s.get("timestamp", "").replace("Z", "+00:00")) except Exception: return datetime.min display_signals.sort(key=_ts, reverse=True) display_signals = display_signals[:20] if not display_signals: await update.message.reply_text( "📭 No voice note signals found. Voice notes are transcribed automatically when sent here.", parse_mode="Markdown", ) return type_emoji = { "architecture_decision": "🏗️", "tech_debt": "⚠️", "action_item": "📌", "blocker": "🚧", "feature_request": "💡", "promise": "🤝", "risk": "🔴", "recurring_bug": "🐛", "voice_transcript": "🎤", } filter_label = "" if filter_type: filter_label = f" — {filter_type.replace('_', ' ').title()}" elif filter_speaker: filter_label = f" — @{filter_speaker}" elif search_query: filter_label = f" — '{search_query}'" lines = [f"🎤 *Voice Note Audit Trail*{filter_label}\n_{len(display_signals)} signal(s)_\n"] for sig in display_signals: ts = sig.get("timestamp", "") date_str = "" if ts: try: dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) date_str = dt.strftime("%b %d") except Exception: date_str = ts[:10] speaker = sig.get("speaker", "") duration = sig.get("voice_duration", 0) duration_str = format_duration(int(duration)) if duration else "" emoji = type_emoji.get(sig.get("type", ""), "🎤") summary = sig.get("summary", "") if summary.startswith("[Voice @"): summary = summary.split("] ", 1)[-1] if "] " in summary else summary meta_parts = [f"@{speaker}" if speaker else "", date_str, duration_str] meta = " · ".join(filter(None, meta_parts)) lines.append(f"{emoji} *{meta}*\n _{summary[:100]}_\n") await update.message.reply_text("\n".join(lines), parse_mode="Markdown")