ranranrunforit commited on
Commit
1ac3e1d
·
verified ·
1 Parent(s): aacf484

Upload 18 files

Browse files
Files changed (3) hide show
  1. app.py +24 -10
  2. emailer.py +71 -13
  3. 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
- yield progress, report, gr.update(choices=research_agent.list_reports())
 
 
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
- """Runs once, automatically, the moment every sub-agent is loaded shows a
163
- self-test verdict without the user clicking anything."""
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
- out = llm_local.quick_test()
171
- ok = "not loaded" not in out and "error" not in out.lower()
172
- _SELFTEST["done"] = True
173
- _SELFTEST["result"] = (("✅ **Every agent is OK now** — self-test passed:\n\n" + out)
174
- if ok else ("⚠️ Self-test finished with issues:\n\n" + out))
175
- return _SELFTEST["result"]
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(lambda: llm_local.quick_test(), None, model_test_out)
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
- """Wrap the result (markdown-ish text) in a clean monospace HTML block."""
65
- safe = html.escape(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return (
67
- '<html><body style="margin:0;padding:16px;background:#f6f7f9;">'
68
- '<div style="font-family:Menlo,Consolas,monospace;font-size:13px;'
69
- 'line-height:1.55;white-space:pre-wrap;word-wrap:break-word;color:#1a1a1a;'
70
- 'background:#ffffff;padding:18px 22px;border-radius:10px;'
71
- 'border:1px solid #e3e6ea;max-width:760px;">'
72
- f'{safe}'
73
- '</div>'
74
- '<p style="font-family:sans-serif;font-size:11px;color:#8a8a8a;'
75
- 'margin:10px 0 0;">Sent by Chan Compass · educational tool, not '
76
- 'investment advice.</p>'
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
- return sorted(fs, reverse=True)
 
 
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