diff --git a/anton/core/llm/prompt_builder.py b/anton/core/llm/prompt_builder.py index d7340fe..734bcc5 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 73f57da..42e2e1e 100644 --- a/anton/core/llm/prompts.py +++ b/anton/core/llm/prompts.py @@ -236,124 +236,358 @@ 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 ` + + + + + + + + + + diff --git a/tests/test_prompt_builder_skills.py b/tests/test_prompt_builder_skills.py index 6796e16..3a7878e 100644 --- a/tests/test_prompt_builder_skills.py +++ b/tests/test_prompt_builder_skills.py @@ -48,15 +48,20 @@ def _build_prompt(builder: ChatSystemPromptBuilder, **overrides) -> str: class TestProceduralMemorySection: - def test_no_store_omits_section(self): + def test_no_store_renders_only_builtins(self): builder = ChatSystemPromptBuilder() prompt = _build_prompt(builder, skill_store=None) - assert "Procedural memory" not in prompt + # Section is always present because at least one built-in skill exists. + assert "## Procedural memory" in prompt + assert "`generate_dashboard_html`" in prompt - def test_empty_store_omits_section(self, empty_store: SkillStore): + def test_empty_store_renders_only_builtins(self, empty_store: SkillStore): builder = ChatSystemPromptBuilder() prompt = _build_prompt(builder, skill_store=empty_store) - assert "Procedural memory" not in prompt + assert "## Procedural memory" in prompt + assert "`generate_dashboard_html`" in prompt + # No user-skill labels leak in from an empty store. + assert "`csv_summary`" not in prompt def test_populated_store_renders_section(self, populated_store: SkillStore): builder = ChatSystemPromptBuilder() @@ -130,8 +135,8 @@ def test_handles_skill_without_when_to_use(self, tmp_path: Path): assert "`bare`" in prompt # No crash, even with no when_to_use - def test_skip_section_when_store_raises(self, tmp_path: Path, monkeypatch): - """If the store blows up at read time, the section is omitted gracefully.""" + def test_store_failure_falls_back_to_builtins(self, tmp_path: Path, monkeypatch): + """If the store blows up at read time, we still render the built-ins.""" s = SkillStore(root=tmp_path / "skills_broken") def boom(self): @@ -140,4 +145,5 @@ def boom(self): monkeypatch.setattr(SkillStore, "list_summaries", boom) builder = ChatSystemPromptBuilder() prompt = _build_prompt(builder, skill_store=s) - assert "Procedural memory" not in prompt + assert "## Procedural memory" in prompt + assert "`generate_dashboard_html`" in prompt diff --git a/tests/test_session_skills_init.py b/tests/test_session_skills_init.py index 76c584a..ab09f5b 100644 --- a/tests/test_session_skills_init.py +++ b/tests/test_session_skills_init.py @@ -74,7 +74,7 @@ def test_section_appears_when_store_passed( assert "## Procedural memory" in prompt assert "csv_summary" in prompt - def test_section_omitted_when_no_store(self): + def test_section_renders_builtins_when_no_store(self): builder = ChatSystemPromptBuilder() prompt = builder.build( current_datetime="2026-04-10", @@ -82,7 +82,11 @@ def test_section_omitted_when_no_store(self): proactive_dashboards=False, skill_store=None, ) - assert "Procedural memory" not in prompt + # Section is always present because of hardcoded built-in skills. + assert "## Procedural memory" in prompt + assert "`generate_dashboard_html`" in prompt + # No user skills leak when there's no store. + assert "csv_summary" not in prompt class TestDispatchRoundtrip: