Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ff8efec
fix(ppt-engine): quiet the gates on template pages and read the lower…
Tchen-data Sep 15, 2026
ab086a8
feat(agent): image_search for the deck lanes, keyed by the selected s…
Tchen-data Sep 15, 2026
0c310a1
fix(raven-design): build interpreter, deck skill guidance and 400-ite…
Tchen-data Sep 15, 2026
6f3bc43
fix(*): the icon pen follows the icon's side
Tchen-data Sep 15, 2026
b50aa2c
docs(raven-design): a paper's PDF comes before its published figure
Tchen-data Sep 15, 2026
685a55a
fix(ppt-engine): keep the borrowed template files apart from the borr…
Tchen-data Sep 16, 2026
483885d
feat(ppt-engine): formulas as pictures and inline scripts for the han…
Tchen-data Sep 16, 2026
f03628e
fix(raven-design): quote the shim's interpreter, and the deck skill a…
Tchen-data Sep 16, 2026
97806cb
fix(raven-design): settle a deck from memory and defaults before asking
Tchen-data Sep 16, 2026
aa5a8f0
fix(ppt-engine): the max lane delivers the .pptx alone
Tchen-data Sep 16, 2026
8bc0dc4
fix(raven-design): the deck route uses the engine's helper modules an…
Tchen-data Sep 16, 2026
74092ce
fix(raven-design): the deck route reads the editorial skill and names…
Tchen-data Sep 16, 2026
57b7b95
fix(raven-design): furniture from the modules, the rest by hand, and …
Tchen-data Sep 16, 2026
0316742
docs(raven-design): keep nine template compositions, the ones the ref…
Tchen-data Sep 16, 2026
9623e58
fix(raven-design): the table helper is the ordinary grid, and real ma…
Tchen-data Sep 16, 2026
bbdc0d9
fix(raven-design): the body of a deck page is filled, and copy stays …
Tchen-data Sep 16, 2026
85d39e1
fix(raven-design): full components in a clear hierarchy, composed to …
Tchen-data Sep 16, 2026
1e5ef75
fix(raven-design): three lines on fullness, hierarchy and the whole page
Tchen-data Sep 16, 2026
b831add
fix(agent): read a routed lane's own picture vendor, and give the bui…
Tchen-data Sep 16, 2026
6995b78
fix(agent): image_search is offered only where a product asks for it
Tchen-data Sep 16, 2026
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
2 changes: 1 addition & 1 deletion agents/raven-design/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"agents": {
"defaults": {
"contextWindowTokens": 262144,
"maxToolIterations": 150,
"maxToolIterations": 400,
"llmCallTimeout": 1800,
"compaction": {
"enabled": true,
Expand Down
2 changes: 1 addition & 1 deletion agents/raven-design/modes/max.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"agents": {
"defaults": {
"maxToolIterations": 300
"maxToolIterations": 400
}
}
}
2 changes: 1 addition & 1 deletion agents/raven-design/modes/medium.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"agents": {
"defaults": {
"maxToolIterations": 60,
"maxToolIterations": 400,
"reasoningEffort": "low"
}
}
Expand Down
72 changes: 13 additions & 59 deletions agents/raven-design/route-raven-ppt.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
This request is a deck. The deliverable is a .pptx file -- design it yourself, from your own design skills, rather than from a packaged deck template, and still hand back a .pptx on disk (not a web page, an image, a PDF or a Markdown outline). Name the .pptx path in your reply.
This request is a deck. The deliverable is a .pptx file -- design it yourself, from your own design skills, rather than from a packaged deck template, and still hand back a .pptx on disk (not a web page, an image, a PDF or a Markdown outline). Name the .pptx path in your reply. What the request leaves open is settled by what memory recalled about the user, then by the defaults: the request's language, a general audience, about 20 pages, a light ground. Ask at most once, only for what none of these settles, recommending the default; dark only when the request names it or the brand's own material is dark.

A gap is what is left after looking, not before it. Where the material is thin, search: `web_search` first, then `web_fetch` the page itself, and keep the source beside what you took from it. Where a picture is missing, search for that too -- a real logo, a product shot, a published chart exists somewhere, and a real one is evidence where a drawn one is decoration. Invent no number, no source, no person and no place. What you could not find stays a gap that names what is missing and who has to supply it.
On this route there is no ANTI-SLOP-CHECK.md, no contract and no Task State: do not initialize or update one, and the per-turn note that says it is not initialized changes nothing. Everything else the design skills ask for still applies. The check before the file is handed over is the render: every page, at 90 dpi or more, read back, and what it showed fixed and rendered again. Every component is full; components stand in a hierarchy, one thing read first and the rest stepping down; the layout uses the whole page.

Generate what does not exist. The cover, the contents page, the closing page and every section opener get a background picture you make with `image_generate` -- not a flat colour block, not a body page's photograph moved over, and not nothing.
Before the first page, `read_skill local/deck-to-pptx` and `read_skill local/design-editorial-and-presentations`; the second owns the content order and the editorial judgement, and the rule about ledgers above does not excuse reading it. The first is this deployment's own deck skill -- which tools answer here, what their parameters are, what the gates refuse -- and the domain catalog you were shown does not list it.

A gap is what is left after looking, not before it. Where the material is thin, search: `web_search` first, then `web_fetch` the page itself, and keep the source beside what you took from it. Where a picture is missing, `image_search` it: a real logo, a product shot, a published chart, a paper's own figure and a photograph of a real place -- a skyline, a landmark, a street -- exist already, and a real one is evidence where a drawn one is decoration. Go through the plan once before the first page, name the picture each page wants, and pass them all as `queries=[...]` in one call; every hit carries the direct image URL, its pixel size and the page it came from. Download what you pick and look at it before placing it, and keep the page link beside it for the source note. When `image_search` is not in your tool list, this deployment has no image search: say so, and carry those pages on type, grid and colour. Invent no number, no source, no person and no place. What you could not find stays a gap that names what is missing and who has to supply it.

Generate what does not exist -- illustration, backdrop, atmosphere -- and never a stand-in for something a search would find: a generated skyline of a real city, a generated product or a generated person is a page stating something false about a thing that exists. The cover, the contents page, the closing page and every section opener get a background picture -- a photograph the search found, laid under a plane of ink with light type over it, or one you make with `image_generate` when what the page wants is texture, light or abstraction rather than a place -- not a flat colour block, not a body page's photograph moved over, and not nothing.

Two things before each generation, in this order.

Expand All @@ -11,64 +15,14 @@ Two things before each generation, in this order.

Then write the prompt as subject, mood, and what to leave out. Name the region the title needs by side and by share -- "the left 55% of the canvas stays almost pure dark, reserved for title text" -- and say no text, no letters, no numbers: generated lettering comes out wrong in every language, and every word on the page is set by the typography. Render the page again afterwards and look at it, because the picture was for that page and only the composed page counts.

Icons are packaged and do not have to be drawn by hand, generated, or taken off the web. `raven_ppt.services.assets.icons` sits beside this agent and holds 1304 Tabler outline icons, each carrying its upstream tags, so the search reads what a unit is *about* rather than what a file is called: `icon_candidates("deadline")` answers `calendar_due`, `icon_candidates("risk")` answers `warning`, `icon_candidates("inventory")` reaches `building_warehouse`. Ask one concrete word at a time; an abstraction ("throughput", "supply chain") comes back with nothing usable. `resolve_icon_name` takes the upstream spelling (`map-pin` -> `map_pin`). `icon_paths(name)` hands back that icon's strokes on a 24x24 grid as `(op, coords)` pairs: `M` starts a new run, `L` adds a point, `C` is a cubic carrying two control points then its endpoint, and there is no `Z`. Draw them as freeform shapes:

```python
from pptx.dml.color import RGBColor
from pptx.util import Emu, Pt
from raven_ppt.services.assets.icons import icon_paths

EMU, STYLE = 914400, "{http://schemas.openxmlformats.org/presentationml/2006/main}style"

Page furniture comes from the engine's helper modules and is not rewritten; what makes a page its own is drawn by hand. Before the build script, write the modules beside it, once:

def icon_runs(name):
runs = []
for path in icon_paths(name):
run, here = [], None
for op, xy in path:
if op == "M":
if len(run) > 1:
runs.append(run)
here = (xy[0], xy[1])
run = [here]
elif op == "L":
here = (xy[0], xy[1])
run.append(here)
elif op == "C" and here is not None:
(x0, y0), (x1, y1, x2, y2, x3, y3) = here, xy
for step in range(1, 9):
t, u = step / 8, 1 - step / 8
run.append((u**3 * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t**3 * x3,
u**3 * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t**3 * y3))
here = (x3, y3)
if len(run) > 1:
runs.append(run)
return runs


def add_icon(slide, name, left_in, top_in, size_in, rgb, width_pt=1.5):
scale, pen = size_in * EMU / 24.0, Pt(width_pt)
for run in icon_runs(name):
pts = [(left_in * EMU + x * scale, top_in * EMU + y * scale) for x, y in run]
xs, ys = [x for x, _ in pts], [y for _, y in pts]
dot = max(max(xs) - min(xs), max(ys) - min(ys)) < pen
if dot:
pts = [(xs[0], ys[0]), (xs[0] + pen, ys[0]), (xs[0] + pen, ys[0] + pen), (xs[0], ys[0] + pen)]
builder = slide.shapes.build_freeform(Emu(int(pts[0][0])), Emu(int(pts[0][1])))
builder.add_line_segments([(Emu(int(x)), Emu(int(y))) for x, y in pts[1:]], close=dot)
shape = builder.convert_to_shape()
if dot:
shape.fill.solid()
shape.fill.fore_color.rgb = RGBColor.from_string(rgb)
shape.line.fill.background()
else:
shape.fill.background()
shape.line.color.rgb = RGBColor.from_string(rgb)
shape.line.width = pen
for style in shape._element.findall(STYLE):
shape._element.remove(style)
```
raven-python -c "from pathlib import Path; from raven_ppt.services.assets.script_helpers import script_helper_files; [Path(n).write_text(t, encoding='utf-8') for n, t in script_helper_files().items()]"
```

That puts `ppt_layout.py`, `ppt_theme.py`, `ppt_icons.py`, `ppt_shapes.py` and `ppt_charts.py` in the working directory. Furniture from them: `page`, `heading`, `write`, `points`, `card`, `plane`, `rule`, `footer`, `table`, `table_size`, `picture_fit`, `formula`, `stack`, `Box`, `fits` and `text_size` from `ppt_layout` (`table` is the ordinary grid; merged cells, a header spanning columns, an icon or a colour inside a cell, or more columns than fit at 14pt are drawn by hand); `add_icon` and `find_icons` from `ppt_icons`; `connect`, `timeline` and `preset` from `ppt_shapes`; the chart functions from `ppt_charts`. Every drawing call takes a theme dict: start from `THEMES[next(iter(THEMES))]` in `ppt_theme` and copy it with the deck's own values -- `background`, `surface`, `foreground`, `muted`, `accent`, `accent_soft`, `grid`, `font_family`, `cjk_font_family`. Coordinates are inches in a `Box`; ask a region to divide itself (`page().body.grid(3, 1)`, `split_left`, `stack`) rather than typing coordinates. By hand, with python-pptx: a diagram, a polygon, a map, a custom arrow, a gradient, a hero number, a cover or section composition, and the treatment of every placed picture -- a hairline or a frame, a shadow, a scrim under type laid over it, a crop to the shape the page wants, a full-bleed background under a plane of ink. `picture_fit` places and captions; it does not treat.

A run whose whole span is thinner than the pen is one of upstream's dots and is painted rather than stroked, which is what the `dot` branch is for; the `p:style` element has to go or the theme stamps a drop shadow under every stroke. An icon is a mark and not an illustration: around 0.7in on a card, one weight and one colour across the deck, and no filled coloured disc behind each one. Both imports live on the interpreter raven itself runs on, which is not the `python3` that PATH resolves to: probe with `python3 -c "import raven_ppt, pptx"` and, where that fails, run the build script on the raven installation's own `.venv/bin/python`, or put that interpreter's `site-packages` first on `sys.path` at the top of the script. `icons` is the module `icons.py` and not a directory, so listing that path comes back empty whether or not the package is there -- the import is the only thing that answers. If that import is not available here, design the marks yourself -- do not generate them as pictures.
Generic marks -- a camera, a warning, a calendar -- come from the packaged icon set and are not drawn by hand or generated; the mark of a real thing -- a company's logo, a product's icon, a framework's badge -- is searched with `image_search`, downloaded and placed as a picture, and a deck that names companies or products carries their marks. `raven_ppt.services.assets.icons` sits beside this agent and holds 1304 Tabler outline icons, each carrying its upstream tags, so the search reads what a unit is *about* rather than what a file is called: `icon_candidates("deadline")` answers `calendar_due`, `icon_candidates("risk")` answers `warning`, `icon_candidates("inventory")` reaches `building_warehouse`; in the script, `find_icons("risk")` from `ppt_icons` answers the same names. Ask one concrete word at a time; an abstraction ("throughput", "supply chain") comes back with nothing usable. `resolve_icon_name` takes the upstream spelling (`map-pin` -> `map_pin`). `add_icon(slide, name, Inches(x), Inches(y), Inches(0.7), colour)` draws one: around 0.7in on a card, one weight and one colour across the deck, no filled coloured disc behind it, and no `width_pt`. Run the build script with `raven-python`, the interpreter on PATH that has `pptx` and `raven_ppt`; `python3` does not. Check with `raven-python -c "import raven_ppt, pptx"`. Do not look for a `.venv`, build one, or install python-pptx. If that import is not available here, design the marks yourself -- do not generate them as pictures. A formula that stacks (fraction, root, sum with limits): `formula(slide, box, tex, theme, size=...)` from `ppt_layout`, or `add_formula(slide, tex, left_in, top_in, size_pt=...)` from `raven_ppt.services.assets.formulas`. A sentence carrying symbols (`p(theta | D_A)`, `F_i`, `theta*`): `math_runs(paragraph, text, size_pt=...)` from the same module. Never leave `_A` or `^2` as characters, and never type a formula as a line of text.

With no image credentials configured, say so plainly and carry those pages on typography, grid and colour. Do not claim a search or a generation you did not run.
69 changes: 65 additions & 4 deletions agents/raven-design/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import importlib.util
import json
import os
import shlex
import sys
from copy import deepcopy
from pathlib import Path
Expand All @@ -24,19 +25,26 @@

PRODUCT = "raven-design"

# The build interpreter the deck skill names. `python3` on PATH is whichever
# the machine has; the one with python-pptx and raven_ppt is the one this
# launcher runs on, and a symlink to a venv's python loses the venv (CPython
# resolves it back to the base interpreter), so the shim is a shell wrapper.
INTERPRETER_SHIM = "raven-python"
INTERPRETER_SHIM_CMD = "raven-python.cmd"

MODES_DIR = HERE / "modes"
MODE_LABELS = {
"medium": (
"Medium",
"Bounded: 60 tool iterations. A quick draft or a small revision.",
"400 tool iterations at low reasoning effort. A quick draft or a small revision.",
),
"high": (
"High",
"The default: 150 tool iterations.",
"The default: 400 tool iterations.",
),
"max": (
"Max",
"300 tool iterations. A full deliverable where the ceiling matters more than the bill.",
"400 tool iterations at the host's full reasoning effort. A full deliverable where the ceiling matters more than the bill.",
),
}
BASELINE_MODE = "high"
Expand All @@ -58,6 +66,28 @@ def state_root() -> Path:
return render.product_state_root(PRODUCT, override=env_value("DESIGN_STATE_ROOT"))


def write_interpreter_shim(root: Path) -> Path:
"""Write ``<root>/bin/raven-python`` and ``raven-python.cmd`` running this interpreter; return the bin dir.

Rewritten on every launch: a reinstall moves the interpreter, and a shim
pointing at the old one would fail exactly the way `python3` does.
"""
bin_dir = root / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
# Two files for one command: the POSIX shell resolves `raven-python` to the
# extensionless script, and cmd.exe resolves it to `raven-python.cmd` through
# PATHEXT -- a shell script is not executable there at all.
for name, text in (
(INTERPRETER_SHIM, f'#!/bin/sh\nexec {shlex.quote(sys.executable)} "$@"\n'),
(INTERPRETER_SHIM_CMD, f'@echo off\r\n"{sys.executable}" %*\r\n'),
):
staged = bin_dir / f".{name}.{os.getpid()}"
staged.write_text(text, encoding="utf-8", newline="")
staged.chmod(0o755)
os.replace(staged, bin_dir / name)
return bin_dir
Comment thread
gloryfromca marked this conversation as resolved.
Resolved


def engine_skill_dir() -> Path | None:
"""The domain-Skill corpus directory inside the installed engine wheel.

Expand Down Expand Up @@ -94,6 +124,23 @@ def configure_image_generation(config: dict, host: dict) -> None:
render.inherit_media_image(config, host)


def inherit_everos_address(config: dict, host: dict) -> str | None:
"""Point this product's memory at the host's EverOS server, when the host names one.

The product config carries the stock port; a host that runs its own EverOS on
another port (every second instance on one machine does) would otherwise send
this lane's turns to whatever answers on the stock one.
"""
host_slice = ((host.get("plugins") or {}).get("config") or {}).get("everos-memory") or {}
base_url = host_slice.get("base_url") if isinstance(host_slice, dict) else None
if not isinstance(base_url, str) or not base_url.strip():
return None
own = config.setdefault("plugins", {}).setdefault("config", {}).setdefault("everos-memory", {})
if isinstance(own, dict):
own["base_url"] = base_url.strip()
return base_url.strip()


def render_config(source: Path) -> Path:
"""Write a copy of ``source`` with the secrets merged in, under the state root.

Expand All @@ -105,8 +152,13 @@ def render_config(source: Path) -> Path:
config = json.loads(source.read_text(encoding="utf-8"))
host = render.host_config()

config.setdefault("tools", {})["web"] = deepcopy((host.get("tools") or {}).get("web") or {})
web = deepcopy((host.get("tools") or {}).get("web") or {})
# The picture search is off unless a product asks for it; this one places
# pictures, so it asks, on top of whatever vendor and key the host holds.
web.setdefault("search", {})["images"] = True
config.setdefault("tools", {})["web"] = web
configure_image_generation(config, host)
inherit_everos_address(config, host)

defaults = config.setdefault("agents", {}).setdefault("defaults", {})
for key in ("model", "provider", "reasoningEffort"):
Expand Down Expand Up @@ -192,6 +244,15 @@ def render_config(source: Path) -> Path:
# Still no plugins.dirs: the design-engine wheel arrives by entry point,
# never by directory (the everos-memory shape).
root.mkdir(parents=True, exist_ok=True)

# Appended, never prepended: `python3` stays the machine's own, and an
# operator's pathAppend keeps every entry they wrote ahead of ours.
exec_config = config.setdefault("tools", {}).setdefault("exec", {})
if isinstance(exec_config, dict):
own = str(exec_config.get("pathAppend") or "")
bin_dir = str(write_interpreter_shim(root))
exec_config["pathAppend"] = os.pathsep.join(part for part in (own, bin_dir) if part)

render.sweep_stale_renders(root)
return render.write_rendered(config, root)

Expand Down
Loading
Loading