rugmuncher-backend / app /databus /daily_intel.py
cryptorugmunch
merge: chore/cleanup-remove-bloat-and-secrets into main
bde2f3a
Raw
History Blame Contribute Delete
16.8 kB
"""
RugCharts Daily Intelligence Briefing
======================================
THE daily market briefing. AI-researched, AI-written, human-quality.
Published 6:30 AM ET to X (@CryptoRugMunch), Telegram, Ghost CMS.
Pipeline:
1. Gather โ€” all DataBus sources (prices, news, CT, sentiment, fear/greed, memes)
2. Research โ€” OpenRouter free model analyzes everything
3. Write โ€” OpenRouter free model produces the final report
4. Publish โ€” X/Twitter, Telegram, Ghost CMS
Free models used (zero cost):
Research: nvidia/nemotron-3-super-120b-a12b:free (1M ctx, 120B MoE)
Writing: google/gemma-4-26b-a4b-it:free (262K ctx, excellent prose)
"""
import logging
import os
import re
import subprocess
from datetime import UTC, datetime
import httpx
logger = logging.getLogger("daily_intel")
OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY", "")
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
# Free models for each phase
RESEARCH_MODEL = "nvidia/nemotron-3-super-120b-a12b:free"
WRITING_MODEL = "google/gemma-4-26b-a4b-it:free"
# Fallback if primary unavailable
FALLBACK_RESEARCH = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free"
FALLBACK_WRITING = "moonshotai/kimi-k2.6:free"
# Publishing targets
X_ACCOUNT = "CryptoRugMunch"
GHOST_URL = os.getenv("GHOST_URL", "http://172.19.0.3:2368")
GHOST_KEY = os.getenv("GHOST_ADMIN_API_KEY", "") or os.getenv("GHOST_CONTENT_API_KEY", "")
async def _openrouter_chat(model: str, system: str, user: str, max_tokens: int = 1500, temperature: float = 0.5) -> str:
"""Call OpenRouter with a free model."""
if not OPENROUTER_KEY:
return ""
try:
async with httpx.AsyncClient(timeout=90) as c:
r = await c.post(
OPENROUTER_URL,
headers={
"Authorization": f"Bearer {OPENROUTER_KEY}",
"Content-Type": "application/json",
"HTTP-Referer": "https://rugmunch.io",
"X-Title": "RugCharts Daily Intel",
},
json={
"model": model,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"temperature": temperature,
"max_tokens": max_tokens,
},
)
if r.status_code == 200:
return r.json()["choices"][0]["message"]["content"]
else:
logger.warning(f"OpenRouter {model}: {r.status_code} {r.text[:200]}")
return ""
except Exception as e:
logger.warning(f"OpenRouter error: {e}")
return ""
async def _gather_all_data() -> dict:
"""Gather comprehensive data from ALL DataBus sources."""
data = {
"market": {},
"fear_greed": {},
"news": {},
"ct": {},
"social": {},
"prediction_markets": {},
}
# Market data
try:
from app.databus.news_provider import get_market_brief
data["market"] = await get_market_brief()
except Exception:
pass
# News intel
try:
from app.databus.news_intel import aggregate_all_news
data["news"] = await aggregate_all_news(limit=20)
except Exception:
pass
# CT Rundown
try:
from app.databus.x_intel import fetch_ct_rundown
data["ct"] = await fetch_ct_rundown(limit=15)
except Exception:
pass
# Social metrics
try:
from app.databus.social_intel import get_social_metrics
data["social"] = await get_social_metrics()
except Exception:
pass
# Fear & Greed
try:
from app.databus.news_provider import get_fear_greed
data["fear_greed"] = await get_fear_greed()
except Exception:
pass
# Prediction markets
try:
from app.databus.news_provider import get_prediction_markets
data["prediction_markets"] = await get_prediction_markets(limit=5)
except Exception:
pass
return data
def _build_research_context(data: dict) -> str:
"""Build comprehensive context for the research model."""
parts = []
# Market snapshot
market = data.get("market", {})
if market.get("brief"):
parts.append(f"## MARKET SNAPSHOT\n{market['brief']}")
# Fear & Greed
fg = data.get("fear_greed", {})
if fg.get("value"):
parts.append(f"## FEAR & GREED INDEX\n{fg['value']}/100 โ€” {fg.get('classification', 'Neutral')}")
# News headlines
news = data.get("news", {})
articles = news.get("articles", [])
if articles:
headlines = "\n".join(
f"- [{a.get('sentiment', {}).get('sentiment', 'โž–')}] {a.get('title', '')}" for a in articles[:15]
)
parts.append(f"## TOP HEADLINES\n{headlines}")
# CT Pulse
ct = data.get("ct", {})
rundown = ct.get("rundown", [])
if rundown:
ct_pulse = "\n".join(f"- @{s.get('author_handle', '?')}: {s.get('text', '')[:150]}" for s in rundown[:10])
parts.append(f"## CRYPTO TWITTER PULSE\n{ct_pulse}")
# Social metrics
social = data.get("social", {})
if social.get("trending_topics"):
topics = social["trending_topics"]
parts.append(f"## TRENDING TOPICS\n{', '.join(list(topics.keys())[:10])}")
# Prediction markets
pm = data.get("prediction_markets", {})
pmarkets = pm.get("markets", [])
if pmarkets:
pm_str = "\n".join(f"- {m.get('title', '')[:80]}: ${m.get('volume', 0):,.0f} vol" for m in pmarkets[:3])
parts.append(f"## PREDICTION MARKETS\n{pm_str}")
return "\n\n".join(parts)
WRITING_STANDARDS = """You are a senior financial writer for RugCharts Daily Intelligence.
WRITING STANDARDS:
- Human, conversational tone. Like a sharp newsletter, not a robot.
- No AI-isms: never use "delve", "tapestry", "landscape", "robust", "moreover", "furthermore"
- Lead with the most important story. Hook the reader.
- Be specific: use numbers, names, percentages. No vague statements.
- Include market sentiment, social mood, and what traders are actually talking about
- One section on MEMES/CULTURE โ€” what's trending on CT
- One section on RISK RADAR โ€” scams, hacks, regulatory threats to watch
- End with BOTTOM LINE โ€” actionable takeaway in 2 sentences
FORMAT EXACTLY LIKE THIS:
# RUGCHARTS DAILY INTELLIGENCE
## {Date}
### MARKET SNAPSHOT
{2-3 sentences on overall market}
### TOP STORIES
{3-5 bullet points of most important news with brief context}
### SENTIMENT CHECK
{Market mood: fear/greed, social sentiment, what CT is feeling}
### MEMES & CULTURE
{What's trending on CT, notable memes, cultural moments}
### RISK RADAR
{Scams, hacks, regulatory actions, things to avoid today}
### BOTTOM LINE
{1-2 sentence actionable takeaway}
---
Published by RugCharts Daily Intelligence
Subscribe: https://rugmunch.io/news"""
async def generate_daily_intel(publish: bool = False, **kw) -> dict | None:
"""Generate the complete Daily Intelligence Briefing with quality review.
Pipeline: Gather โ†’ Research โ†’ Write โ†’ Review โ†’ Fix โ†’ Publish
All AI calls through model_registry (free models only).
Ghost is canonical. X/Telegram are syndication.
Args:
publish: If True, publish to Ghost (primary) + X/Telegram (syndication)
"""
from app.databus.model_registry import ai_call, review_content
# โ”€โ”€ PHASE 0: Gather all data โ”€โ”€
logger.info("Daily Intel: gathering data...")
data = await _gather_all_data()
context = _build_research_context(data)
if len(context) < 100:
return {"error": "Insufficient data gathered"}
# โ”€โ”€ PHASE 1: Research โ”€โ”€
logger.info("Daily Intel: research phase (free model)...")
research_notes = await ai_call(
"research",
"You are a senior crypto research analyst. Analyze data and produce structured research notes with specific numbers and names.",
f"Analyze today's crypto market data. Identify top 3 stories, sentiment drivers, risks, cultural trends, and on-chain signals:\n\n{context}",
max_tokens=1200,
temperature=0.3,
)
if not research_notes:
research_notes = "Research phase: raw data analysis (no AI available).\n\n" + context[:2000]
# โ”€โ”€ PHASE 2: Writing โ”€โ”€
logger.info("Daily Intel: writing phase (free model)...")
now = datetime.now(UTC)
date_str = now.strftime("%A, %B %d, %Y")
writing_prompt = f"""Write today's RugCharts Daily Intelligence.
Today: {date_str}
Research notes:
{research_notes}
Raw context:
{context[:2500]}
FORMAT:
# RUGCHARTS DAILY INTELLIGENCE
## {date_str}
### MARKET SNAPSHOT
2-3 sentences on overall market direction and key drivers.
### TOP STORIES
3-5 bullet points with specific numbers, names, and context.
### SENTIMENT CHECK
Market mood, social sentiment, fear/greed, what CT is saying.
### MEMES & CULTURE
What's trending on CT. Notable narratives. Cultural moments.
### RISK RADAR
Scams, hacks, regulatory actions. What to avoid today.
### BOTTOM LINE
1-2 sentence actionable takeaway.
"""
final_report = await ai_call("writing", WRITING_STANDARDS, writing_prompt, max_tokens=2000, temperature=0.7)
if not final_report or len(final_report) < 200:
headlines = data.get("news", {}).get("articles", [])
final_report = f"""# RUGCHARTS DAILY INTELLIGENCE
## {date_str}
### MARKET SNAPSHOT
{data.get("market", {}).get("brief", "Market data unavailable")}
### TOP STORIES
{chr(10).join("- " + a.get("title", "") for a in headlines[:5])}
### SENTIMENT CHECK
Fear & Greed: {data.get("fear_greed", {}).get("value", "?")}/100
### BOTTOM LINE
Stay sharp. Data-driven decisions only."""
# โ”€โ”€ PHASE 3: Review โ”€โ”€
logger.info("Daily Intel: quality review...")
review = await review_content(final_report, "daily_briefing")
if not review["pass"] and review.get("fixed_version"):
logger.info(f"Daily Intel: auto-fixed (score {review['score']}/100)")
final_report = review["fixed_version"]
else:
logger.info(f"Daily Intel: passed review ({review['score']}/100)")
report_data = {
"report": final_report,
"date": date_str,
"research_model": "free_openrouter",
"writing_model": "free_openrouter",
"review_score": review["score"],
"review_issues": review.get("issues", []),
"data_sources": sum(1 for v in data.values() if v),
"generated_at": datetime.now(UTC).isoformat(),
"published": False,
"source": "daily_intel_briefing",
}
# โ”€โ”€ PHASE 4: Publish (Ghost first, then syndicate) โ”€โ”€
if publish:
pub_results = await _publish_briefing(final_report, date_str)
report_data["published"] = True
report_data["publish_results"] = pub_results
return report_data
async def _publish_briefing(report: str, date_str: str) -> dict:
"""Publish the briefing to all channels."""
results = {}
# โ”€โ”€ X/Twitter via xurl โ”€โ”€
x_result = await _publish_to_x(report, date_str)
results["x"] = x_result
# โ”€โ”€ Ghost CMS โ”€โ”€
ghost_result = await _publish_to_ghost(report, date_str)
results["ghost"] = ghost_result
# โ”€โ”€ Telegram (via send_message or bot) โ”€โ”€
tg_result = await _publish_to_telegram(report, date_str)
results["telegram"] = tg_result
return results
async def _publish_to_x(report: str, date_str: str) -> dict:
"""Publish briefing summary to X @CryptoRugMunch via xurl."""
# Extract top story + TLDR for tweet thread
lines = report.split("\n")
headline = ""
tldr = ""
for line in lines:
if line.startswith("### MARKET SNAPSHOT") or line.startswith("##"):
continue
if not headline and len(line.strip()) > 20:
headline = line.strip().lstrip("#- ")[:240]
if "BOTTOM LINE" in line:
# Grab the next line
idx = lines.index(line)
if idx + 1 < len(lines):
tldr = lines[idx + 1].strip().lstrip("- ")[:240]
if not headline:
headline = f"RugCharts Daily Intelligence โ€” {date_str}"
tweet_text = f"๐Ÿ“Š {headline}\n\n{tldr}\n\nFull report: https://rugmunch.io/news"
try:
result = subprocess.run(
["xurl", "post", tweet_text, "--auth", "oauth2"],
capture_output=True,
text=True,
timeout=20,
)
if result.returncode == 0:
return {"status": "posted", "platform": "x", "length": len(tweet_text)}
else:
return {"status": "failed", "platform": "x", "error": result.stderr[:200]}
except Exception as e:
return {"status": "error", "platform": "x", "error": str(e)[:200]}
async def _publish_to_ghost(report: str, date_str: str) -> dict:
"""Publish briefing to Ghost CMS under 'daily' tag."""
if not GHOST_URL or not GHOST_KEY:
return {"status": "skipped", "reason": "Ghost not configured"}
try:
# Extract title from report
report.split("\n")
title = f"Daily Intelligence โ€” {date_str}"
# Convert markdown to Ghost HTML
html = _markdown_to_html(report)
async with httpx.AsyncClient(timeout=20) as c:
r = await c.post(
f"{GHOST_URL}/ghost/api/admin/posts/",
headers={
"Authorization": f"Ghost {GHOST_KEY}",
"Content-Type": "application/json",
"Accept-Version": "v5.0",
},
json={
"posts": [
{
"title": title,
"html": html,
"status": "published",
"tags": ["daily", "intelligence", "briefing"],
"feature_image": "",
}
]
},
)
if r.status_code in (200, 201):
return {"status": "published", "platform": "ghost"}
else:
return {"status": "failed", "platform": "ghost", "error": r.text[:200]}
except Exception as e:
return {"status": "error", "platform": "ghost", "error": str(e)[:200]}
async def _publish_to_telegram(report: str, date_str: str) -> dict:
"""Send briefing to Telegram channel."""
bot_token = os.getenv("TELEGRAM_BOT_TOKEN", "")
channel = os.getenv("CHANNEL_NEWS", "") or os.getenv("CHANNEL_ALERTS", "")
if not bot_token or not channel:
return {"status": "skipped", "reason": "Telegram not configured"}
# Create a shorter version for Telegram
lines = report.split("\n")
tg_text = f"๐Ÿ“Š *RugCharts Daily Intelligence*\n{date_str}\n\n"
# Extract key sections
for i, line in enumerate(lines):
if line.startswith("### "):
tg_text += f"\n*{line.strip('# ')}*\n"
elif line.startswith("- ") and len(tg_text) < 3500:
tg_text += f"{line}\n"
elif "BOTTOM LINE" in line and i + 1 < len(lines):
tg_text += f"\n๐Ÿ’ก *Bottom Line:* {next_line}\n"
break
tg_text += "\n๐Ÿ”— Full report: https://rugmunch.io/news"
try:
async with httpx.AsyncClient(timeout=15) as c:
r = await c.post(
f"https://api.telegram.org/bot{bot_token}/sendMessage",
json={
"chat_id": channel,
"text": tg_text[:4000],
"parse_mode": "Markdown",
"disable_web_page_preview": False,
},
)
if r.status_code == 200:
return {"status": "sent", "platform": "telegram"}
else:
return {"status": "failed", "platform": "telegram", "error": r.text[:200]}
except Exception as e:
return {"status": "error", "platform": "telegram", "error": str(e)[:200]}
def _markdown_to_html(md: str) -> str:
"""Simple markdown to HTML conversion for Ghost."""
html = md
html = re.sub(r"^# (.+)$", r"<h1>\1</h1>", html, flags=re.MULTILINE)
html = re.sub(r"^## (.+)$", r"<h2>\1</h2>", html, flags=re.MULTILINE)
html = re.sub(r"^### (.+)$", r"<h3>\1</h3>", html, flags=re.MULTILINE)
html = re.sub(r"^- (.+)$", r"<li>\1</li>", html, flags=re.MULTILINE)
html = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", html)
html = html.replace("\n\n", "</p><p>").replace("\n", "<br>")
html = f"<p>{html}</p>"
html = html.replace("<p><h", "<h").replace("</h2></p>", "</h2>").replace("</h1></p>", "</h1>")
return html