Spaces:
Runtime error
Runtime error
File size: 12,268 Bytes
55d0eca | 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 | """The AI-composed infographic: a page frame of ours (head, takeaway, background, mark) around a body the model writes in
HTML from a small vocabulary of components (flow charts, timelines, hierarchies, comparisons, cards, numbers). The
model's markup is never trusted: it is parsed and rebuilt from an allowlist of tags and class names, with no
attributes but `class`, so it can carry no script, style, link, image or address."""
import hashlib
import random
import re
from dataclasses import dataclass
from pathlib import Path
from bs4 import BeautifulSoup, Comment, NavigableString, Tag
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
from markupsafe import Markup
from app.infographics.render import LOGO_SVG, SIZES
_DIR = Path(__file__).parent / "templates"
CSS = (_DIR / "design.css").read_text(encoding="utf-8")
@dataclass(frozen=True)
class Look:
id: str
name: str
blurb: str
vars: dict # CSS variables of the palette
LOOKS: dict[str, Look] = {l.id: l for l in (
Look("midnight", "Midnight", "Black and blue, white type", {
"bg": "#03050a", "bg2": "#0b1a3a", "ink": "#ffffff", "mute": "#9fb0cc", "acc": "#2f7bff", "acc2": "#8db8ff",
"line": "rgba(130,170,255,.26)", "card": "rgba(255,255,255,.04)", "hl": "rgba(47,123,255,.18)", "paper": "#ffffff",
"paperink": "#03050a", "star": "#ffffff", "markbg": "rgba(3,5,10,.75)", "markline": "rgba(141,184,255,.4)", "markink": "#ffffff"}),
Look("paper", "Orbit", "White, black type, blue orbits and stars", {
"bg": "#ffffff", "bg2": "#eaf1ff", "ink": "#05070d", "mute": "#4b5668", "acc": "#1f5fe6", "acc2": "#1f5fe6",
"line": "rgba(5,7,13,.18)", "card": "#ffffff", "hl": "rgba(31,95,230,.09)", "paper": "#05070d",
"paperink": "#ffffff", "star": "#1f5fe6", "markbg": "#05070d", "markline": "#05070d", "markink": "#ffffff"}),
)}
DEFAULT_LOOK = "midnight"
# What the person can ask the layout to lean towards; "auto" lets the AI choose from the content.
STYLES = {
"auto": "Choose the layout that suits the content",
"flowchart": "A flow chart: boxes joined by arrows, steps in order",
"timeline": "A timeline: what happened or happens, in order",
"compare": "A comparison: two sides set against each other",
"hierarchy": "A hierarchy: one idea and the parts it breaks into",
"overview": "An overview: cards of the key ideas and the numbers that matter",
}
# Every class the model's markup may use (the components in design.css); anything else is dropped.
ALLOWED_CLASSES = {
"sec", "sec-t", "lead", "mono", "small", "hl", "node", "n-no", "n-t", "n-d", "flow", "col", "grid", "c2", "c3", "c4",
"card", "c-k", "c-t", "c-d", "timeline", "t-item", "t-when", "t-t", "t-d", "tree", "t-root", "t-kids", "compare",
"side", "s-h", "stats", "stat", "s-n", "s-l", "cols", "pills", "pill", "callout",
}
ALLOWED_TAGS = {"div", "section", "span", "p", "ul", "ol", "li", "strong", "b", "em", "i", "small", "br", "h3", "h4"}
DROP_WITH_CONTENT = {"script", "style", "iframe", "object", "embed", "link", "meta", "svg", "img", "video", "audio", "form",
"input", "button", "textarea", "select", "canvas", "math", "head", "title", "noscript", "template"}
MAX_BODY_CHARS = 14_000
MAX_ELEMENTS = 320
_SPACE = re.compile(r"\s+")
def _clean_text(text: str) -> str:
return text.replace("—", ",").replace("–", "-")
def sanitize_body(raw: object) -> str | None:
"""The model's body markup rebuilt from the allowlist, or None when nothing readable is left."""
if not isinstance(raw, str) or not raw.strip():
return None
soup = BeautifulSoup(raw[: MAX_BODY_CHARS * 2], "html.parser")
for c in soup.find_all(string=lambda t: isinstance(t, Comment)):
c.extract()
for tag in soup.find_all(DROP_WITH_CONTENT):
tag.decompose()
count = 0
for tag in list(soup.find_all(True)):
count += 1
if tag.name not in ALLOWED_TAGS or count > MAX_ELEMENTS:
tag.unwrap() if count <= MAX_ELEMENTS else tag.decompose()
continue
classes = [c for c in (tag.get("class") or []) if c in ALLOWED_CLASSES]
tag.attrs = {"class": classes} if classes else {}
for node in soup.find_all(string=True):
if isinstance(node, NavigableString):
node.replace_with(NavigableString(_clean_text(str(node))))
html = _SPACE.sub(" ", str(soup)).strip()
text = soup.get_text(strip=True)
if len(html) > MAX_BODY_CHARS or len(text) < 40:
return None
return html
def _clip(text: object, limit: int) -> str:
s = _SPACE.sub(" ", re.sub(r"<[^>]*>", " ", str(text or ""))).strip()
s = _clean_text(s)
if len(s) <= limit:
return s
cut = s[: limit - 1].rsplit(" ", 1)[0].rstrip(" ,;:.-") or s[: limit - 1]
return cut + "…"
def build_content(raw: dict) -> dict | None:
"""The stored content for a model answer (title, subtitle, eyebrow, key_figures, body_html, rule, notes), or None when it is
too thin to draw."""
if hasattr(raw, "model_dump"):
raw = raw.model_dump()
title = _clip(raw.get("title"), 70)
body = sanitize_body(raw.get("body_html"))
if not title or not body:
return None
figures = []
for f in raw.get("key_figures") or []:
f = f if isinstance(f, dict) else getattr(f, "__dict__", {})
value, label = _clip(f.get("value"), 14), _clip(f.get("label"), 44)
if value and label:
figures.append({"value": value, "label": label})
notes = [n for n in (_clip(n, 100) for n in raw.get("notes") or []) if n][:3]
return {"title": title, "subtitle": _clip(raw.get("subtitle"), 130), "eyebrow": _clip(raw.get("eyebrow"), 30) or "Key ideas",
"key_figures": figures[:3], "body_html": body, "rule": _clip(raw.get("rule"), 90), "notes": notes}
def _stars(seed: str, w: int, h: int, n: int = 90) -> str:
rnd = random.Random(int(hashlib.sha1(seed.encode()).hexdigest()[:8], 16))
out = []
for _ in range(n):
r = rnd.choice((1.2, 1.5, 1.5, 2, 2.6))
out.append(f'<circle cx="{rnd.randint(0, w)}" cy="{rnd.randint(0, h)}" r="{r}" fill="var(--star)" opacity="{round(rnd.uniform(.12, .5), 2)}"/>')
return "".join(out)
def _orbits(w: int, h: int) -> str:
"""The Orbit look's scenery: a ringed planet like the app's icon, orbit rings with small moons, and shooting stars."""
big = round(w * 0.2)
cx, cy = round(w * 0.86), round(h * 0.085)
pr = round(w * 0.05)
def ring(x, y, rx, ry, op, dash=""):
return (f'<ellipse cx="{x}" cy="{y}" rx="{rx}" ry="{ry}" transform="rotate(-20 {x} {y})" fill="none" stroke="var(--acc)" '
f'stroke-width="2" opacity="{op}"{dash}/>')
def planet_ring(front: bool):
sweep = 0 if front else 1
return (f'<g transform="rotate(-22 {cx} {cy})"><path d="M{cx - pr * 2} {cy} A{pr * 2} {round(pr * .6)} 0 0 {sweep} {cx + pr * 2} {cy}" '
f'fill="none" stroke="var(--acc)" stroke-width="5" opacity="{1 if front else .5}"/></g>')
out = [
'<defs><linearGradient id="shoot" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="var(--acc)" stop-opacity="0"/>'
'<stop offset="1" stop-color="var(--acc)" stop-opacity=".85"/></linearGradient></defs>',
ring(cx, cy, big, round(big * .3), .35, ' stroke-dasharray="3 12"'),
ring(cx, cy, round(big * 1.55), round(big * .46), .22),
ring(round(w * .06), round(h * .96), round(w * .5), round(w * .14), .2, ' stroke-dasharray="3 12"'),
ring(round(w * .06), round(h * .96), round(w * .34), round(w * .09), .28),
planet_ring(False),
f'<circle cx="{cx}" cy="{cy}" r="{pr}" fill="var(--bg)" stroke="var(--acc)" stroke-width="5"/>',
f'<circle cx="{cx}" cy="{cy}" r="{pr - 12}" fill="var(--acc)" opacity=".12"/>',
planet_ring(True),
f'<circle cx="{round(w * .6)}" cy="{round(h * .036)}" r="9" fill="var(--acc)" opacity=".7"/>',
f'<circle cx="{round(w * .31)}" cy="{round(h * .93)}" r="8" fill="var(--acc)" opacity=".6"/>',
]
for fx, fy, length, op in ((.46, .2, 190, .8), (.7, .4, 150, .6), (.12, .55, 120, .5)):
x, y = round(w * fx), round(h * fy)
out.append(f'<g transform="rotate(28 {x} {y})" opacity="{op}"><rect x="{x - length}" y="{y - 2}" width="{length}" height="4" rx="2" fill="url(#shoot)"/>'
f'<circle cx="{x}" cy="{y}" r="5" fill="var(--acc)"/></g>')
return "".join(out)
_ENV = Environment(loader=FileSystemLoader(_DIR), autoescape=select_autoescape(["j2", "html"], default=True), undefined=StrictUndefined,
trim_blocks=True, lstrip_blocks=True)
def render_design(look: str, content: dict, layout: str = "portrait", fit: float | None = None) -> str:
"""The page for an AI-composed infographic. `fit` is the scale that makes its body fit (stored with the content)."""
look = look if look in LOOKS else DEFAULT_LOOK
layout = layout if layout in SIZES else "portrait"
w, h = SIZES[layout]
if fit is None:
fit = float(((content.get("fit") or {}) if isinstance(content.get("fit"), dict) else {}).get(layout) or 1)
fit = max(0.5, min(1.0, fit))
vars_css = ";".join(f"--{k}:{v}" for k, v in LOOKS[look].vars.items())
return _ENV.get_template("design.html.j2").render(
c=content, look=look, layout=layout, w=w, h=h, fit=fit, vars_css=vars_css, css=Markup(CSS), logo=Markup(LOGO_SVG),
body=Markup(content["body_html"]),
stars=Markup(_stars(content["title"] + look, w, h) + (_orbits(w, h) if look == "paper" else "")),
fonts="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap")
SAMPLE_BODY = (
'<section class="sec"><div class="sec-t">The five layers</div><div class="flow">'
'<div class="node"><span class="n-no">01</span><span class="n-t">Prompt</span><span class="n-d">Wording, examples and format of one request.</span></div>'
'<div class="node"><span class="n-no">02</span><span class="n-t">Context</span><span class="n-d">Docs, history, tools and memory for each call.</span></div>'
'<div class="node hl"><span class="n-no">03</span><span class="n-t">Harness</span><span class="n-d">Tools, checks and guardrails around the model.</span></div>'
'<div class="node"><span class="n-no">04</span><span class="n-t">Loop</span><span class="n-d">Reason, act and observe until done.</span></div></div></section>'
'<section class="sec"><div class="sec-t">Every request goes through</div><div class="grid c4">'
+ "".join(f'<div class="card{" hl" if i == 3 else ""}"><span class="c-k">Step {i + 1}</span><span class="c-t">{t}</span><span class="c-d">{d}</span></div>'
for i, (t, d) in enumerate([("Auth", "Identity, limits and input guards."), ("Cache", "Answers to repeat questions."),
("Safety", "Scrub PII, catch injection."), ("Memory", "Session history, summarised."),
("Retrieve", "Fetch docs, split queries."), ("Answer", "Sub-queries in parallel."),
("Validate", "Check quality first."), ("Save", "Store and log one trace.")]))
+ '</div></section>'
'<section class="sec"><div class="sec-t">Why it matters</div><div class="callout">Each layer builds on the one below it, so start at the bottom and add a layer only when the task needs it.</div></section>'
)
SAMPLE = {"title": "From Prompt to Production Agent", "subtitle": "Five engineering layers and the steps every request takes",
"eyebrow": "Agent engineering", "body_html": SAMPLE_BODY,
"key_figures": [{"value": "5", "label": "engineering layers"}, {"value": "8", "label": "steps on every request"},
{"value": "30%", "label": "of traffic skips the pipeline"}],
"rule": "Most tasks only need the bottom layers; avoid complex graphs without cause.",
"notes": ["Each layer builds on the one below it.", "Cache and middleware run outside the graph.",
"Every request produces one end-to-end trace."]}
|