mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
150
thirdeye/backend/bot/commands.py
Normal file
150
thirdeye/backend/bot/commands.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user