import os import re import openai import pandas as pd import traceback from collections import Counter, defaultdict from apscheduler.schedulers.background import BackgroundScheduler import gradio as gr from utils import ( SECTOR, DYN, save_dynamic, fetch_news, keyword_hit, validate_ticker, send_tg, ist_now ) # ── initialize LLM client ───────────────────────────────────────────────── openai.api_base = "https://api.groq.com/openai/v1" openai.api_key = os.getenv("GROQ_API_KEY") # ── helper: ask LLM for rationale ────────────────────────────────────────── def llm_reason(theme, tickers, headlines): prompt = f""" Theme: {theme} Sample news: {' | '.join(headlines[:4])} Write TWO crisp bullets (<60 words total) explaining why {', '.join(tickers)} benefit now. """ rsp = openai.ChatCompletion.create( model="llama3-70b-8192", messages=[{"role":"user","content":prompt}], temperature=0.35, max_tokens=130 ) return rsp.choices[0].message.content.strip() # ── helper: ask LLM for new tickers ──────────────────────────────────────── def suggest_tickers(theme): ask = openai.ChatCompletion.create( model="llama3-8b-8192", messages=[{"role":"user","content": f"List up to 5 NSE tickers that will benefit from '{theme}'. Return comma-separated tickers only." }], temperature=0.4, max_tokens=20 ) raw = re.split(r"[,\s]+", ask.choices[0].message.content.strip()) return [validate_ticker(t.upper()) for t in raw] # ── core logic (may raise) ──────────────────────────────────────────────── def _daily_job(): news = fetch_news() freq = Counter() hits = defaultdict(list) # count keyword hits per theme for art in news: text = art["title"] + " " + art.get("description", "") for theme, cfg in SECTOR.items(): if keyword_hit(text, cfg["keywords"]): freq[theme] += 1 hits[theme].append(art["title"]) rows, cards = [], [] if not freq: return pd.DataFrame(rows, columns=["theme","hits","stocks","reason"]) for theme, count in freq.most_common(): base = SECTOR[theme]["tickers"] extra = DYN.get(theme, []) new = [t for t in suggest_tickers(theme) if t and t not in base+extra][:3] if new: DYN.setdefault(theme, []).extend(new) save_dynamic(DYN) tickers = list(dict.fromkeys(base + extra + new))[:6] reason = llm_reason(theme, tickers, hits[theme]) cards.append( f"*{theme}* ({count} hits)\n" f"Stocks: {', '.join('`'+t[:-3]+'`' for t in tickers)}\n" f"{reason}" ) rows.append({ "theme": theme, "hits": count, "stocks": ', '.join(tickers), "reason": reason }) header = f"📈 *Macro Theme Brief* {ist_now().strftime('%d %b %Y %H:%M')}" send_tg(header + "\n\n" + "\n\n".join(cards)) return pd.DataFrame(rows) # ── safe wrapper (catches & displays errors) ────────────────────────────── def daily_job_safe(): try: return _daily_job() except Exception: tb = traceback.format_exc() print("Error in daily_job:", tb) return "```python\n" + tb + "\n```" # ── schedule to run at 08:00 IST (02:30 UTC) ─────────────────────────────── sched = BackgroundScheduler(timezone="UTC") sched.add_job(daily_job_safe, trigger="cron", hour=2, minute=30) sched.start() # ── Gradio UI ───────────────────────────────────────────────────────────── if __name__ == "__main__": port = int(os.environ.get("PORT", 7860)) with gr.Blocks() as demo: gr.Markdown("### 🇮🇳 Macro‑Theme Bot (Elec MFG | Semicon | H₂ | Ethanol | RE)") out = gr.Textbox(lines=20, label="Output / Error") gr.Button("Run now").click(fn=daily_job_safe, outputs=out) demo.launch( server_name="0.0.0.0", server_port=port, ssr_mode=False )