ranranrunforit commited on
Commit
11a847d
·
verified ·
1 Parent(s): 3d1576e

Upload 18 files

Browse files
Files changed (2) hide show
  1. app.py +29 -0
  2. emailer.py +128 -0
app.py CHANGED
@@ -17,6 +17,7 @@ import gradio as gr
17
  import pandas as pd
18
 
19
  import automation
 
20
  import finetune_data
21
  import llm_local
22
  import news_watch
@@ -211,6 +212,11 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
211
  explain_btn = gr.Button("🤖 AI summary (local LLM)", scale=1)
212
  detail_box = gr.Markdown(label="Raw read")
213
  explain_box = gr.Markdown(elem_classes=["ai-panel"])
 
 
 
 
 
214
 
215
  with gr.Tab("🔄 Sector Rotation"):
216
  gr.Markdown("Capital rotation across the 11 SPDR sector ETFs (full S&P 500 coverage). "
@@ -226,6 +232,11 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
226
  rot_5d = gr.Dataframe(label="5-Day (week trend)", interactive=False)
227
  rot_20d = gr.Dataframe(label="20-Day (month trend)", interactive=False)
228
  rot_ai = gr.Markdown(label="AI rotation narrative", elem_classes=["ai-panel"])
 
 
 
 
 
229
 
230
  with gr.Tab("📰 Watchlist News"):
231
  gr.Markdown("Daily rule: for each **holding**, only **today's** news is checked. "
@@ -238,6 +249,11 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
238
  news_btn = gr.Button("🔍 Check today's news", variant="primary", scale=1)
239
  hold_status = gr.Markdown()
240
  news_out = gr.Markdown(elem_classes=["ai-panel"])
 
 
 
 
 
241
 
242
  with gr.Tab("🧪 Auto Research"):
243
  gr.Markdown("**Multi-step research agent** (fully local): PLAN → 5 evidence tools "
@@ -251,6 +267,11 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
251
  res_btn = gr.Button("🤖 Run research agent", variant="primary", scale=1)
252
  res_progress = gr.Markdown()
253
  res_out = gr.Markdown(elem_classes=["ai-panel"])
 
 
 
 
 
254
  gr.Markdown("**Report library** (auto + manual, stored on `/data`):",
255
  elem_classes=["s2-footnote"])
256
  with gr.Row():
@@ -322,6 +343,14 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
322
  save_btn.click(ui_save_holdings, hold_in, hold_status)
323
  news_btn.click(ui_check_news, None, news_out)
324
  res_btn.click(ui_research, res_in, [res_progress, res_out, rep_pick])
 
 
 
 
 
 
 
 
325
  rep_open.click(ui_open_report, rep_pick, rep_view)
326
  auto_now.click(lambda: automation.run_pipeline(force=True), None, auto_msg)
327
  load_btn.click(ui_load_model, model_pick, model_status)
 
17
  import pandas as pd
18
 
19
  import automation
20
+ import emailer
21
  import finetune_data
22
  import llm_local
23
  import news_watch
 
212
  explain_btn = gr.Button("🤖 AI summary (local LLM)", scale=1)
213
  detail_box = gr.Markdown(label="Raw read")
214
  explain_box = gr.Markdown(elem_classes=["ai-panel"])
215
+ with gr.Row():
216
+ sig_email = gr.Textbox(label="Email this summary to", scale=4,
217
+ placeholder="name@example.com (comma-separate for several)")
218
+ sig_email_btn = gr.Button("✉ Send Email", scale=1)
219
+ sig_email_status = gr.Markdown()
220
 
221
  with gr.Tab("🔄 Sector Rotation"):
222
  gr.Markdown("Capital rotation across the 11 SPDR sector ETFs (full S&P 500 coverage). "
 
232
  rot_5d = gr.Dataframe(label="5-Day (week trend)", interactive=False)
233
  rot_20d = gr.Dataframe(label="20-Day (month trend)", interactive=False)
234
  rot_ai = gr.Markdown(label="AI rotation narrative", elem_classes=["ai-panel"])
235
+ with gr.Row():
236
+ rot_email = gr.Textbox(label="Email this narrative to", scale=4,
237
+ placeholder="name@example.com (comma-separate for several)")
238
+ rot_email_btn = gr.Button("✉ Send Email", scale=1)
239
+ rot_email_status = gr.Markdown()
240
 
241
  with gr.Tab("📰 Watchlist News"):
242
  gr.Markdown("Daily rule: for each **holding**, only **today's** news is checked. "
 
249
  news_btn = gr.Button("🔍 Check today's news", variant="primary", scale=1)
250
  hold_status = gr.Markdown()
251
  news_out = gr.Markdown(elem_classes=["ai-panel"])
252
+ with gr.Row():
253
+ news_email = gr.Textbox(label="Email this news brief to", scale=4,
254
+ placeholder="name@example.com (comma-separate for several)")
255
+ news_email_btn = gr.Button("✉ Send Email", scale=1)
256
+ news_email_status = gr.Markdown()
257
 
258
  with gr.Tab("🧪 Auto Research"):
259
  gr.Markdown("**Multi-step research agent** (fully local): PLAN → 5 evidence tools "
 
267
  res_btn = gr.Button("🤖 Run research agent", variant="primary", scale=1)
268
  res_progress = gr.Markdown()
269
  res_out = gr.Markdown(elem_classes=["ai-panel"])
270
+ with gr.Row():
271
+ res_email = gr.Textbox(label="Email this report to", scale=4,
272
+ placeholder="name@example.com (comma-separate for several)")
273
+ res_email_btn = gr.Button("✉ Send Email", scale=1)
274
+ res_email_status = gr.Markdown()
275
  gr.Markdown("**Report library** (auto + manual, stored on `/data`):",
276
  elem_classes=["s2-footnote"])
277
  with gr.Row():
 
343
  save_btn.click(ui_save_holdings, hold_in, hold_status)
344
  news_btn.click(ui_check_news, None, news_out)
345
  res_btn.click(ui_research, res_in, [res_progress, res_out, rep_pick])
346
+ sig_email_btn.click(lambda body, to: emailer.send_result(body, to, "Signals summary"),
347
+ [explain_box, sig_email], sig_email_status)
348
+ rot_email_btn.click(lambda body, to: emailer.send_result(body, to, "Sector rotation"),
349
+ [rot_ai, rot_email], rot_email_status)
350
+ news_email_btn.click(lambda body, to: emailer.send_result(body, to, "Watchlist news"),
351
+ [news_out, news_email], news_email_status)
352
+ res_email_btn.click(lambda body, to: emailer.send_result(body, to, "Research report"),
353
+ [res_out, res_email], res_email_status)
354
  rep_open.click(ui_open_report, rep_pick, rep_view)
355
  auto_now.click(lambda: automation.run_pipeline(force=True), None, auto_msg)
356
  load_btn.click(ui_load_model, model_pick, model_status)
emailer.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ emailer.py — send a tab's AI result to any address (English UI + content).
3
+
4
+ Adapted from the user's send_predict_email.py: same multi-domain SMTP
5
+ auto-detection, plain+HTML multipart, but English subjects/labels and a generic
6
+ `send_result()` the Gradio tabs call.
7
+
8
+ Sender credentials live here (a Gmail App Password). To change the sender, edit
9
+ EMAIL_SENDER / EMAIL_PASSWORD. You can also override them with the environment
10
+ variables CHAN_EMAIL_SENDER / CHAN_EMAIL_PASSWORD on the Space (recommended:
11
+ store the App Password as a Space *secret* rather than in code).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import html
17
+ import logging
18
+ import smtplib
19
+ import re
20
+ from datetime import datetime
21
+ from email.mime.text import MIMEText
22
+ from email.mime.multipart import MIMEMultipart
23
+ from email.header import Header
24
+ from email.utils import formataddr
25
+
26
+ logger = logging.getLogger("chan_emailer")
27
+
28
+ # ── sender account (override via Space secrets if you prefer) ──
29
+ EMAIL_SENDER = os.environ.get("CHAN_EMAIL_SENDER", "cz78illinoisedu@gmail.com")
30
+ EMAIL_PASSWORD = os.environ.get("CHAN_EMAIL_PASSWORD", "tknl qpmb gzye zlva")
31
+
32
+ SMTP_CONFIGS = {
33
+ "gmail.com": {"server": "smtp.gmail.com", "port": 465, "ssl": True},
34
+ "qq.com": {"server": "smtp.qq.com", "port": 465, "ssl": True},
35
+ "163.com": {"server": "smtp.163.com", "port": 465, "ssl": True},
36
+ "126.com": {"server": "smtp.126.com", "port": 465, "ssl": True},
37
+ "outlook.com": {"server": "smtp.office365.com", "port": 587, "ssl": False},
38
+ "hotmail.com": {"server": "smtp.office365.com", "port": 587, "ssl": False},
39
+ "foxmail.com": {"server": "smtp.qq.com", "port": 465, "ssl": True},
40
+ "sina.com": {"server": "smtp.sina.com", "port": 465, "ssl": True},
41
+ "aliyun.com": {"server": "smtp.aliyun.com", "port": 465, "ssl": True},
42
+ }
43
+
44
+ _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
45
+
46
+
47
+ def _sender_address(sender: str) -> str:
48
+ return formataddr((str(Header("Chan Compass", "utf-8")), sender))
49
+
50
+
51
+ def _md_to_html(text: str) -> str:
52
+ """Wrap the result (markdown-ish text) in a clean monospace HTML block."""
53
+ safe = html.escape(text)
54
+ return (
55
+ '<html><body style="margin:0;padding:16px;background:#f6f7f9;">'
56
+ '<div style="font-family:Menlo,Consolas,monospace;font-size:13px;'
57
+ 'line-height:1.55;white-space:pre-wrap;word-wrap:break-word;color:#1a1a1a;'
58
+ 'background:#ffffff;padding:18px 22px;border-radius:10px;'
59
+ 'border:1px solid #e3e6ea;max-width:760px;">'
60
+ f'{safe}'
61
+ '</div>'
62
+ '<p style="font-family:sans-serif;font-size:11px;color:#8a8a8a;'
63
+ 'margin:10px 0 0;">Sent by Chan Compass · educational tool, not '
64
+ 'investment advice.</p>'
65
+ '</body></html>'
66
+ )
67
+
68
+
69
+ def _parse_recipients(raw: str) -> list:
70
+ parts = re.split(r"[,;\s]+", (raw or "").strip())
71
+ return [p for p in parts if _EMAIL_RE.match(p)]
72
+
73
+
74
+ def _close(server):
75
+ if server is not None:
76
+ try:
77
+ server.quit()
78
+ except Exception:
79
+ try:
80
+ server.close()
81
+ except Exception:
82
+ pass
83
+
84
+
85
+ def send_result(content: str, recipients_raw: str, subject_tag: str) -> str:
86
+ """Send `content` to the address(es) in `recipients_raw`. Returns a status
87
+ string for the UI. Plain English throughout."""
88
+ if not content or not content.strip():
89
+ return "⚠️ Nothing to send yet — generate a result first."
90
+ if content.strip().startswith(("⏳", "🤖 _", "Run ", "Enter ", "Select ")):
91
+ return "⚠️ Wait for the AI result to finish, then send."
92
+ recipients = _parse_recipients(recipients_raw)
93
+ if not recipients:
94
+ return "⚠️ Enter a valid email address (e.g. name@example.com)."
95
+ if not EMAIL_SENDER or not EMAIL_PASSWORD:
96
+ return "⚠️ Sender email is not configured on the server."
97
+
98
+ subject = (f"Chan Compass · {subject_tag} · "
99
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M')}")
100
+ msg = MIMEMultipart("alternative")
101
+ msg["Subject"] = Header(subject, "utf-8")
102
+ msg["From"] = _sender_address(EMAIL_SENDER)
103
+ msg["To"] = ", ".join(recipients)
104
+ msg.attach(MIMEText(content, "plain", "utf-8"))
105
+ msg.attach(MIMEText(_md_to_html(content), "html", "utf-8"))
106
+
107
+ domain = EMAIL_SENDER.split("@")[-1].lower()
108
+ sc = SMTP_CONFIGS.get(domain, {"server": f"smtp.{domain}", "port": 465, "ssl": True})
109
+ server = None
110
+ try:
111
+ if sc["ssl"]:
112
+ server = smtplib.SMTP_SSL(sc["server"], sc["port"], timeout=30)
113
+ else:
114
+ server = smtplib.SMTP(sc["server"], sc["port"], timeout=30)
115
+ server.starttls()
116
+ server.login(EMAIL_SENDER, EMAIL_PASSWORD)
117
+ server.send_message(msg)
118
+ logger.info("email sent to %s", recipients)
119
+ return f"✅ Sent to {', '.join(recipients)}."
120
+ except smtplib.SMTPAuthenticationError:
121
+ return ("❌ Authentication failed — check the sender account / App "
122
+ "Password (Gmail needs a 16-char App Password).")
123
+ except smtplib.SMTPConnectError as e:
124
+ return f"❌ Could not connect to the mail server: {e}"
125
+ except Exception as e:
126
+ return f"❌ Send failed: {e}"
127
+ finally:
128
+ _close(server)