Spaces:
Runtime error
Runtime error
Upload 18 files
Browse files- app.py +24 -10
- emailer.py +71 -13
- research_agent.py +3 -1
app.py
CHANGED
|
@@ -128,9 +128,12 @@ def ui_check_news():
|
|
| 128 |
|
| 129 |
|
| 130 |
def ui_research(ticker):
|
|
|
|
| 131 |
for progress, report in research_agent.run_research_stream(ticker):
|
| 132 |
yield progress, report, gr.update()
|
| 133 |
-
|
|
|
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
def ui_open_report(fname):
|
|
@@ -158,21 +161,32 @@ def _export_dataset():
|
|
| 158 |
return msg, gr.update(value=path, visible=True)
|
| 159 |
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
def _auto_selftest():
|
| 162 |
-
"""
|
| 163 |
-
|
| 164 |
if _SELFTEST["done"]:
|
| 165 |
return _SELFTEST["result"]
|
| 166 |
workers = llm_local.WORKERS
|
| 167 |
if not all(w["llm"] is not None for w in workers.values()):
|
| 168 |
ready = sum(1 for w in workers.values() if w["llm"] is not None)
|
| 169 |
return f"⏳ Loading sub-agents… {ready}/{len(workers)} ready (self-test will run automatically)."
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
return
|
| 176 |
|
| 177 |
|
| 178 |
# ─────────────────────────────────────────── layout ──
|
|
@@ -354,7 +368,7 @@ with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
|
|
| 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)
|
| 357 |
-
test_btn.click(
|
| 358 |
|
| 359 |
automation.start_scheduler()
|
| 360 |
|
|
|
|
| 128 |
|
| 129 |
|
| 130 |
def ui_research(ticker):
|
| 131 |
+
progress, report = "", ""
|
| 132 |
for progress, report in research_agent.run_research_stream(ticker):
|
| 133 |
yield progress, report, gr.update()
|
| 134 |
+
reports = research_agent.list_reports()
|
| 135 |
+
newest = reports[0] if reports else None
|
| 136 |
+
yield progress, report, gr.update(choices=reports, value=newest)
|
| 137 |
|
| 138 |
|
| 139 |
def ui_open_report(fname):
|
|
|
|
| 161 |
return msg, gr.update(value=path, visible=True)
|
| 162 |
|
| 163 |
|
| 164 |
+
def _run_selftest():
|
| 165 |
+
"""Run the self-test now (used by both the auto-timer and the manual button)
|
| 166 |
+
and cache the verdict so the two never fight over the output box."""
|
| 167 |
+
out = llm_local.quick_test()
|
| 168 |
+
ok = "not loaded" not in out and "error" not in out.lower()
|
| 169 |
+
_SELFTEST["done"] = True
|
| 170 |
+
_SELFTEST["result"] = (("✅ **Every agent is OK now** — self-test passed:\n\n" + out)
|
| 171 |
+
if ok else ("⚠️ Self-test finished with issues:\n\n" + out))
|
| 172 |
+
return _SELFTEST["result"]
|
| 173 |
+
|
| 174 |
+
|
| 175 |
def _auto_selftest():
|
| 176 |
+
"""Auto-runs ONCE the moment every sub-agent is loaded. After that it returns
|
| 177 |
+
the cached verdict unchanged, so it never overwrites a manual test result."""
|
| 178 |
if _SELFTEST["done"]:
|
| 179 |
return _SELFTEST["result"]
|
| 180 |
workers = llm_local.WORKERS
|
| 181 |
if not all(w["llm"] is not None for w in workers.values()):
|
| 182 |
ready = sum(1 for w in workers.values() if w["llm"] is not None)
|
| 183 |
return f"⏳ Loading sub-agents… {ready}/{len(workers)} ready (self-test will run automatically)."
|
| 184 |
+
return _run_selftest()
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _manual_selftest():
|
| 188 |
+
"""Manual button: always re-runs and shows the fresh verdict."""
|
| 189 |
+
return _run_selftest()
|
| 190 |
|
| 191 |
|
| 192 |
# ─────────────────────────────────────────── layout ──
|
|
|
|
| 368 |
rep_open.click(ui_open_report, rep_pick, rep_view)
|
| 369 |
auto_now.click(lambda: automation.run_pipeline(force=True), None, auto_msg)
|
| 370 |
load_btn.click(ui_load_model, model_pick, model_status)
|
| 371 |
+
test_btn.click(_manual_selftest, None, model_test_out)
|
| 372 |
|
| 373 |
automation.start_scheduler()
|
| 374 |
|
emailer.py
CHANGED
|
@@ -60,21 +60,79 @@ def _sender_address(sender: str) -> str:
|
|
| 60 |
return formataddr((str(Header("Chan Compass", "utf-8")), sender))
|
| 61 |
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
def _md_to_html(text: str) -> str:
|
| 64 |
-
"""
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
return (
|
| 67 |
-
'<html><body style="margin:0;padding:
|
| 68 |
-
'<div style="font-family:
|
| 69 |
-
'line-height:1.55;
|
| 70 |
-
'
|
| 71 |
-
'
|
| 72 |
-
f'{
|
| 73 |
-
'<
|
| 74 |
-
'
|
| 75 |
-
'
|
| 76 |
-
'
|
| 77 |
-
'</body></html>'
|
| 78 |
)
|
| 79 |
|
| 80 |
|
|
|
|
| 60 |
return formataddr((str(Header("Chan Compass", "utf-8")), sender))
|
| 61 |
|
| 62 |
|
| 63 |
+
def _inline_md(s: str) -> str:
|
| 64 |
+
s = html.escape(s)
|
| 65 |
+
s = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s)
|
| 66 |
+
s = re.sub(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"<em>\1</em>", s)
|
| 67 |
+
s = re.sub(r"`(.+?)`", r"<code>\1</code>", s)
|
| 68 |
+
s = re.sub(r"\[(.+?)\]\((https?://[^\s)]+)\)",
|
| 69 |
+
r'<a href="\2">\1</a>', s)
|
| 70 |
+
return s
|
| 71 |
+
|
| 72 |
+
|
| 73 |
def _md_to_html(text: str) -> str:
|
| 74 |
+
"""Minimal but correct Markdown → HTML (headings, tables, lists, bold,
|
| 75 |
+
code, links). The reports are Markdown, so emailing them as preformatted
|
| 76 |
+
text showed raw `#`/`|` symbols — this renders them properly."""
|
| 77 |
+
lines = text.split("\n")
|
| 78 |
+
out, i, n = [], 0, len(lines)
|
| 79 |
+
while i < n:
|
| 80 |
+
line = lines[i]
|
| 81 |
+
# table block: a header row of pipes followed by a |---| separator
|
| 82 |
+
if "|" in line and i + 1 < n and re.match(r"^\s*\|?[\s:\-|]+\|?\s*$", lines[i + 1]):
|
| 83 |
+
header = [c.strip() for c in line.strip().strip("|").split("|")]
|
| 84 |
+
i += 2
|
| 85 |
+
rows = []
|
| 86 |
+
while i < n and "|" in lines[i]:
|
| 87 |
+
rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")])
|
| 88 |
+
i += 1
|
| 89 |
+
th = "".join(f"<th style='text-align:left;padding:6px 10px;"
|
| 90 |
+
f"border-bottom:2px solid #ddd'>{_inline_md(c)}</th>" for c in header)
|
| 91 |
+
trs = ""
|
| 92 |
+
for r in rows:
|
| 93 |
+
tds = "".join(f"<td style='padding:6px 10px;border-bottom:1px solid #eee'>"
|
| 94 |
+
f"{_inline_md(c)}</td>" for c in r)
|
| 95 |
+
trs += f"<tr>{tds}</tr>"
|
| 96 |
+
out.append(f"<table style='border-collapse:collapse;margin:10px 0;"
|
| 97 |
+
f"font-size:13px'><thead><tr>{th}</tr></thead><tbody>{trs}</tbody></table>")
|
| 98 |
+
continue
|
| 99 |
+
m = re.match(r"^(#{1,4})\s+(.*)$", line)
|
| 100 |
+
if m:
|
| 101 |
+
lvl = len(m.group(1))
|
| 102 |
+
size = {1: 20, 2: 16, 3: 14, 4: 13}[lvl]
|
| 103 |
+
out.append(f"<h{lvl} style='font-size:{size}px;margin:14px 0 6px;"
|
| 104 |
+
f"color:#0265DC'>{_inline_md(m.group(2))}</h{lvl}>")
|
| 105 |
+
i += 1
|
| 106 |
+
continue
|
| 107 |
+
if re.match(r"^\s*[-*]\s+", line):
|
| 108 |
+
items = []
|
| 109 |
+
while i < n and re.match(r"^\s*[-*]\s+", lines[i]):
|
| 110 |
+
items.append(f"<li>{_inline_md(re.sub(r'^\s*[-*]\s+', '', lines[i]))}</li>")
|
| 111 |
+
i += 1
|
| 112 |
+
out.append(f"<ul style='margin:6px 0 6px 18px'>{''.join(items)}</ul>")
|
| 113 |
+
continue
|
| 114 |
+
if line.strip() in ("---", "***", "___"):
|
| 115 |
+
out.append("<hr style='border:none;border-top:1px solid #e3e6ea;margin:12px 0'>")
|
| 116 |
+
i += 1
|
| 117 |
+
continue
|
| 118 |
+
if line.strip() == "":
|
| 119 |
+
out.append("<div style='height:6px'></div>")
|
| 120 |
+
i += 1
|
| 121 |
+
continue
|
| 122 |
+
out.append(f"<p style='margin:4px 0'>{_inline_md(line)}</p>")
|
| 123 |
+
i += 1
|
| 124 |
+
body = "".join(out)
|
| 125 |
return (
|
| 126 |
+
'<html><body style="margin:0;padding:18px;background:#f6f7f9;">'
|
| 127 |
+
'<div style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;'
|
| 128 |
+
'font-size:14px;line-height:1.55;color:#1a1a1a;background:#ffffff;'
|
| 129 |
+
'padding:22px 26px;border-radius:12px;border:1px solid #e3e6ea;'
|
| 130 |
+
'max-width:780px;margin:0 auto;">'
|
| 131 |
+
f'{body}'
|
| 132 |
+
'<p style="font-size:11px;color:#8a8a8a;margin:16px 0 0;border-top:'
|
| 133 |
+
'1px solid #eee;padding-top:10px;">Sent by Chan Compass · educational '
|
| 134 |
+
'tool, not investment advice.</p>'
|
| 135 |
+
'</div></body></html>'
|
|
|
|
| 136 |
)
|
| 137 |
|
| 138 |
|
research_agent.py
CHANGED
|
@@ -310,7 +310,9 @@ def run_research(ticker: str, auto: bool = False) -> tuple:
|
|
| 310 |
def list_reports() -> list:
|
| 311 |
try:
|
| 312 |
fs = [f for f in os.listdir(paths.REPORTS_DIR) if f.endswith(".md")]
|
| 313 |
-
|
|
|
|
|
|
|
| 314 |
except OSError:
|
| 315 |
return []
|
| 316 |
|
|
|
|
| 310 |
def list_reports() -> list:
|
| 311 |
try:
|
| 312 |
fs = [f for f in os.listdir(paths.REPORTS_DIR) if f.endswith(".md")]
|
| 313 |
+
fs.sort(key=lambda f: os.path.getmtime(os.path.join(paths.REPORTS_DIR, f)),
|
| 314 |
+
reverse=True)
|
| 315 |
+
return fs
|
| 316 |
except OSError:
|
| 317 |
return []
|
| 318 |
|