From ff97a693b33151baf85cf4a1fef8fcdb424a6056 Mon Sep 17 00:00:00 2001 From: Max Stepanov Date: Fri, 24 Apr 2026 15:11:15 +0300 Subject: [PATCH 01/11] update prompt for html generation --- anton/core/llm/prompt_builder.py | 29 ++- anton/core/llm/prompts.py | 423 ++++++++++++++++++++++--------- anton/core/tools/recall_skill.py | 18 ++ 3 files changed, 349 insertions(+), 121 deletions(-) diff --git a/anton/core/llm/prompt_builder.py b/anton/core/llm/prompt_builder.py index d7340fe6..734bcc55 100644 --- a/anton/core/llm/prompt_builder.py +++ b/anton/core/llm/prompt_builder.py @@ -61,6 +61,18 @@ def _build_tool_prompts_section(self, tool_defs: list["ToolDef"] | None) -> str: return "\n\n".join(chunks) + # Built-in skills that are always available regardless of ~/.anton/skills/. + # Each entry must have "label" and "when_to_use". + _BUILTIN_SKILL_SUMMARIES: list[dict] = [ + { + "label": "generate_dashboard_html", + "when_to_use": ( + "when the user asks to build a dashboard, chart, report, " + "presentation, or any data visualization as a standalone HTML page" + ), + }, + ] + def _build_procedural_memory_section( self, skill_store: "SkillStore | None" ) -> str: @@ -71,12 +83,17 @@ def _build_procedural_memory_section( the full procedure. Returns an empty string if no store is wired or no skills are saved — the caller skips the section entirely. """ - if skill_store is None: - return "" - try: - summaries = skill_store.list_summaries() - except Exception: - return "" + summaries: list[dict] = list(self._BUILTIN_SKILL_SUMMARIES) + if skill_store is not None: + try: + user_summaries = skill_store.list_summaries() + except Exception: + user_summaries = [] + # User skills override builtins with the same label. + builtin_labels = {s["label"] for s in summaries} + for s in user_summaries: + if s.get("label") not in builtin_labels: + summaries.append(s) if not summaries: return "" diff --git a/anton/core/llm/prompts.py b/anton/core/llm/prompts.py index 73f57da6..3de22211 100644 --- a/anton/core/llm/prompts.py +++ b/anton/core/llm/prompts.py @@ -236,124 +236,317 @@ VISUALIZATIONS_HTML_OUTPUT_FORMAT_PROMPT = """\ -LIST THE INSIGHTS (terse — one line each, not an essay): -Before coding, list the insights you want to present/convey/highlight as `1 - : ..` -Example: `1 - Line chart of weekly signups: shows growth inflection after the March launch, flags whether momentum is sustained.` -This is a checklist, not a brief — no narrative prose, no design discussion. - -BUILD THE DASHBOARD — use multiple scratchpad cells, but produce ONE single self-contained HTML file: - - CRITICAL: The final dashboard MUST be a single .html file with ALL data, CSS, and JS inlined. \ -Do NOT reference external local files (like data.js) — browsers block local file:// cross-references \ -for security reasons and the dashboard will silently fail to load data. - - SECURITY (critical): Dashboards may be published to the web. NEVER embed API keys, tokens, \ -passwords, connection strings, or any credentials in the HTML, JS, or inline data. Fetch data \ -in scratchpad cells using credentials from environment variables, then serialize only the \ -resulting data into the dashboard. If the user explicitly asks to embed a credential \ -(e.g. for a live-updating dashboard), warn them that publishing will expose it and get \ -confirmation before proceeding. - - Build the parts in separate cells, then assemble at the end: - - CELL 1 — Serialize data to a JS string variable (programmatic, no HTML): - Serialize all computed data (dataframes, metrics, KPIs) into a Python string. Build a \ -Python dict with keys like "kpis", "tables", "charts" — each containing the relevant data. \ -Convert DataFrames with df.to_dict(orient='records'). Use json.dumps(data, default=str) to \ -handle dates, Decimal, numpy types. Store as a Python variable: \ -`data_js = 'const D = ' + json_string + ';'` — do NOT write to a separate file. - - CELL 2 — Build CSS + HTML structure as a Python string variable: - Write the HTML head (styles, CDN script tags) and body structure (header, KPIs, chart divs, \ -tabs, tables) as a Python string variable `html_body`. This cell builds the template. - - CELL 3+ — Build JS chart rendering logic as Python string variables: - Write the JavaScript that initializes charts, populates tables, handles tabs, etc. \ -Split across multiple cells if needed to avoid token limits. Store as `js_charts` etc. - - FINAL CELL — Assemble and write the HTML file: - Combine: `html = html_body.replace('', f'')` \ -or similar. - - SELF-CONTAINED OUTPUT (critical): - Prefer inlining everything — CSS in ` + + + + + + + + + + + From bf2a477796fb1539d973f08bd5875a72ac86c993 Mon Sep 17 00:00:00 2001 From: Max Stepanov Date: Fri, 24 Apr 2026 15:54:49 +0300 Subject: [PATCH 03/11] erase_scratchpad_history tool --- anton/core/llm/prompts.py | 17 ++++++++++++++++- anton/core/session.py | 6 ++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/anton/core/llm/prompts.py b/anton/core/llm/prompts.py index 3de22211..83f28a7c 100644 --- a/anton/core/llm/prompts.py +++ b/anton/core/llm/prompts.py @@ -500,6 +500,11 @@ def __read_html_template(): shared state — other functions rely on the original. - Keep each cell small. If a single block would push the cell past a few hundred lines, split it into smaller helper blocks in separate cells. +- **Every cell in the dashboard build must start with a `# DELETABLE: ` comment on the very first line** (e.g. `# DELETABLE: build header + HTML`). Once the final file is on disk, the intermediate code is no longer + useful in context; the marker is what lets `erase_scratchpad_history` clear + these cells afterwards. The comment has no effect on execution. Cell layout — one function per marker in the template, plus one function per block from DashSpec: @@ -521,6 +526,7 @@ def __read_html_template(): The final cell looks roughly like this: ```python +# DELETABLE: assemble template and write final HTML file def __get_html(): template = __read_html_template() content = ( @@ -546,7 +552,16 @@ def __get_html(): Splitting the work across cells catches syntax errors early (a broken string in one block fails fast), keeps each cell well under the scratchpad's 120-second -timeout, and makes the final assembly trivially reviewable.\ +timeout, and makes the final assembly trivially reviewable. + +### Step 4 — Reclaim context +After the final `__get_html()` cell has returned the path, call the +`erase_scratchpad_history` tool (no arguments). It walks every cell you +marked `# DELETABLE: ` and replaces its code with `# DELETED: ` +in both the live scratchpad and the message history — the dashboard HTML on +disk is the artifact, so the intermediate code no longer needs to occupy +context. Skipping this step is not an error, but it bloats later turns with +code the user can reread from the file any time.\ """ diff --git a/anton/core/session.py b/anton/core/session.py index 25720bb7..8efc81ee 100644 --- a/anton/core/session.py +++ b/anton/core/session.py @@ -13,6 +13,7 @@ from anton.core.memory.cerebellum import Cerebellum from anton.core.memory.skills import SkillStore from anton.core.tools.recall_skill import RECALL_SKILL_TOOL +from anton.core.tools.erase_scratchpad_history import ERASE_SCRATCHPAD_HISTORY_TOOL from anton.core.llm.prompts import RESILIENCE_NUDGE from anton.core.llm.provider import ( ContextOverflowError, @@ -405,6 +406,11 @@ def _build_core_tools(self) -> None: # Procedural memory retrieval — always available, no-op if no skills. self.tool_registry.register_tool(RECALL_SKILL_TOOL) + # Context compaction for file-artifact workflows: the agent marks + # intermediate cells with `# DELETABLE: ` and calls this after + # writing the final file to disk. + self.tool_registry.register_tool(ERASE_SCRATCHPAD_HISTORY_TOOL) + async def close(self) -> None: """Clean up scratchpads and other resources.""" await self._scratchpads.close_all() From f0711b1035376d6abb72ea8337bc815307f869b7 Mon Sep 17 00:00:00 2001 From: Max Stepanov Date: Fri, 24 Apr 2026 17:18:45 +0300 Subject: [PATCH 04/11] fix --- anton/templates/template-dark-github.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anton/templates/template-dark-github.html b/anton/templates/template-dark-github.html index cd936c6d..2f4123c1 100644 --- a/anton/templates/template-dark-github.html +++ b/anton/templates/template-dark-github.html @@ -1,5 +1,5 @@ - + From b74c425caa66b2c0796a391d43d41bad8aa1fbe8 Mon Sep 17 00:00:00 2001 From: Max Stepanov Date: Fri, 24 Apr 2026 17:20:05 +0300 Subject: [PATCH 05/11] new template --- anton/core/llm/prompts.py | 12 +- anton/core/tools/recall_skill.py | 2 +- ...te-dark-github.html => template-dark.html} | 122 ++++++++---------- 3 files changed, 64 insertions(+), 72 deletions(-) rename anton/templates/{template-dark-github.html => template-dark.html} (53%) diff --git a/anton/core/llm/prompts.py b/anton/core/llm/prompts.py index 83f28a7c..a5059003 100644 --- a/anton/core/llm/prompts.py +++ b/anton/core/llm/prompts.py @@ -317,14 +317,14 @@ ## 3. Reading the template in scratchpad The template file ships inside the installed `anton` package at -`anton/templates/template-dark-github.html`. Define the helper below in one of +`anton/templates/template-dark.html`. Define the helper below in one of your first scratchpad cells so the final assembly cell can call it: ```python def __read_html_template(): import anton from pathlib import Path - path = Path(anton.__file__).parent / "templates" / "template-dark-github.html" + path = Path(anton.__file__).parent / "templates" / "template-dark.html" return path.read_text(encoding="utf-8") ``` @@ -505,6 +505,14 @@ def __read_html_template(): HTML`). Once the final file is on disk, the intermediate code is no longer useful in context; the marker is what lets `erase_scratchpad_history` clear these cells afterwards. The comment has no effect on execution. +- **Append an approximate progress percentage to the end of every cell's + `one_line_description`** so the user sees the build advancing. Compute + `total = 6 + N` where `N = len(DashSpec.blocks)` (5 fixed cells + N block + cells + 1 final `__get_html()`). For the K-th cell (1-based) append + `" (~{{pct}}% done)"` with `pct = round(K / total * 100)`. Example for a + dashboard with 3 blocks (`total=9`): cell 1 → `"build page title (~11% done)"`, + cell 5 → `"build chart inits (~56% done)"`, final cell → `"assemble and save + dashboard (~100% done)"`. Cell layout — one function per marker in the template, plus one function per block from DashSpec: diff --git a/anton/core/tools/recall_skill.py b/anton/core/tools/recall_skill.py index 8f78aeec..2948f2b3 100644 --- a/anton/core/tools/recall_skill.py +++ b/anton/core/tools/recall_skill.py @@ -23,7 +23,7 @@ _DASHBOARD_TEMPLATE_PATH = ( Path(__file__).resolve().parent.parent.parent / "templates" - / "template-dark-github.html" + / "template-dark.html" ) _DASHBOARD_TEMPLATE_MARKER = "" diff --git a/anton/templates/template-dark-github.html b/anton/templates/template-dark.html similarity index 53% rename from anton/templates/template-dark-github.html rename to anton/templates/template-dark.html index 2f4123c1..d969d256 100644 --- a/anton/templates/template-dark-github.html +++ b/anton/templates/template-dark.html @@ -1,3 +1,4 @@ + @@ -25,55 +26,34 @@ body { background: var(--bg); color: var(--text); - font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; padding: 24px; + max-width: 1440px; + margin: 0 auto; min-height: 100vh; } -/* Header */ -.header { text-align: center; padding: 40px 20px 32px; border-bottom: 1px solid var(--border); margin-bottom: 32px; } -.header .badge { display: inline-block; background: rgba(88,166,255,0.12); border: 1px solid rgba(88,166,255,0.3); color: var(--accent); font-size: 12px; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; padding: 4px 12px; border-radius: 20px; margin-bottom: 16px; } -.header h1 { font-size: 2.2rem; font-weight: 700; line-height: 1.2; background: linear-gradient(135deg, var(--accent) 0%, var(--purple) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 12px; } -.header .subtitle { color: var(--muted); font-size: 1rem; max-width: 600px; margin: 0 auto; } -.source-link { display: inline-block; margin-top: 12px; color: var(--accent); font-size: 0.85rem; text-decoration: none; opacity: 0.8; } -.source-link:hover { opacity: 1; text-decoration: underline; } - -/* Grids */ -.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 24px; } -.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 24px; } -.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; } +/* Grid — single utility, agent sets --cols inline (default 2) */ +.grid { display: grid; gap: 20px; margin-bottom: 24px; + grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr)); } .col-span-2 { grid-column: span 2; } -.col-span-3 { grid-column: span 3; } /* Card */ .card { background: var(--bg2); border: 1px solid var(--border); border-radius: 12px; padding: 24px; } -.card-title { font-size: 0.7rem; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; } +.card-title { font-size: 0.75rem; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; } .card h2 { font-size: 1.1rem; font-weight: 600; color: var(--text); margin-bottom: 16px; } -/* KPI */ -.kpi { background: var(--bg2); border: 1px solid var(--border); border-radius: 12px; padding: 20px; text-align: center; } -.kpi .kpi-icon { font-size: 1.8rem; margin-bottom: 8px; } -.kpi .kpi-value { font-size: 1.9rem; font-weight: 700; line-height: 1; margin-bottom: 4px; } +/* KPI — use --c for accent color, e.g. style="--c: var(--accent3)" */ +.kpi { background: var(--bg2); border: 1px solid var(--border); border-radius: 12px; padding: 20px; text-align: center; --c: var(--accent); } +.kpi .kpi-value { font-size: 1.9rem; font-weight: 700; line-height: 1; margin-bottom: 4px; color: var(--c); } .kpi .kpi-label { font-size: 0.75rem; color: var(--muted); letter-spacing: 0.5px; } -.kpi.blue .kpi-value { color: var(--accent); } -.kpi.green .kpi-value { color: var(--accent3); } -.kpi.orange .kpi-value { color: var(--accent4); } -.kpi.red .kpi-value { color: var(--accent2); } -.kpi.purple .kpi-value { color: var(--purple); } - -/* Steps */ -.steps { display: flex; flex-direction: column; gap: 0; } -.step { display: flex; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--border); } -.step:last-child { border-bottom: none; } -.step-num { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.9rem; flex-shrink: 0; margin-top: 2px; } -.step-num.s0 { background: rgba(247,129,102,0.2); color: var(--accent2); border: 1px solid rgba(247,129,102,0.4); } -.step-num.s1 { background: rgba(210,153,34,0.2); color: var(--accent4); border: 1px solid rgba(210,153,34,0.4); } -.step-num.s2 { background: rgba(88,166,255,0.2); color: var(--accent); border: 1px solid rgba(88,166,255,0.4); } -.step-num.s3 { background: rgba(63,185,80,0.2); color: var(--accent3); border: 1px solid rgba(63,185,80,0.4); } -.step-num.s4 { background: rgba(188,140,255,0.2); color: var(--purple); border: 1px solid rgba(188,140,255,0.4); } -.step-content h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 4px; color: var(--text); } -.step-content p { font-size: 0.82rem; color: var(--muted); line-height: 1.5; } -.step-content code { background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; font-size: 0.78rem; color: var(--accent3); font-family: 'Consolas', monospace; } + +/* Tags — use --c for color, e.g. */ +.tag { display: inline-block; font-size: 0.7rem; font-weight: 600; padding: 2px 8px; border-radius: 4px; + --c: var(--accent); + color: var(--c); + background: color-mix(in oklab, var(--c) 15%, transparent); + border: 1px solid color-mix(in oklab, var(--c) 30%, transparent); } /* Tables */ .data-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } @@ -82,38 +62,23 @@ .data-table tr:last-child td { border-bottom: none; } .data-table tr:hover td { background: var(--bg3); } -/* Tags */ -.tag { display: inline-block; font-size: 0.7rem; font-weight: 600; padding: 2px 8px; border-radius: 4px; } -.tag-red { background: rgba(247,129,102,0.15); color: var(--accent2); border: 1px solid rgba(247,129,102,0.3); } -.tag-yellow { background: rgba(210,153,34,0.15); color: var(--accent4); border: 1px solid rgba(210,153,34,0.3); } -.tag-blue { background: rgba(88,166,255,0.15); color: var(--accent); border: 1px solid rgba(88,166,255,0.3); } -.tag-green { background: rgba(63,185,80,0.15); color: var(--accent3); border: 1px solid rgba(63,185,80,0.3); } -.tag-purple { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid rgba(188,140,255,0.3); } - /* Chart containers */ .chart-container { width: 100%; height: 320px; } .chart-container-lg { width: 100%; height: 380px; } -.chart-container-sm { width: 100%; height: 200px; } /* Callouts */ .insight-box { background: rgba(88,166,255,0.06); border: 1px solid rgba(88,166,255,0.2); border-left: 3px solid var(--accent); border-radius: 8px; padding: 14px 18px; margin-top: 16px; } .insight-box p { font-size: 0.83rem; color: var(--text); line-height: 1.6; } -.insight-box strong { color: var(--accent); } - -.quote { background: var(--bg3); border-left: 3px solid var(--purple); border-radius: 0 8px 8px 0; padding: 14px 18px; margin: 0; } -.quote p { font-size: 0.9rem; font-style: italic; color: var(--text); line-height: 1.6; } -.quote .author { font-size: 0.75rem; color: var(--muted); margin-top: 8px; font-style: normal; } /* Section */ section { margin-bottom: 32px; } -section > .section-title { font-size: 0.7rem; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); padding-bottom: 12px; border-bottom: 1px solid var(--border); margin-bottom: 20px; display: flex; align-items: center; gap: 8px; } +section > .section-title { font-size: 0.75rem; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); padding-bottom: 12px; border-bottom: 1px solid var(--border); margin-bottom: 20px; display: flex; align-items: center; gap: 8px; } section > .section-title::before { content: ''; display: inline-block; width: 3px; height: 14px; background: var(--accent); border-radius: 2px; } /* Responsive */ @media (max-width: 900px) { - .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } - .col-span-2, .col-span-3 { grid-column: span 1; } - .header h1 { font-size: 1.6rem; } + .grid { grid-template-columns: 1fr; } + .col-span-2 { grid-column: span 1; } } /* AGENT: CUSTOM_STYLES */ @@ -122,8 +87,6 @@ - -