telegram: attach AI panel to alert-quality sends (advisory block)

Auto-scan and level-breach alert paths attach the panel AFTER the
quality gate and dedup decision (no LLM spend on suppressed alerts),
via run_in_executor so the event loop stays free. Alert message gains a
two-line 🤖 AI Panel block (verdict + confidence + rationale, persona
votes); /status shows the daily panel budget when the feature is on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SisdfkxnXNL9iZsXxN3T15
This commit is contained in:
Harwinder Sekhon
2026-08-07 22:32:57 -04:00
parent 26c8449877
commit 3fbe45e013
2 changed files with 47 additions and 0 deletions
+27
View File
@@ -71,6 +71,7 @@ from app_config import (
telegram_channel_id,
)
from providers import BaseDataProvider, make_data_provider
from agent_panel import attach_panel, panel_health
from llm_client import get_llm_client
from news_engine import FundamentalContext, NewsEngine
from pair_signals import PAIR_PRODUCERS
@@ -407,6 +408,17 @@ def format_signal_message(report: SignalReport) -> str:
extra.append(f"⚠️ {_escape_md(w)}")
if report.news_summary:
extra.append(f"📰 {_escape_md(report.news_summary)}")
if report.panel_verdict:
mark = {"AGREE": "✅", "OPPOSE": "⛔", "NEUTRAL": "➖"}.get(
report.panel_verdict, "")
extra.append(
f"🤖 AI Panel {mark} *{_escape_md(report.panel_verdict)}* "
f"({report.panel_confidence:.0f}%) — "
f"{_escape_md(report.panel_rationale)}")
if report.panel_takes:
votes = " · ".join(
f"{t['persona']}:{t['stance'][:3]}" for t in report.panel_takes)
extra.append(f" _{_escape_md(votes)}_")
if extra:
lines += [""] + extra
@@ -720,6 +732,13 @@ async def _scan_timeframe(
async with _alert_lock(key):
if not _should_alert(key, report):
continue
# Panel AFTER the quality gate + dedup decision so no LLM
# spend on suppressed alerts. Runs on the scan executor to
# keep the event loop free; this key's alert lock is held
# for the panel's wall time (≤~25 s), which only delays a
# same-key duplicate that _should_alert would drop anyway.
await asyncio.get_running_loop().run_in_executor(
_executor, attach_panel, report)
detail = format_signal_message(report)
delivered = False
for chat_id in _signal_targets():
@@ -855,6 +874,10 @@ async def _handle_breach(bot, pair_symbol: str, price: float, level_name: str) -
if not _should_alert(key, report):
return
# Same post-gate panel attach as the auto-scan path (see
# _scan_timeframe for the spend/lock rationale).
await asyncio.get_running_loop().run_in_executor(
_executor, attach_panel, report)
header = (
f"⚡ *LEVEL BREAK* — {_escape_md(display)} crossed "
f"*{_escape_md(label)}* @ `{_fmt_price(price)}`\n\n"
@@ -1193,6 +1216,10 @@ def _news_health_lines() -> List[str]:
else:
lines.append(f" ✅ DeepSeek: {used}/{cap} calls today,"
f" {scored} headlines scored")
if get_app_config().agent_panel_settings.get("enabled", False):
ph = panel_health()
lines.append(f" 🤖 AI panel: {ph['used_today']}/{ph['cap']}"
" panels today")
return lines