Files
B.Tech-Project-III/thirdeye/backend/bot/commands.py
2026-04-05 00:43:23 +05:30

151 lines
5.2 KiB
Python

"""
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")