@qvac/skills 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,399 +0,0 @@
1
- # Building a New Deck (python-pptx)
2
-
3
- Create a new `.pptx` from scratch by running Python through the `exec` tool.
4
- A new deck needs **no** `inputs` — do not invent attachment ids. **Exactly
5
- one** `exec` call per user request when that call succeeds.
6
-
7
- **A deck that already exists in this chat is never rebuilt here.** "Add a
8
- slide", "change a title", "revise the deck" — any request that starts from an
9
- existing `.pptx` is an EDIT: load `references/edit.md` and stage the deck by
10
- its `attachmentId`. Building a fresh deck for an edit request throws away
11
- every slide the user already has.
12
-
13
- ## The exec call
14
-
15
- ```json
16
- {
17
- "language": "python",
18
- "packages": ["python-pptx==1.0.2"],
19
- "outputs": ["deck.pptx"],
20
- "command": "..."
21
- }
22
- ```
23
-
24
- - `language` (required) — always `"python"`.
25
- - `packages` (required) — `["python-pptx==1.0.2"]` on every call. Pin the
26
- version; an unpinned install resolves a potentially different library
27
- version. This exact version ships with the app and installs with no network;
28
- any other version has to be downloaded, which fails on a device that is
29
- offline.
30
- - `outputs` — `["deck.pptx"]`. `save("deck.pptx")` must match the declared
31
- output name exactly. A file you write but do not declare here is discarded.
32
- - `command` — the multi-line Python source, with real newline characters.
33
- Never collapse it to one line joined by `;` — a `for`/`if`/`with` after a
34
- semicolon is a `SyntaxError`. `command` is the program: its first line is
35
- the first line of Python that runs. There is no shell and no interpreter to
36
- invoke, and no installer — packages are declared in `packages`.
37
- - No `inputs` key at all for a new deck.
38
- - `maxOutputChars` (stdout cap, default 8192, max 65536) is never needed on a
39
- build call — it prints one line.
40
-
41
- ## Embedding images
42
-
43
- Two kinds of image input, told apart by where the image came from:
44
-
45
- **Tool-produced files** (`generate_image` output, a prior deck return): stage
46
- them with the exact `attachmentId` from the tool result — never placeholders
47
- like `att_deck`, `att_image`, or any id you made up.
48
-
49
- **Images the user uploaded** ("use this image", a photo attached to their
50
- message): there is no id to copy — an uploaded image shows none. Stage them
51
- with `path` only and **no `attachmentId` key**; the first id-less entry is the
52
- first image of the user's latest message, the second is its second image, and
53
- so on — never more id-less entries than that message has images. When it has
54
- none, a single id-less entry resolves to the chat's most recent image instead.
55
-
56
- Seeing the image in your context is not the same as staging it: the deck is
57
- built by Python, which reads the working directory and never your context, so
58
- an uploaded image reaches a slide only through an id-less `inputs` entry. Do
59
- not call `generate_image` to recreate what the user attached, and do not tell
60
- them the image cannot be used — the id-less entry is how it is used.
61
-
62
- **Files the user uploaded that are not images** (a `.pptx` to revise, any
63
- document): these *do* show an id, on the `[Attached file "…" — attachmentId:
64
- …]` line of the message that carried them. Copy it verbatim into
65
- `attachmentId`, exactly as for a tool-produced file. The id-less form never
66
- reaches them. (Revising an existing deck is its own flow — load
67
- `references/edit.md`.)
68
-
69
- ```json
70
- {
71
- "language": "python",
72
- "packages": ["python-pptx==1.0.2"],
73
- "inputs": [
74
- { "attachmentId": "<id from generate_image or prior deck>", "path": "slide1.png" },
75
- { "path": "uploaded.png" }
76
- ],
77
- "outputs": ["deck.pptx"],
78
- "command": "..."
79
- }
80
- ```
81
-
82
- Staged files land in the working directory under the bare `path` names —
83
- reference `slide.shapes.add_picture("slide1.png", …)` by that name only.
84
- Paths must be unique bare filenames. The working directory is fresh on every
85
- call, so a file written by an earlier call is gone unless it is staged again
86
- as a chat attachment; an attachment from an earlier turn can be used when its
87
- attachment id is available in the conversation — from a tool result or an
88
- `[Attached file …]` line — otherwise ask the user to attach the file again.
89
-
90
- `attachment … not found in this chat` means you invented an id or the file is
91
- not attached. If the file you meant is an image the user uploaded, drop the
92
- `attachmentId` key; if it came from a tool result or an `[Attached file …]`
93
- line, re-copy the exact id; for a new deck drop `inputs` entirely; otherwise
94
- ask the user to re-attach.
95
-
96
- If an image was staged in `inputs`, embed it in **that** single build with
97
- `slide.shapes.add_picture` — never deliver a deck and then rebuild to add the
98
- image. Soft-failing (`try`/`except` around the picture) and saving without it
99
- is a failed turn, not a success.
100
-
101
- ## Image URLs do not work — never download
102
-
103
- Your Python code has **no network access**: `requests`, `urllib`, and `socket`
104
- all fail with a network error, and `http_request` returns truncated text, never
105
- image bytes. When the user gives an image URL, do not try to fetch it from Python
106
- and do not retry through other tools — that is a dead end. Say the link cannot be
107
- downloaded and ask the user to attach the image itself, or offer `generate_image`
108
- for a similar visual. Then build the deck with the staged attachment as above.
109
-
110
- Say it in the reply, every time. A deck that quietly ships without the image the
111
- user linked is a failed turn: they asked for that image, and silence reads as
112
- though it is on the slide. Name the URL you could not fetch and what you need
113
- instead.
114
-
115
- ## Writing the Deck
116
-
117
- Start from this. It is a complete, working deck — a title slide and a bullet slide,
118
- 16:9, every paragraph sized, saved under the declared output name. Copy it and change
119
- the content; do not assemble a deck from memory.
120
-
121
- **Keep the source multi-line.** A `for`/`if`/`with` after a semicolon is a
122
- `SyntaxError` — paste the block with real newlines, not `stmt; for x in y: …`.
123
-
124
- **Every length is a typed length — `Inches(...)`, `Pt(...)` or `Emu(...)`, never
125
- a bare number.** This holds for every position and size anywhere in the deck:
126
- both pairs of `add_textbox(left, top, width, height)`, the `left`/`top` and the
127
- `width`/`height` of `add_picture(...)`, `prs.slide_width` and `prs.slide_height`,
128
- table column widths and row heights, and every margin or offset.
129
-
130
- python-pptx reads a bare number as **EMU**, and there are 914400 EMU to the inch.
131
- So `add_textbox(0, 0, 12, 0)` is not "12 wide" — it is a box 0.000013in wide and
132
- 0in tall, pinned to the top-left corner. Nothing raises: `exitCode` is `0`, there
133
- is no traceback and no warning, and the deck is delivered looking broken. Write
134
- `add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))` instead.
135
-
136
- Recognise the symptom, because it is the only signal you get: **text crammed
137
- into the top-left corner, or a box that has no size**, means a raw number reached
138
- an argument that required a typed length. Do not tune the numbers — wrap them.
139
-
140
- ```python
141
- from pptx.util import Inches
142
-
143
- box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1.5))
144
- # NOT add_textbox(1, 1, 8, 2) — that is 8 EMU wide, an invisible box
145
- ```
146
-
147
- **Keep every underscore in API names.** `text_frame`, `add_slide`, `slide_layouts`,
148
- `slide_width`, `word_wrap`, `add_paragraph`, `add_picture`, `add_textbox`, `PP_ALIGN`
149
- — stripping them to `textframe` / `addslide` / `addpicture` fails. Copy identifiers
150
- exactly as written below:
151
-
152
- ```python
153
- from pptx import Presentation
154
- from pptx.util import Inches, Pt
155
- from pptx.enum.text import PP_ALIGN
156
-
157
- prs = Presentation()
158
- prs.slide_width = Inches(13.333) # 16:9 is not the default
159
- prs.slide_height = Inches(7.5)
160
-
161
- # Title slide — layout 0 owns a title and a subtitle
162
- slide = prs.slides.add_slide(prs.slide_layouts[0])
163
- slide.shapes.title.text = "Why the Sky Is Blue"
164
- slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(44)
165
- subtitle = slide.placeholders[1].text_frame
166
- subtitle.text = "Rayleigh scattering, in four points"
167
- subtitle.paragraphs[0].font.size = Pt(24)
168
-
169
- # Content slide — layout 1 owns a title and a body
170
- slide = prs.slides.add_slide(prs.slide_layouts[1])
171
- slide.shapes.title.text = "What Happens"
172
- slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36)
173
- tf = slide.placeholders[1].text_frame
174
- tf.word_wrap = True
175
- for index, point in enumerate([
176
- "Sunlight arrives carrying every visible wavelength",
177
- "Air molecules scatter short wavelengths hardest",
178
- "Blue scatters far more than red",
179
- "So the daytime sky reads blue in every direction",
180
- ]):
181
- p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()
182
- p.text = point
183
- p.font.size = Pt(20)
184
-
185
- prs.save("deck.pptx") # must match the declared output exactly
186
- print(f"{len(prs.slides)} slides")
187
- ```
188
-
189
- Layouts `0` and `1` own the placeholders that example writes to.
190
-
191
- **Choosing a layout has one rule, and it depends on where the deck came from:**
192
-
193
- - **Adding to a deck the user gave you** — reuse the layout its own slides
194
- already use: read a comparable existing slide and pass its `.slide_layout` to
195
- `add_slide`. That is the only way the new slide inherits the deck's theme.
196
- That flow is `references/edit.md` — load it.
197
- - **Building a new deck** — index the bundled template, which commonly uses `0`
198
- (title), `1` (title + content), `5` (title only), and `6` (blank). Those
199
- indices belong to *that* template and mean nothing on an uploaded deck.
200
-
201
- A layout only owns the placeholders it declares, and python-pptx returns `None` for
202
- the rest — on the blank layout `shapes.title` is `None`, so `shapes.title.text = …`
203
- raises `AttributeError: 'NoneType' object has no attribute 'text'`, and
204
- `placeholders[1]` raises `KeyError`. So each slide is one of exactly two kinds, never
205
- a mix:
206
-
207
- | slide kind | layout | how you write text |
208
- | --- | --- | --- |
209
- | title / title + body | the deck's own layout, or `0`, `1`, `5` in a new deck | `shapes.title`, `placeholders[1]` |
210
- | hand-designed | `6` (blank) | `shapes.add_textbox(...)` for **every** box, title included |
211
-
212
- On layout `6` there is no title to reach for — the title is a text box you add.
213
-
214
- The blank layout is **not** a co-equal way to write a content slide. It is for a
215
- slide you are genuinely designing by hand — a full-bleed image, a diagram, a
216
- custom split — and it inherits no font, size, colour or position from the
217
- template. Using it plus `add_textbox` to hold ordinary title-and-bullets content
218
- on a deck the user uploaded produces a slide that visibly does not belong: wrong
219
- typeface, wrong sizes, wrong margins, and no bullets. Placeholders exist so you
220
- do not have to reproduce a theme you cannot see.
221
-
222
- Bullets — set `tf.text` for the first bullet, then `add_paragraph()` for the rest.
223
- Using `add_paragraph()` for the first one leaves a blank leading line.
224
-
225
- A new text frame holds exactly **one** paragraph, and `tf.paragraphs` is a tuple, so
226
- `tf.paragraphs[1]` raises `IndexError: tuple index out of range` until you have added
227
- it. Grow the frame with `p = tf.add_paragraph()`, which returns the new paragraph, and
228
- write through that. A paragraph owns `.text`, `.font` and `.alignment` and nothing
229
- else — it has no `.paragraphs` and no `.add_paragraph()`, so never reassign your frame
230
- variable to a paragraph.
231
-
232
- The template's body placeholder inherits 28pt, so size every paragraph you add —
233
- including sub-levels — or it renders far larger than intended:
234
-
235
- ```python
236
- from pptx.util import Pt
237
-
238
- slide = prs.slides.add_slide(prs.slide_layouts[1])
239
- slide.shapes.title.text = "Agenda"
240
- tf = slide.placeholders[1].text_frame
241
- tf.word_wrap = True
242
- for index, point in enumerate(["first point", "second point"]):
243
- p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()
244
- p.text = point
245
- p.font.size = Pt(20) # an unsized paragraph inherits 28pt
246
- ```
247
-
248
- Every deck returned through `outputs` is fitted before it reaches the user, so text
249
- that would overflow its box is shrunk to fit automatically. That is a safety net, not
250
- a licence to overfill: shrinking below about 16pt is unreadable from a room. Budget
251
- each slide at no more than five bullets of about 100 characters. A sixth bullet is a
252
- second slide titled `... (cont.)`, never a smaller font — and prose belongs in the
253
- chat reply, not on a slide.
254
-
255
- Free text — the only way to add text outside a placeholder is
256
- `shapes.add_textbox(left, top, width, height)` — all four are required, and all
257
- four are typed lengths, never bare numbers — then write into its `.text_frame`.
258
- There is no `add_text_frame`, no `add_text`, and no `add_paragraph` on `shapes`:
259
-
260
- ```python
261
- slide = prs.slides.add_slide(prs.slide_layouts[6])
262
- box = slide.shapes.add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))
263
- tf = box.text_frame
264
- tf.word_wrap = True
265
- tf.text = "Why the sky is blue"
266
- tf.paragraphs[0].font.size = Pt(40) # from pptx.util import Pt
267
- ```
268
-
269
- Text always lives on the `.text_frame`, never on the shape: `box.paragraphs`,
270
- `box.add_paragraph()`, `box.word_wrap`, and `box.font` all raise AttributeError. Go
271
- through `tf = box.text_frame` first — `tf.text`, `tf.paragraphs[0]`,
272
- `tf.add_paragraph()`, `tf.word_wrap`. `shape.text` is the one shortcut that reads
273
- through to the frame; there is no matching `shape.font`.
274
-
275
- Formatting lives one level lower still — on a paragraph or a run, never on a shape or
276
- a frame. A title is sized through its paragraph:
277
-
278
- ```python
279
- title = slide.shapes.title
280
- title.text = "Why the Sky is Blue"
281
- title.text_frame.paragraphs[0].font.size = Pt(44) # not title.font.size
282
- title.text_frame.paragraphs[0].font.bold = True
283
- title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
284
- ```
285
-
286
- Slides are added with `prs.slides.add_slide(layout)` — `prs.add_slide` does not
287
- exist. Alignment comes from an enum import, not an attribute path:
288
-
289
- ```python
290
- from pptx.enum.text import PP_ALIGN
291
-
292
- tf.paragraphs[0].alignment = PP_ALIGN.CENTER
293
- ```
294
-
295
- Bullet characters are not needed — a placeholder body renders bullets itself. Give
296
- each bullet its own paragraph; never pack several `\n`-joined bullets into one.
297
- Never type the marker into the text: `"1. "`, `"2. "`, `"- "` and `"• "` prefixes
298
- render *next to* the bullet the placeholder already draws, in the wrong font.
299
- Bullets belong in a body placeholder for exactly this reason — a bare
300
- `add_textbox` draws none, and typing them by hand to compensate is the wrong fix.
301
- Put the content in a placeholder instead.
302
-
303
- Font color — the type is `RGBColor` with **RGB in all caps**. Not `RgbColor`,
304
- `rgbColor`, or `rgb_color`:
305
-
306
- ```python
307
- from pptx.dml.color import RGBColor
308
-
309
- title.text_frame.paragraphs[0].font.color.rgb = RGBColor(0x1A, 0x73, 0xE8)
310
- ```
311
-
312
- Backgrounds and fills — `background` hangs off the slide itself, never off
313
- `slide.shapes` or a shape, and the fill is set in two steps: `solid()` first,
314
- then the color:
315
-
316
- ```python
317
- from pptx.dml.color import RGBColor
318
-
319
- fill = slide.background.fill # slide.shapes has no background
320
- fill.solid()
321
- fill.fore_color.rgb = RGBColor(0x0B, 0x1F, 0x3A)
322
-
323
- box.fill.solid() # a shape is tinted through its own .fill
324
- box.fill.fore_color.rgb = RGBColor(0xF2, 0xF2, 0xF2)
325
- ```
326
-
327
- A paragraph has no `.fill` at all — coloring text goes through the font,
328
- `paragraph.font.color.rgb = RGBColor(...)`, as above. `fill` exists on a shape
329
- and on `slide.background`, nowhere else you will need.
330
-
331
- Images — go through the shapes collection: `slide.shapes.add_picture(...)`. A
332
- `Slide` has no picture or text-box methods of its own — `slide.add_picture`,
333
- `slide.addpicture`, `slide.add_textbox`, and `slide.addtextbox` all raise
334
- `AttributeError: 'Slide' object has no attribute '…'`. Fix: put `.shapes` between
335
- `slide` and the method. Pass only one of `width`/`height`; passing both distorts
336
- the picture. Generate image-slide visuals at 1024×512 so they fill the content box;
337
- a 512×512 square letterboxes with wide empty bands either side.
338
-
339
- **Do not soft-fail images or imports.** Never wrap `add_picture` or color imports in
340
- `try`/`except` that prints a warning and continues. A missing file or
341
- `cannot import name 'RgbColor'` must raise so you fix it and rerun — a deck that
342
- saves without the requested image is a failed turn, not a success.
343
-
344
- ```python
345
- slide = prs.slides.add_slide(prs.slide_layouts[6])
346
- # correct: slide.shapes.add_picture — never slide.add_picture / slide.addpicture
347
- slide.shapes.add_picture("slide1.png", Inches(0.75), Inches(1.0), width=Inches(11.83))
348
- prs.save("deck.pptx")
349
- print(f"{len(prs.slides)} slides")
350
- ```
351
-
352
- ## Errors
353
-
354
- - Never print the deck's bytes or base64 — stdout is capped (8 KB by default)
355
- and the file travels through `outputs`. A build call prints only the slide
356
- count line (e.g. `7 slides`). No "Presentation created successfully", no
357
- try/except warnings on stdout.
358
- - `cannot import name 'RgbColor' from 'pptx.dml.color'` means the name is wrong —
359
- use `RGBColor` (all-caps RGB). Do not catch the ImportError and save anyway.
360
- - Never pass an absolute path to `save()`.
361
- - `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was
362
- collapsed — restore multi-line newlines from the sample and rerun. Underscores
363
- in names (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`
364
- or change the package pin.
365
- - `outputs declare a .pptx but command does not build one` means the source never
366
- calls `Presentation(...).save(...)` — paste the skill sample (edited for content),
367
- not a diagnostic `os.listdir` or shell wrapper.
368
- - `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or
369
- wrong — add `["python-pptx==1.0.2"]` and rerun. Never try to install it.
370
- - `attachment … not found in this chat` means `inputs` listed an id that is not in
371
- this chat (often a copied placeholder like `att_deck`). For a new deck, omit
372
- `inputs` entirely and rerun. Only stage real ids from prior tool results.
373
- - Text crammed into the top-left corner, or a shape with no visible size, means a
374
- bare number reached an argument that required a typed length — python-pptx reads
375
- it as EMU (914400 to the inch), so `add_textbox(0, 0, 12, 0)` is an invisible box
376
- in the corner. Nothing raises and `exitCode` is `0`, so this only ever shows up in
377
- the delivered deck. Wrap every position and size in `Inches(...)` / `Pt(...)` /
378
- `Emu(...)` and rerun — do not tune the raw numbers.
379
- - On an `AttributeError` from python-pptx the API name is wrong, and on a `TypeError`
380
- about missing positional arguments a required argument was left out — fix either
381
- against this file's examples. Do not retry the same call, and do not switch to a shell.
382
- - `AttributeError: 'Slide' object has no attribute 'add_picture'` (or `addpicture`,
383
- `add_textbox`, `addtextbox`) means the call skipped `.shapes` — use
384
- `slide.shapes.add_picture(...)` / `slide.shapes.add_textbox(...)`, never
385
- `slide.add_*`.
386
- - `AttributeError: 'SlideShapes' object has no attribute 'background'` means the
387
- background was reached through the shapes collection — it lives on the slide:
388
- `slide.background.fill.solid()` then `fill.fore_color.rgb = RGBColor(...)`.
389
- - `AttributeError: '_Paragraph' object has no attribute 'fill'` means a fill was
390
- asked of text — paragraphs have none. Color text with
391
- `paragraph.font.color.rgb = RGBColor(...)`; `.fill` belongs to a shape or to
392
- `slide.background`.
393
-
394
- ## Finish
395
-
396
- When `exitCode` is `0` and `attachments` lists the `.pptx`, stop tool use and
397
- answer with one line: file name + the slide count from stdout. Exactly one
398
- successful build `exec` per request — re-running the same build is spam, not
399
- quality.