Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 58 additions & 42 deletions anton/core/llm/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,20 +259,30 @@
- Throwaway files inside the scratchpad's own working directory.

WORKFLOW:
1. NEW artifact: call `create_artifact(name, description, type, primary?)` \
→ use the returned `<artifact_path>` for every subsequent write.
2. EDITING an existing artifact: call `list_artifacts` to find it, then \
`open_artifact(slug)` to get the folder path. Do NOT call `create_artifact` \
again — that creates a duplicate.
3. If you discover the entry-point filename only later (or change it), call \
`update_artifact(slug, primary=...)` so the renderer opens the right file.
4. AFTER FINISHING — reference the artifact in your final message. Once the \
artifact's files are written, tell the user what was created and point to it by \
`name` and `slug`, and include the primary file's path \
(`<artifact_path>/<primary>`) so it is clickable/openable in a plain CLI. NEVER \
end with only a description of the content and no pointer to the result. (For \
fullstack apps, prefer the `url` returned by `launch_backend` as the primary \
pointer — see the BACKEND & FULLSTACK section.)
1. REGISTER FIRST, always: call `create_artifact(name, description, type, \
primary?)`. It claims the folder and returns `<artifact_path>`.
2. THEN, for `html-app`, `fullstack-stateless-app` and \
`fullstack-stateful-app` — call `generate_artifact(slug, context)` and let it \
produce every file. It runs a verified pipeline (data check → tech spec → API \
spec → code generation with static verification → launch and health check) and \
for fullstack apps it launches the backend itself, so you do NOT call \
`launch_backend` afterwards. Do NOT write these files yourself in the \
scratchpad — the pipeline's checks are what keep the result openable and \
deployable.
3. For the other types (`document`, `dataset`, `image`, `mixed`) there is no \
generator: write the files yourself into `<artifact_path>`.
4. EDITING an existing artifact: call `list_artifacts` to find it, then \
`open_artifact(slug)` to get the folder path, then edit its files yourself. Do \
NOT call `create_artifact` again — that creates a duplicate. `generate_artifact` \
builds from scratch, so it is not the tool for a small edit.
5. If you discover the entry-point filename only later (or change it), call \
`update_artifact(slug, primary=...)` so the renderer opens the right file. \
(`generate_artifact` does this for you.)
6. AFTER FINISHING — reference the artifact in your final message. Tell the user \
what was created and point to it by `name` and `slug`, and include the primary \
file's path (`<artifact_path>/<primary>`) so it is clickable/openable in a plain \
CLI. NEVER end with only a description of the content and no pointer to the \
result. For fullstack apps, prefer the `url` the launch step returned.
"""


Expand Down Expand Up @@ -310,14 +320,18 @@
VISUALIZATIONS_HTML_OUTPUT_FORMAT_PROMPT = """\
Present analysis results as HTML dashboards/reports — the user has proactive \
dashboards enabled. Narrate the key insights in chat first (per the workflow \
above), then build the visualization as a self-contained HTML artifact.

MANDATORY: BEFORE writing any dashboard, chart, or report HTML, call \
`recall_skill("build-html-dashboard")` and follow the loaded output contract \
(artifact registration, file layout, charting library, theme, data embedding). \
Do NOT build dashboard HTML from memory of those rules — recall the skill in \
every conversation that produces one. Recalling it too often is fine; \
skipping it is not.\
above), then produce the visualization as an artifact.

Normal path: `create_artifact(type="html-app", …)` then \
`generate_artifact(slug, context)`. The generator writes the dashboard through a \
verified pipeline and its own output contract — you do NOT recall a skill or \
write the HTML yourself for this.

Building it BY HAND is the exception — editing an existing dashboard, or \
`generate_artifact` returned an error and the user asked you to continue \
manually. Only then call `recall_skill("build-html-dashboard")` first and follow \
the loaded output contract (charting library, theme, file layout, large-dataset \
handling). Never write dashboard HTML by hand from memory of those rules.\
"""


Expand All @@ -331,33 +345,35 @@
- Use markdown tables for tabular data. Keep columns aligned and readable.
- Use bold/headers for section structure. Use bullet points for lists.
- For large datasets, summarize the top N and offer to show more.
- When the user EXPLICITLY asks for a chart, dashboard, plot, or HTML visualization, \
THEN build it as a self-contained HTML file with inlined CSS, JS, and data. \
Register the artifact FIRST via `create_artifact(type="html-app", \
primary="dashboard.html", ...)` and write into the returned `<artifact_path>` — \
see the ARTIFACTS section above for the full contract. \
- When the user EXPLICITLY asks for a chart, dashboard, plot, or HTML \
visualization, THEN produce it as an artifact: `create_artifact(type="html-app", \
primary="dashboard.html", ...)` followed by `generate_artifact(slug, context)` — \
see the ARTIFACTS section above. If you end up building it BY HAND instead \
(editing an existing dashboard, or the generator failed and the user asked you \
to continue), call `recall_skill("build-html-dashboard")` first and follow the \
loaded output contract. \
Fallback only if `create_artifact` is unavailable: save to `{output_dir}` \
(create it if needed). \
MANDATORY: call `recall_skill("build-html-dashboard")` BEFORE writing the HTML \
and follow the loaded output contract (charting library, theme, file layout, \
large-dataset handling). Recalling it too often is fine; skipping it is not.\
(create it if needed).\
"""


BACKEND_GENERATION_PROMPT = """\
BACKEND & FULLSTACK APPLICATION GENERATION:

Building a backend service, API, or fullstack web app (artifact types \
`fullstack-stateless-app` / `fullstack-stateful-app`, launched via \
`launch_backend`) follows a STRICT contract: a canonical FastAPI+Mangum \
backend.py template, SECRETS handling, the `/api/*` route prefix, the \
`static/` frontend layout, requirements.txt, and a launch/preview workflow. \
The full procedure is NOT in this prompt — it lives in the \
`build-fullstack-backend` skill. MANDATORY: call \
`recall_skill("build-fullstack-backend")` BEFORE registering a fullstack \
artifact or writing any backend code. Code written without it WILL fail \
launch and deployment. If there is any chance the task involves a backend, \
recall the skill first — recalling it too often is fine; skipping it is not.\
Normal path: register the artifact (`fullstack-stateless-app` — prefer this — or \
`fullstack-stateful-app`), then call `generate_artifact(slug, context)`. The \
generator writes `backend.py`, `requirements.txt` and `static/index.html` \
against a hard contract, verifies the backend by importing it and checking its \
routes, then launches it and health-checks `/api/health`. You do NOT recall a \
skill, write backend code, or call `launch_backend` yourself on this path.

Backend code written BY HAND must follow that same strict contract — the \
canonical FastAPI+Mangum `backend.py` template, `SECRETS` read at point of use, \
the `/api/*` route prefix, the `static/` frontend layout, `requirements.txt`, \
and the launch/preview workflow — or it WILL fail launch and deployment. The \
full procedure is not in this prompt: if you are editing an existing fullstack \
artifact, or the generator failed and the user asked you to continue manually, \
call `recall_skill("build-fullstack-backend")` BEFORE touching any backend code.\
"""

CONSOLIDATION_PROMPT = """\
Expand Down
68 changes: 68 additions & 0 deletions anton/core/llm/serialize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Shared serialization of LLM messages/responses to plain JSON-able dicts.

Used by the optional debug loggers (`anton._llm_history_hook` and the
`generate_artifact` step trace). Kept provider-agnostic and dependency-free
so any logger can import it without pulling in the provider classes.
"""

from __future__ import annotations

from typing import Any


def serialize_content(content: Any) -> Any:
"""Normalize a message `content` field to JSON-able form.

Non-list content (plain strings) passes through untouched. List content
is mapped block-by-block; unknown block shapes pass through unchanged so
nothing is silently dropped.
"""
if not isinstance(content, list):
return content
blocks: list[Any] = []
for block in content:
if not isinstance(block, dict):
blocks.append(block)
continue
t = block.get("type")
if t == "text":
blocks.append({"type": "text", "text": block.get("text", "")})
elif t == "tool_use":
blocks.append({
"type": "tool_use",
"id": block.get("id"),
"name": block.get("name"),
"input": block.get("input"),
})
elif t == "tool_result":
blocks.append({
"type": "tool_result",
"tool_use_id": block.get("tool_use_id"),
"content": block.get("content"),
})
else:
blocks.append(block)
return blocks


def serialize_messages(messages: list[dict]) -> list[dict]:
"""Normalize a list of chat messages to `[{role, content}]`."""
return [
{"role": m.get("role", "?"), "content": serialize_content(m.get("content", ""))}
for m in messages
]


def serialize_response(response) -> dict:
"""Normalize an `LLMResponse` to a JSON-able dict."""
return {
"content": response.content or "",
"tool_calls": [
{"name": tc.name, "input": tc.input} for tc in response.tool_calls
],
"usage": {
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
},
"stop_reason": response.stop_reason,
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
name: build-fullstack-backend
description: 'MANDATORY reading before writing ANY backend, API, server, or fullstack
application code (create_artifact types fullstack-stateless-app / fullstack-stateful-app,
or anything that will be launched with launch_backend). Contains the complete hard
contract: the canonical FastAPI+Mangum backend.py template, SECRETS handling, /api/*
route prefix rules, static/ frontend layout, requirements.txt, and the launch/preview
workflow. Building a backend without recalling this skill first WILL break launch
and deployment. When in doubt, recall it.'
description: 'ONLY for writing backend or fullstack code BY HAND. NOT needed on the
normal path — create_artifact(fullstack-stateless-app or fullstack-stateful-app)
followed by generate_artifact writes the backend, verifies it by importing it and
launches it. Recall this when editing an existing fullstack artifact, or when
generate_artifact failed and the user asked you to continue manually: it is the hard
contract — canonical FastAPI+Mangum backend.py template, SECRETS handling, /api/*
route prefix rules, static/ frontend layout, requirements.txt, launch and preview.
Covers backend, API and server code.'
metadata:
display_name: Backend & fullstack app generation
provenance: builtin
Expand Down
12 changes: 6 additions & 6 deletions anton/core/memory/builtin_skills/build-html-dashboard/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: build-html-dashboard
description: 'MANDATORY reading before building ANY HTML dashboard, chart, plot, interactive
report, or browser-based visualization (create_artifact type="html-app", or the
frontend of a fullstack app). Contains the full HTML output contract: self-contained
file rules, Apache ECharts setup, dark theme, layout/design standards, and large-dataset
handling. Recall it BEFORE writing the first line of dashboard HTML. When in doubt,
recall it.'
description: 'ONLY for writing dashboard or chart HTML BY HAND. NOT needed on the normal
path — create_artifact(type="html-app") followed by generate_artifact writes the
dashboard itself and carries this contract internally. Recall this when editing an
existing html-app, or when generate_artifact failed and the user asked you to continue
manually: it is the output contract — self-contained file rules, Apache ECharts setup,
dark theme, layout/design standards, large-dataset handling.'
metadata:
display_name: HTML dashboard & visualization output format
provenance: builtin
Expand Down
2 changes: 2 additions & 0 deletions anton/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from anton.core.tools.registry import ToolRegistry
from anton.core.tools.tool_defs import (
CREATE_ARTIFACT_TOOL,
GENERATE_ARTIFACT_TOOL,
LAUNCH_BACKEND_TOOL,
LIST_ARTIFACTS_TOOL,
MEMORIZE_TOOL,
Expand Down Expand Up @@ -943,6 +944,7 @@ def _build_core_tools(self) -> None:
self.tool_registry.register_tool(OPEN_ARTIFACT_TOOL)
self.tool_registry.register_tool(UPDATE_ARTIFACT_METADATA_TOOL)
self.tool_registry.register_tool(LAUNCH_BACKEND_TOOL)
self.tool_registry.register_tool(GENERATE_ARTIFACT_TOOL)

async def close(self) -> None:
"""Clean up scratchpads and other resources."""
Expand Down
14 changes: 14 additions & 0 deletions anton/core/tools/generate_artifact/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Deterministic generation FSM behind the ``generate_artifact`` tool.

Public entry point: ``generate(session, artifact_type, artifact_path,
context, slug)``. The outer tool handler validates input and reads artifact
metadata; everything below this surface is provider-agnostic. ``generate``
builds a ``GenState`` and hands off to ``orchestrator.run``, which walks the
graph nodes (data-sufficiency loop → tech spec → api spec → backend/frontend
generation with verification → launch & health check). Nodes reach real data
via the `scratchpad` sub-tool, guided by the brief's ``## Data`` section.
"""

from .engine import generate, MAX_ROUNDS

__all__ = ["generate", "MAX_ROUNDS"]
107 changes: 107 additions & 0 deletions anton/core/tools/generate_artifact/debug_trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Optional, generation-scoped step trace for the `generate_artifact` tool.

Activated by the `ANTON_DEBUG_ARTIFACT_GENERATE_TOOL` env var: when its value
is a non-empty path, `make_trace()` returns a `GenTrace` that appends one
JSON-lines event per step to that file; otherwise a `NullTrace` whose methods
are all no-ops, so call sites never branch.

The trace is stored on `GenState` and every FSM node logs with an explicit
`node` label. That explicitness is deliberate: the backend and frontend
sub-loops run under `asyncio.gather`, so an ambient "current node" would race.

Every write is best-effort — the logger must never raise into generation code.
"""

from __future__ import annotations

import datetime
import json
import os
from pathlib import Path
from typing import Any


class NullTrace:
"""No-op trace: identical surface to `GenTrace`, does nothing."""

def run_start(self, **_: Any) -> None: ...
def node(self, *_: Any, **__: Any) -> None: ...
def llm_call(self, **_: Any) -> None: ...
def verdict(self, **_: Any) -> None: ...
def scratchpad(self, **_: Any) -> None: ...
def file_written(self, **_: Any) -> None: ...
def verifier(self, **_: Any) -> None: ...
def run_result(self, **_: Any) -> None: ...


class GenTrace:
"""Append-only JSON-lines writer for one process's generation runs."""

def __init__(self, path: str) -> None:
self._path = Path(path)

def _emit(self, event: str, payload: dict) -> None:
try:
rec = {
"ts": datetime.datetime.now().isoformat(timespec="seconds"),
"event": event,
**payload,
}
with open(self._path, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
except Exception:
# Best-effort: never let the logger break generation.
pass

def run_start(self, *, slug, artifact_type, artifact_path, brief, is_fullstack) -> None:
self._emit("run_start", {
"slug": slug,
"artifact_type": artifact_type,
"artifact_path": str(artifact_path),
"brief": brief,
"is_fullstack": is_fullstack,
})

def node(self, node, outcome, detail="") -> None:
self._emit("node", {"node": node, "outcome": outcome, "detail": detail})

def llm_call(self, *, node, method, system, messages,
response=None, value=None, attempt=None, round=None) -> None:
from anton.core.llm.serialize import serialize_messages, serialize_response

if response is not None:
resp = serialize_response(response)
else:
resp = {"structured": value}
self._emit("llm_call", {
"node": node,
"method": method,
"attempt": attempt,
"round": round,
"system": system,
"messages": serialize_messages(messages),
"response": resp,
})

def verdict(self, *, node, schema, value) -> None:
self._emit("verdict", {"node": node, "schema": schema, "value": value})

def scratchpad(self, *, node, input, output) -> None:
self._emit("scratchpad", {"node": node, "input": input, "output": output})

def file_written(self, *, node, path) -> None:
self._emit("file_written", {"node": node, "path": path})

def verifier(self, *, node, ok, errors, warnings) -> None:
self._emit("verifier", {
"node": node, "ok": ok, "errors": errors, "warnings": warnings,
})

def run_result(self, *, ok, result=None, error=None) -> None:
self._emit("run_result", {"ok": ok, "result": result, "error": error})


def make_trace() -> GenTrace | NullTrace:
"""Return a `GenTrace` when the debug env var names a file, else `NullTrace`."""
path = os.environ.get("ANTON_DEBUG_ARTIFACT_GENERATE_TOOL", "").strip()
return GenTrace(path) if path else NullTrace()
Loading
Loading