Spaces:
Running
Running
File size: 8,837 Bytes
69e310f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """Render a verified brief to Markdown / HTML.
Claim references are substituted here and nowhere else, so the value a reader
sees is the value the verifier checked. A reference with no matching claim
renders as an explicit ``[unverified]`` marker rather than silently vanishing.
"""
from __future__ import annotations
import html
import re
from collections.abc import Mapping
from app.models.brief import CLAIM_REF_PATTERN, Brief, NumericClaim
UNVERIFIED_MARKER = "[unverified]"
def format_claim(claim: NumericClaim) -> str:
"""Human-facing rendering of one verified figure."""
value = claim.value
if claim.unit == "usd":
return f"${value:,.2f}"
if claim.unit == "percent":
return f"{value:+.2f}%"
if claim.unit == "ratio":
return f"{value:,.2f}"
if claim.unit == "score":
return f"{value:+.2f}"
return f"{value}"
def substitute(text: str, claims: Mapping[str, NumericClaim]) -> str:
"""Replace every ``{{cN}}`` with its verified, formatted value."""
def _replace(match: re.Match[str]) -> str:
claim = claims.get(match.group(1))
return format_claim(claim) if claim else UNVERIFIED_MARKER
return CLAIM_REF_PATTERN.sub(_replace, text or "")
def _cell(claim_id: str | None, claims: Mapping[str, NumericClaim]) -> str:
if not claim_id:
return "β"
claim = claims.get(claim_id)
return format_claim(claim) if claim else UNVERIFIED_MARKER
def render_markdown(brief: Brief) -> str:
"""The brief as Markdown β used for the email body and the archive."""
claims = brief.claims_by_id()
out: list[str] = []
out.append(f"# AlphaBrief β {brief.generated_for}")
out.append("")
out.append(f"**{substitute(brief.headline, claims)}**")
out.append("")
if brief.partial:
out.append("> β οΈ **Partial coverage** β at least one ticker failed to return data.")
out.append("")
out.append(substitute(brief.executive_summary, claims))
out.append("")
if brief.snapshot:
out.append("## Snapshot")
out.append("")
out.append(
"| Ticker | Company | Close | 1D | 30D | Vol (ann.) | Max DD | P/E | Sentiment |"
)
out.append("| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |")
for row in brief.snapshot:
out.append(
"| "
+ " | ".join(
[
row.ticker,
row.company or "β",
_cell(row.last_close, claims),
_cell(row.change_1d, claims),
_cell(row.return_30d, claims),
_cell(row.volatility, claims),
_cell(row.max_drawdown, claims),
_cell(row.pe_ratio, claims),
_cell(row.sentiment, claims),
]
)
+ " |"
)
out.append("")
if brief.key_moves:
out.append("## Key moves")
out.append("")
for move in brief.key_moves:
out.append(f"- **{move.ticker}** β {substitute(move.narrative, claims)}")
out.append("")
if brief.news_and_sentiment:
out.append("## News & sentiment")
out.append("")
for entry in brief.news_and_sentiment:
out.append(f"- **{entry.ticker}** β {substitute(entry.summary, claims)}")
if entry.top_headline:
source = f" _({entry.headline_source})_" if entry.headline_source else ""
out.append(f" - Top headline: β{entry.top_headline}β{source}")
out.append("")
if brief.risk_flags:
out.append("## Risk flags")
out.append("")
for flag in brief.risk_flags:
out.append(
f"- **{flag.ticker}** Β· _{flag.category}_ β {substitute(flag.assessment, claims)}"
)
out.append(f" - Evidence: β{flag.evidence}β")
out.append("")
if brief.watch_items:
out.append("## Watch items")
out.append("")
for item in brief.watch_items:
prefix = f"**{item.ticker}** β " if item.ticker else ""
out.append(f"- {prefix}{substitute(item.item, claims)}")
out.append("")
if brief.data_gaps:
out.append("## Data gaps")
out.append("")
for gap in brief.data_gaps:
out.append(f"- {gap}")
out.append("")
out.append("---")
out.append(
"_Every figure above was computed by the MCP tool layer and independently "
"recomputed by a deterministic verifier before this brief was released. "
"Delivery required human approval._"
)
return "\n".join(out)
def render_html(brief: Brief) -> str:
"""A self-contained dark-theme HTML email body."""
claims = brief.claims_by_id()
esc = html.escape
rows = "".join(
"<tr>"
+ "".join(
f"<td>{esc(value)}</td>"
for value in [
row.ticker,
row.company or "β",
_cell(row.last_close, claims),
_cell(row.change_1d, claims),
_cell(row.return_30d, claims),
_cell(row.volatility, claims),
_cell(row.max_drawdown, claims),
_cell(row.pe_ratio, claims),
_cell(row.sentiment, claims),
]
)
+ "</tr>"
for row in brief.snapshot
)
moves = "".join(
f"<li><strong>{esc(m.ticker)}</strong> β {esc(substitute(m.narrative, claims))}</li>"
for m in brief.key_moves
)
news = "".join(
f"<li><strong>{esc(n.ticker)}</strong> β {esc(substitute(n.summary, claims))}"
+ (f"<br><em>β{esc(n.top_headline)}β</em>" if n.top_headline else "")
+ "</li>"
for n in brief.news_and_sentiment
)
risks = "".join(
f"<li><strong>{esc(r.ticker)}</strong> Β· {esc(r.category)} β "
f"{esc(substitute(r.assessment, claims))}<br><em>β{esc(r.evidence)}β</em></li>"
for r in brief.risk_flags
)
watch = "".join(
f"<li>{esc((w.ticker + ' β ') if w.ticker else '')}{esc(substitute(w.item, claims))}</li>"
for w in brief.watch_items
)
gaps = "".join(f"<li>{esc(g)}</li>" for g in brief.data_gaps)
partial_banner = (
'<p style="background:#3b1d1d;border-left:3px solid #f87171;padding:10px 14px;'
'border-radius:6px;">β οΈ <strong>Partial coverage</strong> β at least one ticker '
"failed to return data.</p>"
if brief.partial
else ""
)
return f"""<!doctype html>
<html><body style="margin:0;padding:24px;background:#0a0e1a;color:#e2e8f0;
font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;">
<div style="max-width:760px;margin:0 auto;">
<p style="color:#22d3ee;letter-spacing:.18em;font-size:12px;margin:0 0 4px;">ALPHABRIEF</p>
<h1 style="margin:0 0 4px;font-size:24px;">{esc(substitute(brief.headline, claims))}</h1>
<p style="color:#94a3b8;margin:0 0 20px;font-size:13px;">{esc(brief.generated_for)}</p>
{partial_banner}
<p style="line-height:1.65;">{esc(substitute(brief.executive_summary, claims))}</p>
<h2 style="color:#22d3ee;font-size:15px;margin-top:28px;">Snapshot</h2>
<table cellspacing="0" cellpadding="8" style="width:100%;border-collapse:collapse;font-size:13px;">
<thead><tr style="color:#94a3b8;text-align:left;">
<th>Ticker</th><th>Company</th><th>Close</th><th>1D</th><th>30D</th>
<th>Vol</th><th>Max DD</th><th>P/E</th><th>Sent.</th>
</tr></thead>
<tbody>{rows}</tbody>
</table>
{f'<h2 style="color:#22d3ee;font-size:15px;margin-top:28px;">Key moves</h2><ul style="line-height:1.7;">{moves}</ul>' if moves else ""}
{f'<h2 style="color:#22d3ee;font-size:15px;margin-top:28px;">News & sentiment</h2><ul style="line-height:1.7;">{news}</ul>' if news else ""}
{f'<h2 style="color:#f87171;font-size:15px;margin-top:28px;">Risk flags</h2><ul style="line-height:1.7;">{risks}</ul>' if risks else ""}
{f'<h2 style="color:#22d3ee;font-size:15px;margin-top:28px;">Watch items</h2><ul style="line-height:1.7;">{watch}</ul>' if watch else ""}
{f'<h2 style="color:#94a3b8;font-size:15px;margin-top:28px;">Data gaps</h2><ul style="line-height:1.7;color:#94a3b8;">{gaps}</ul>' if gaps else ""}
<hr style="border:none;border-top:1px solid #1e293b;margin:28px 0 12px;">
<p style="color:#64748b;font-size:12px;line-height:1.6;">
Every figure above was computed by the MCP tool layer and independently recomputed by a
deterministic verifier before release. Delivery required human approval.
</p>
</div>
</body></html>"""
|