mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
151 lines
5.2 KiB
Python
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")
|