@qvac/skills 0.1.3 → 0.1.5
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.
- package/bundled.js +9 -9
- package/hash.js +1 -1
- package/package.json +1 -1
- package/skills/apple-notes/create-note.applescript +6 -3
- package/skills/apple-notes/references/write.md +9 -0
- package/skills/spotify/SKILL.md +26 -21
- package/skills/weather/SKILL.md +39 -23
- package/skills/word/SKILL.md +37 -43
- package/skills/word/references/create.md +39 -7
- package/skills/word/references/{replace.md → paragraphs.md} +49 -10
- package/skills/word/references/{edit.md → rework.md} +77 -14
- package/skills/word/scripts/list_paragraphs.py +6 -0
package/hash.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Autogenerated by scripts/build.mjs from skills/. Do not edit.
|
|
2
|
-
export const SKILLS_HASH = '
|
|
2
|
+
export const SKILLS_HASH = '8357531791b7cdb8'
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
-- Create a Notes note. argv: name, htmlBody, [folder]
|
|
2
|
-
--
|
|
3
|
-
--
|
|
2
|
+
-- A note's title is its first line. iCloud notes carry no separate `name` and
|
|
3
|
+
-- refuse `set name` (-10006) after the note already exists, which reported a
|
|
4
|
+
-- created note as failed. So the title goes into the body as its heading.
|
|
4
5
|
on run argv
|
|
5
6
|
set noteName to item 1 of argv
|
|
6
7
|
set noteBody to item 2 of argv
|
|
8
|
+
set heading to "<h1>" & noteName & "</h1>"
|
|
9
|
+
if noteBody does not start with heading then set noteBody to heading & noteBody
|
|
7
10
|
tell application "Notes"
|
|
8
11
|
if (count of argv) > 2 then
|
|
9
12
|
set newNote to make new note at folder (item 3 of argv) with properties {body:noteBody}
|
|
10
13
|
else
|
|
11
14
|
set newNote to make new note with properties {body:noteBody}
|
|
12
15
|
end if
|
|
13
|
-
|
|
16
|
+
return id of newNote
|
|
14
17
|
end tell
|
|
15
18
|
end run
|
|
@@ -29,6 +29,15 @@ level so its double quotes need no escaping. Never embed a body inside
|
|
|
29
29
|
`osascript -e '...'`: three nested quoting layers drop the closing `"`/`}` and
|
|
30
30
|
produce `syntax error: Expected "}"` and no note.
|
|
31
31
|
|
|
32
|
+
On success the command prints the new note's id (`x-coredata://…`) and nothing
|
|
33
|
+
else. That id IS the confirmation: the note exists, so answer the user — never
|
|
34
|
+
run the command again to check or "retry". Only a non-zero exit with an
|
|
35
|
+
`execution error` means no note was created.
|
|
36
|
+
|
|
37
|
+
The note's title is its first line, so the script makes sure the body starts
|
|
38
|
+
with `<h1>name</h1>`, adding it when the body does not already begin with it.
|
|
39
|
+
Write the title once as the leading `<h1>` and the script changes nothing.
|
|
40
|
+
|
|
32
41
|
Plain note in the default folder:
|
|
33
42
|
|
|
34
43
|
```bash
|
package/skills/spotify/SKILL.md
CHANGED
|
@@ -22,37 +22,25 @@ metadata:
|
|
|
22
22
|
|
|
23
23
|
# Spotify
|
|
24
24
|
|
|
25
|
-
Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block**.
|
|
25
|
+
Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block**.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
"url": "https://api.spotify.com/v1/search",
|
|
30
|
-
"method": "GET",
|
|
31
|
-
"query": { "q": "Radiohead Creep", "type": "track", "limit": 5 }
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Hard rules
|
|
27
|
+
## Playing a song takes two calls. Always two.
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
- Always search before playing by name. Play carries URIs only in the JSON `body` (`"uris": ["…"]`), never as query parameters.
|
|
39
|
-
- A bare `PUT /me/player/play` with no body only resumes paused playback — it never plays a requested song. For an album/artist/playlist use `{ "context_uri": "<uri>" }` instead of `uris`.
|
|
40
|
-
- Do not ask about tokens or setup up front. A **401** means Spotify isn't connected (tell the user to run `/connect spotify`). A **404** from `/me/player` means no active device (tell them to open Spotify).
|
|
29
|
+
"Play X" is not answered until **both** have run:
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
1. `GET /v1/search` — find the track.
|
|
32
|
+
2. `PUT /v1/me/player/play` — start it.
|
|
43
33
|
|
|
44
|
-
|
|
34
|
+
Search alone plays nothing. If you have searched and not yet called play, you are not finished: make the play call now.
|
|
45
35
|
|
|
46
36
|
```json
|
|
47
37
|
{
|
|
48
38
|
"url": "https://api.spotify.com/v1/search",
|
|
49
39
|
"method": "GET",
|
|
50
|
-
"query": { "q": "Radiohead Creep", "type": "track", "limit":
|
|
40
|
+
"query": { "q": "Radiohead Creep", "type": "track", "limit": 3, "market": "from_token" }
|
|
51
41
|
}
|
|
52
42
|
```
|
|
53
43
|
|
|
54
|
-
2. Copy `tracks.items[0].uri` into the play body:
|
|
55
|
-
|
|
56
44
|
```json
|
|
57
45
|
{
|
|
58
46
|
"url": "https://api.spotify.com/v1/me/player/play",
|
|
@@ -61,6 +49,20 @@ Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential
|
|
|
61
49
|
}
|
|
62
50
|
```
|
|
63
51
|
|
|
52
|
+
Take `tracks.items[0].uri` from the search response and paste it into `uris`. A 204 means it started.
|
|
53
|
+
|
|
54
|
+
## Hard rules
|
|
55
|
+
|
|
56
|
+
- **`q` carries every word the user named — the title and the artist.** "play Creep by Radiohead" searches `q=Radiohead Creep`, never `q=Creep`. Drop the artist and the top hit is a different band's song with the same title.
|
|
57
|
+
- **Before playing, check the item you picked.** Compare its `artists[0].name` with the artist the user named. If they do not match, take the first result that does. A title match under the wrong artist is the wrong song, and the user hears it immediately.
|
|
58
|
+
- **Never write a URI, a JSON block, or "I'll play it now" to the user in place of calling play.** Describing the call is not making it.
|
|
59
|
+
- **Every URI you send is one you copied from a search response in this turn.** Never type a `spotify:track:` id from memory or from an earlier turn. A well-formed id that is not real stops what was playing and starts nothing.
|
|
60
|
+
- **A track goes in `uris`. Only `uris`.** `context_uri` takes an album, artist or playlist URI — a track URI there plays nothing. URIs go in the JSON `body`, never in query parameters.
|
|
61
|
+
- **One call per intent.** A 204 means the call landed; do not send it again. Repeating a queue or play call burns the turn and changes nothing.
|
|
62
|
+
- **Name what actually played, read back from the item you used** — its `name` and `artists[0].name`. A 204 says the call was accepted, not which song it was, so never report a title you did not read out of the response.
|
|
63
|
+
- A bare song/artist/album/playlist name is enough — search immediately, take the top match, and tell the user what you picked. Do not ask "which one?" before searching.
|
|
64
|
+
- Do not ask about tokens or setup up front. A **401** means Spotify isn't connected (tell the user to run `/connect spotify`). A **404** from `/me/player` means no active device (tell them to open Spotify).
|
|
65
|
+
|
|
64
66
|
## Other operations
|
|
65
67
|
|
|
66
68
|
All paths are under `https://api.spotify.com/v1`.
|
|
@@ -69,17 +71,20 @@ All paths are under `https://api.spotify.com/v1`.
|
|
|
69
71
|
| --------------- | ----------------------------------------------------------------------- |
|
|
70
72
|
| What's playing? | `GET /me/player/currently-playing` |
|
|
71
73
|
| Pause | `PUT /me/player/pause` |
|
|
72
|
-
| Resume | `PUT /me/player/play` (no body)
|
|
74
|
+
| Resume | `PUT /me/player/play` (no body — resumes only, never starts a new song) |
|
|
73
75
|
| Next track | `POST /me/player/next` |
|
|
74
76
|
| Add to queue | `POST /me/player/queue` with `query`: `{ "uri": "spotify:track:<id>" }` |
|
|
77
|
+
| Play an album/artist/playlist | `PUT /me/player/play` with `body`: `{ "context_uri": "<uri>" }` |
|
|
75
78
|
| My playlists | `GET /me/playlists` |
|
|
76
79
|
| Top tracks | `GET /me/top/tracks` with `query`: `{ "time_range": "medium_term" }` |
|
|
77
80
|
| Recently played | `GET /me/player/recently-played` |
|
|
78
81
|
| List devices | `GET /me/player/devices` |
|
|
79
82
|
|
|
83
|
+
Queueing is the same two calls as playing: search for the track, then `POST /me/player/queue` with the `uri` you just read. Queueing does not start playback.
|
|
84
|
+
|
|
80
85
|
## Notes
|
|
81
86
|
|
|
82
|
-
- Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at
|
|
87
|
+
- Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at 3 and **always pass `market`**: a search without it spends most of the budget on `available_markets`, and the results behind the first one are cut off before you can read them. `/search` has no `fields` param, so `market` is the only lever there. On playlist/track endpoints request only what you need with `fields` (e.g. `query`: `{ "fields": "items(track(name,artists(name),uri))" }`). Read just the top item unless the user asked for a list.
|
|
83
88
|
- Present results as a short numbered list — track, artist, album, duration — and devices as `1. Name (active/idle)`. Never dump raw JSON to the user.
|
|
84
89
|
- Playback commands return **204 No Content** on success (empty body). A **204** from currently-playing means nothing is playing.
|
|
85
90
|
- **403** with `PREMIUM_REQUIRED` → user needs Spotify Premium. Other **403**s are usually app-scope/Development Mode limits — report and stop.
|
package/skills/weather/SKILL.md
CHANGED
|
@@ -1,43 +1,59 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: weather
|
|
3
3
|
description: Get current weather and short forecasts for cities via wttr.in.
|
|
4
|
-
tools: [
|
|
4
|
+
tools: [weather_lookup]
|
|
5
5
|
platform: [darwin, linux, win32, ios, android]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
# v2: few-shot table. 2 repeats: overall 51/132 -> 77/130, "Nassau, Bahamas" 1/4 -> 4/4, city+country 2/24 -> 18/24,
|
|
10
|
-
# ambiguous city 0/20 -> 6/20; 4B control 41/66 -> 59/66. Rules-first layouts lost on suffix slips (?3, ?1T).
|
|
6
|
+
version: 4
|
|
7
|
+
# Tuned on Qwen3.5-2B with an offline eval (35 prompts, mention route), QVAC-24701.
|
|
8
|
+
# v2: few-shot table. 2 repeats: overall 51/132 -> 77/130, "Nassau, Bahamas" 1/4 -> 4/4, city+country 2/24 -> 18/24.
|
|
11
9
|
# v3: + country row. 3 repeats: v2 110/198 -> v3 121/198 (city+country 25 -> 31/36, forecast 17 -> 23/30).
|
|
12
|
-
# Aliases, a second ambiguity row
|
|
10
|
+
# Aliases, a second ambiguity row and dropping ?T were tried and did not help (73, 74, 54 of ~130).
|
|
11
|
+
# v4: weather_lookup instead of http_request, after v3 answered a 200 for a place that does not exist.
|
|
12
|
+
# 5 repeats, all variants in one sweep so they share a baseline: v3 97/171 -> v4 120/169. city+country
|
|
13
|
+
# 87 -> 93%, unambiguous 57 -> 91%, unknown 57 -> 84%, forecast 68 -> 88%, casual 65 -> 80%, the ticket
|
|
14
|
+
# prompt 80 -> 100%. Context is a wash (2335 -> 2378) though the body is 387 bytes smaller: the tool owns
|
|
15
|
+
# the URL, so three rules about spelling one went away.
|
|
16
|
+
# Not fixed. "Ask which Nassau" is 3/25 on v3 and 0/25 here, and asking when no place is named is 1/10 for
|
|
17
|
+
# both: a one-argument call is easy, so the model calls rather than asks. Rules that tell the model not to
|
|
18
|
+
# act have never cleared ~17% on a 2B in four versions, and a variant that made the tool refuse an
|
|
19
|
+
# ambiguous name measured 13% against 17%, so it was dropped rather than shipped.
|
|
13
20
|
---
|
|
14
21
|
|
|
15
22
|
# Weather
|
|
16
23
|
|
|
17
|
-
One `
|
|
24
|
+
One `weather_lookup` call, then answer from the result. Always call it exactly like this:
|
|
18
25
|
|
|
19
26
|
```json
|
|
20
|
-
{ "
|
|
27
|
+
{ "location": "Nassau, Bahamas" }
|
|
21
28
|
```
|
|
22
29
|
|
|
23
|
-
Match the user's request to a row for the
|
|
30
|
+
Match the user's request to a row for the call:
|
|
24
31
|
|
|
25
32
|
| User asks | Call |
|
|
26
33
|
| --- | --- |
|
|
27
|
-
| "weather
|
|
28
|
-
| "weather in
|
|
29
|
-
| "
|
|
30
|
-
| "
|
|
31
|
-
| "
|
|
32
|
-
| "
|
|
33
|
-
| "
|
|
34
|
+
| "what's the weather?" — no place named | **no call.** Ask which city |
|
|
35
|
+
| "weather in Nassau, Bahamas" | `{ "location": "Nassau, Bahamas" }` |
|
|
36
|
+
| "weather in London" / "London today" | `{ "location": "London" }` |
|
|
37
|
+
| "Berlin tomorrow" / "this weekend" | `{ "location": "Berlin" }` |
|
|
38
|
+
| "Rome for the next 3 days" | `{ "location": "Rome" }` |
|
|
39
|
+
| "how hot is it in Georgia, the country" | `{ "location": "Tbilisi, Georgia" }` (a country → its capital) |
|
|
40
|
+
| "weather in Nassau" | `{ "location": "Nassau" }` — the result names both, ask which |
|
|
41
|
+
|
|
42
|
+
The result is the place on the first line, then the weather now, then one line per day:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Nassau, New Providence, Bahamas
|
|
46
|
+
Now: 29°C / 84°F, Patchy rain nearby, feels like 33°C, humidity 71%, wind 21 km/h
|
|
47
|
+
2026-09-16 (today): 29-29°C / 84-85°F, Partly Cloudy
|
|
48
|
+
2026-09-17 (tomorrow): 28-29°C / 83-85°F, Moderate or heavy rain shower
|
|
49
|
+
```
|
|
34
50
|
|
|
35
51
|
## Rules
|
|
36
52
|
|
|
37
|
-
1. **
|
|
38
|
-
2. **
|
|
39
|
-
3. **
|
|
40
|
-
4. **
|
|
41
|
-
5.
|
|
53
|
+
1. **There is no default place.** If the user named none, do not call: ask which city. Never use a place from this file.
|
|
54
|
+
2. **The location is exactly what the user wrote.** Keep a country or state they gave (`Nassau, Bahamas`, not `Nassau`). Never add one they did not give. A country → its capital (`Tbilisi`).
|
|
55
|
+
3. **Name the place from the first line of the result, not the words the user used.** If they ask for Rome and the first line says `Lome, Maritime, Togo`, say it is Lome in Togo.
|
|
56
|
+
4. **If the result is not a weather report, say what it says, in those words.** `No such place: "Xyzzyville, Atlantis"` → tell the user that place does not exist and ask for a real one. Do not look it up again under another name, and never state a temperature you did not receive.
|
|
57
|
+
5. Forecasts stop at 3 days: for "next week" say so and give the 3 days you have.
|
|
42
58
|
|
|
43
|
-
|
|
59
|
+
Answer in one plain sentence with the temperature and the condition.
|
package/skills/word/SKILL.md
CHANGED
|
@@ -19,43 +19,35 @@ metadata:
|
|
|
19
19
|
|
|
20
20
|
# Word
|
|
21
21
|
|
|
22
|
-
Build,
|
|
23
|
-
|
|
24
|
-
`
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
Build, change, or read `.docx` documents. This file holds no Python and no
|
|
23
|
+
recipe: it only says which reference file to load. Load exactly one with the
|
|
24
|
+
`skill` tool, then do what that file says.
|
|
25
|
+
|
|
26
|
+
## Which File to Load
|
|
27
|
+
|
|
28
|
+
Pick the row by **what the user wants done**, then make that exact `skill`
|
|
29
|
+
call. "Edit", "update", "modify", "change", "replace", "rewrite" and "fix" all
|
|
30
|
+
mean the same thing here — the verb never picks the row, the change does.
|
|
31
|
+
|
|
32
|
+
| The user wants | The `skill` call |
|
|
33
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
|
34
|
+
| A new document — "write a report", "make a doc with 30 fun facts about cats", "draft a letter" | `{"name": "word", "file": "references/create.md"}` |
|
|
35
|
+
| Some paragraphs of an existing document changed — "replace the first 10 facts with dog facts", "change fact 3", "reword paragraph 7", "swap these bullets for those" | `{"name": "word", "file": "references/paragraphs.md"}` |
|
|
36
|
+
| Anything else done to an existing document — add a section, remove or rewrite a whole section, make the text bigger, put an image in it | `{"name": "word", "file": "references/rework.md"}` |
|
|
37
|
+
| An answer in the chat from an attached document — "summarize this", "what does it say about X" | `{"name": "word", "file": "references/read.md"}` |
|
|
38
|
+
|
|
39
|
+
- A document that already exists in this chat is never rebuilt with
|
|
40
|
+
`create.md` — that throws away everything the user has. Its `attachmentId`
|
|
41
|
+
is in the `exec` result that produced it or on the `[Attached file …]` line.
|
|
42
|
+
- A summary delivered as a file is `read.md` first, then `create.md`.
|
|
43
|
+
- `paragraphs.md` runs two scripts bundled with this skill and contains no
|
|
44
|
+
Python. `rework.md` and `create.md` carry the python-docx recipes to copy.
|
|
33
45
|
|
|
34
46
|
Each load is a real `skill` tool call — printing the call as JSON or text in
|
|
35
|
-
your reply loads nothing.
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
`file: "references/create.md"`.
|
|
40
|
-
- **Changing some of the facts, points, bullets, items, or paragraphs** of an
|
|
41
|
-
existing `.docx` — "replace the first 10 facts", "change fact 3", "swap the
|
|
42
|
-
bullets for these", "reword paragraph 7": call the `skill` tool with
|
|
43
|
-
`name: "word"` and `file: "references/replace.md"`. This is the file even
|
|
44
|
-
when the user says edit, replace, change, update, or rewrite; it runs two
|
|
45
|
-
bundled scripts and no Python is written.
|
|
46
|
-
- **Any other edit of an existing document** (extend it, trim it, rework a
|
|
47
|
-
whole section, resize the text, embed an image into it): call the `skill`
|
|
48
|
-
tool with `name: "word"` and `file: "references/edit.md"`.
|
|
49
|
-
- **Reading a document to answer in chat** (a summary, a question answered,
|
|
50
|
-
content pulled out — no file delivered): call the `skill` tool with
|
|
51
|
-
`name: "word"` and `file: "references/read.md"`.
|
|
52
|
-
- **A summary delivered as a file** is a read followed by a build: load both
|
|
53
|
-
`references/read.md` and `references/create.md`.
|
|
54
|
-
|
|
55
|
-
Never write the Python from memory. The recipes carry rules (exact version
|
|
56
|
-
pins, attachment staging, run-level formatting, in-place replacement, the only
|
|
57
|
-
working removal idiom) that fail in non-obvious ways when improvised; loading
|
|
58
|
-
the file is one cheap read-only call.
|
|
47
|
+
your reply loads nothing. Never write the Python from memory: the recipes carry
|
|
48
|
+
rules (exact version pins, attachment staging, the only working removal idiom)
|
|
49
|
+
that fail in non-obvious ways when improvised, and loading the file is one
|
|
50
|
+
cheap read-only call.
|
|
59
51
|
|
|
60
52
|
## When to Use
|
|
61
53
|
|
|
@@ -106,13 +98,15 @@ no-`outputs` read that precedes a build delivers nothing and is not one of
|
|
|
106
98
|
them, but it belongs before the build, never after it. Reply with a single
|
|
107
99
|
line: file name + the count line from stdout. If the result has
|
|
108
100
|
`missingOutputs` instead, the file was never written: read stderr first — an
|
|
109
|
-
`AssertionError` there means a guard stopped the save on purpose (see the
|
|
110
|
-
recipe); only when stderr is clean check the `save()` name matches the
|
|
101
|
+
`AssertionError` there means a guard stopped the save on purpose (see the
|
|
102
|
+
rework recipe); only when stderr is clean check the `save()` name matches the
|
|
111
103
|
declared output and rerun once.
|
|
112
104
|
|
|
113
105
|
**Failures are fixed in the code, not around it.** An error in your code is
|
|
114
106
|
never a fault in python-docx or in the runtime; fix the Python against the
|
|
115
|
-
loaded reference file's recipes and Errors and call `exec` again.
|
|
107
|
+
loaded reference file's recipes and Errors and call `exec` again. A bundled
|
|
108
|
+
script that stops with a message is fixed by correcting its arguments and
|
|
109
|
+
rerunning the same script — never by writing Python in its place. If two
|
|
116
110
|
consecutive calls fail with the same error, re-read the traceback
|
|
117
111
|
line-by-line before a third — retrying the identical `command`, or a version
|
|
118
112
|
with only cosmetic changes, is a loop, not a fix. Do not switch package pins
|
|
@@ -130,12 +124,12 @@ and a file you write but do not declare in `outputs` is discarded. The `exec`
|
|
|
130
124
|
result is the only account of what happened — there is no filesystem to check
|
|
131
125
|
and no shell to check it with.
|
|
132
126
|
|
|
133
|
-
**Never overwrite a staged input.**
|
|
134
|
-
derived from the document
|
|
127
|
+
**Never overwrite a staged input.** Changes always save a new output name,
|
|
128
|
+
derived from the document changed — `report.docx` becomes `report_revised.docx`,
|
|
135
129
|
never a fresh name taken from the new content.
|
|
136
130
|
|
|
137
|
-
**
|
|
131
|
+
**A change happens inside the document.** `add_paragraph` and `add_heading`
|
|
138
132
|
append at the end and nowhere else, so replacing content that is already there
|
|
139
133
|
means rewriting those paragraphs, not adding new ones. Delivering the original
|
|
140
|
-
with the new version appended
|
|
141
|
-
|
|
134
|
+
with the new version appended, or a fresh document holding only the new
|
|
135
|
+
content, is a failed turn.
|
|
@@ -5,11 +5,12 @@ A new document needs **no** `inputs` — do not invent attachment ids — unless
|
|
|
5
5
|
it embeds an image (see Embedding Images). **Exactly one** `exec` call per
|
|
6
6
|
user request when that call succeeds.
|
|
7
7
|
|
|
8
|
-
**A document that already exists in this chat is never rebuilt here.** "
|
|
9
|
-
|
|
10
|
-
existing `.docx` is
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
**A document that already exists in this chat is never rebuilt here.** "Replace
|
|
9
|
+
the first 10 facts", "reword this", "add a section" — any request that starts
|
|
10
|
+
from an existing `.docx` is a change to that document: some of its paragraphs
|
|
11
|
+
is `references/paragraphs.md`, anything else is `references/rework.md`, and
|
|
12
|
+
either one stages the document by its `attachmentId`. Building a fresh
|
|
13
|
+
document for such a request throws away everything the user already has.
|
|
13
14
|
|
|
14
15
|
## The exec call
|
|
15
16
|
|
|
@@ -81,9 +82,40 @@ through other tools — that is a dead end. Say the link cannot be downloaded
|
|
|
81
82
|
and ask the user to attach the image itself, or offer `generate_image` for a
|
|
82
83
|
similar visual. Then build the document with the staged attachment as above.
|
|
83
84
|
|
|
85
|
+
## Which Shape
|
|
86
|
+
|
|
87
|
+
| The user asks for | Shape |
|
|
88
|
+
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| "30 fun facts about cats", "10 tips for …", "a list of …", any number of items or points | **List** — a title, then exactly N `List Bullet` paragraphs and nothing else: no intro sentence, no section headings, no numbers typed into the text |
|
|
90
|
+
| a report, memo, letter, plan — anything with sections | **Report** — the recipe under The Recipe below |
|
|
91
|
+
|
|
92
|
+
### The list shape
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from docx import Document
|
|
96
|
+
|
|
97
|
+
doc = Document()
|
|
98
|
+
doc.add_heading("30 Fun Facts About Cats", level=0)
|
|
99
|
+
facts = [
|
|
100
|
+
"Cats sleep for about 70 percent of their lives.",
|
|
101
|
+
"A group of cats is called a clowder.",
|
|
102
|
+
"A cat's nose print is unique, like a fingerprint.",
|
|
103
|
+
] # one plain string per item — write all N here
|
|
104
|
+
for fact in facts:
|
|
105
|
+
doc.add_paragraph(fact, style="List Bullet")
|
|
106
|
+
doc.save("cat_facts.docx") # must match the declared output exactly
|
|
107
|
+
print(f"{len(facts)} items, {len(doc.paragraphs)} paragraphs, {len(doc.tables)} table(s)")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
One string per item, as many as the user asked for. No headings between
|
|
111
|
+
groups of items and no introductory sentence: each of those is a paragraph the
|
|
112
|
+
user did not ask for, and a later "change the first 10 items" then lands on the
|
|
113
|
+
wrong lines. The count line it prints is the reply — a delivered list is done,
|
|
114
|
+
whatever the count says; never rebuild it to fix the number.
|
|
115
|
+
|
|
84
116
|
## The Recipe
|
|
85
117
|
|
|
86
|
-
Start from this. It is a complete, working document — a title, headings,
|
|
118
|
+
Start from this for a report. It is a complete, working document — a title, headings,
|
|
87
119
|
paragraphs with bold and italic runs, a bulleted list, and a table — saved
|
|
88
120
|
under the declared output name. Copy it and change the content; do not
|
|
89
121
|
assemble a document from memory.
|
|
@@ -255,7 +287,7 @@ A **new** `Document()` ships these styles — safe to use without checking:
|
|
|
255
287
|
`Title`, `Heading 1` … `Heading 9`, `Normal`, `List Bullet` (+ ` 2`, ` 3`),
|
|
256
288
|
`List Number` (+ ` 2`, ` 3`), `Intense Quote`, and the table style `Table Grid`.
|
|
257
289
|
Do not invent other names for a new document. (An uploaded document carries
|
|
258
|
-
only its own styles — when editing one, load `references/
|
|
290
|
+
only its own styles — when editing one, load `references/rework.md` for the
|
|
259
291
|
guard.)
|
|
260
292
|
|
|
261
293
|
## Headings and lists
|
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Changing Some Paragraphs of a Document (bundled scripts)
|
|
2
2
|
|
|
3
|
-
"Replace the first 10 facts", "
|
|
4
|
-
"
|
|
5
|
-
skill. **Write no Python.**
|
|
6
|
-
`skill`, `script`, and `scriptArgs` exactly as
|
|
7
|
-
document by its `attachmentId` (from the
|
|
8
|
-
`[Attached file …]` line — copy it verbatim, never
|
|
3
|
+
"Replace the first 10 facts", "change fact 3", "swap these bullets for those",
|
|
4
|
+
"reword paragraph 7": two `exec` calls, both running a script bundled with this
|
|
5
|
+
skill. **Write no Python.** There is no `command` in this job — a call with
|
|
6
|
+
`command` is the wrong call. Pass `skill`, `script`, and `scriptArgs` exactly as
|
|
7
|
+
shown, with `inputs` staging the document by its `attachmentId` (from the
|
|
8
|
+
earlier `exec` result or the `[Attached file …]` line — copy it verbatim, never
|
|
9
|
+
invent one).
|
|
10
|
+
|
|
11
|
+
| Step | The `exec` call |
|
|
12
|
+
| --------------------------------------- | ---------------------------------------------------------------- |
|
|
13
|
+
| 1. see the paragraphs and their indexes | `scripts/list_paragraphs.py`, `inputs` staged, no `outputs` |
|
|
14
|
+
| 2. replace exactly the chosen indexes | `scripts/replace_paragraphs.py`, `inputs` staged, one `outputs` |
|
|
15
|
+
|
|
16
|
+
## Step 0 — find the document's `attachmentId`
|
|
17
|
+
|
|
18
|
+
The id is in the chat already, never invented: a document built earlier in
|
|
19
|
+
this chat has it in the `attachments` of the `exec` result that produced it —
|
|
20
|
+
`{"attachmentId":"922bd4e17517b90593be1c5ae4f12fbd","fileName":"cat_facts.docx"}`
|
|
21
|
+
— and a document the user uploaded has it on the `[Attached file …]` line of
|
|
22
|
+
their message. Copy that exact id into `inputs`. An `inputs` entry with a
|
|
23
|
+
`path` and no `attachmentId` is an _image_ upload and is refused for a
|
|
24
|
+
document:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{ "inputs": [{ "path": "existing.docx" }] }
|
|
28
|
+
```
|
|
9
29
|
|
|
10
30
|
## Step 1 — list the paragraphs (no `outputs`)
|
|
11
31
|
|
|
@@ -20,6 +40,10 @@ document by its `attachmentId` (from the earlier `exec` result or the
|
|
|
20
40
|
}
|
|
21
41
|
```
|
|
22
42
|
|
|
43
|
+
Every key above is required — `inputs` with the document's real
|
|
44
|
+
`attachmentId`, `skill`, `script`, `scriptArgs`. A call missing `skill` or
|
|
45
|
+
`inputs` is refused.
|
|
46
|
+
|
|
23
47
|
It prints one line per paragraph — `index`, style, text — then a count line.
|
|
24
48
|
Pick the indexes to replace from that list:
|
|
25
49
|
|
|
@@ -27,7 +51,7 @@ Pick the indexes to replace from that list:
|
|
|
27
51
|
points, or bullets. `Title`, `Heading N`, and an intro sentence are never
|
|
28
52
|
counted as one.
|
|
29
53
|
- "The first 10 facts" = the first 10 body-paragraph indexes after the heading
|
|
30
|
-
that introduces them — not indexes 0–9.
|
|
54
|
+
or sentence that introduces them — not indexes 0–9.
|
|
31
55
|
- Fewer facts in the document than asked for: replace the ones that exist and
|
|
32
56
|
say so in the reply.
|
|
33
57
|
|
|
@@ -57,6 +81,12 @@ start + 9.
|
|
|
57
81
|
}
|
|
58
82
|
```
|
|
59
83
|
|
|
84
|
+
Wrong, for this job — a `command` instead of a `script`:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{ "command": "from docx import Document\ndoc = Document(\"existing.docx\")\nfor i in range(1, 11): ..." }
|
|
88
|
+
```
|
|
89
|
+
|
|
60
90
|
Each new text is one complete plain sentence, no markdown, each different. The
|
|
61
91
|
script keeps each paragraph's paragraph style, refuses a heading index,
|
|
62
92
|
refuses text that already reads the same, and prints
|
|
@@ -74,6 +104,10 @@ rerun the **same script** — never switch to writing Python.
|
|
|
74
104
|
|
|
75
105
|
- `usage: replace_paragraphs.py …` — the pairs are incomplete: after the two
|
|
76
106
|
file names, arguments alternate `index`, `text`.
|
|
107
|
+
- `scriptArgs name "existing_revised.docx" but the working directory starts
|
|
108
|
+
empty` — the call has no `outputs`; add `"outputs": ["existing_revised.docx"]`
|
|
109
|
+
(the same name as in `scriptArgs`) and rerun the same script.
|
|
110
|
+
- `script runs need the owning skill name in skill` — add `"skill": "word"`.
|
|
77
111
|
- `index N is the heading '…'` — that paragraph is a heading, not a fact. Pick
|
|
78
112
|
body indexes from the Step 1 list.
|
|
79
113
|
- `index N is outside the document's M paragraphs` — re-read the Step 1 list;
|
|
@@ -82,5 +116,10 @@ rerun the **same script** — never switch to writing Python.
|
|
|
82
116
|
a different sentence.
|
|
83
117
|
- `output … must be a new name` — the output name equals the input's; use
|
|
84
118
|
`existing_revised.docx`.
|
|
85
|
-
- `
|
|
86
|
-
|
|
119
|
+
- `an id-less input stages an uploaded image` — the `inputs` entry has no
|
|
120
|
+
`attachmentId`; add the document's id from Step 0 and rerun the same script.
|
|
121
|
+
- `usage: list_paragraphs.py <input.docx>` — `scriptArgs` was left out; pass
|
|
122
|
+
the staged path, `["existing.docx"]`.
|
|
123
|
+
- `PackageNotFoundError` / `attachment … not found` / `does not exist in the
|
|
124
|
+
working directory` — `inputs` is missing or carries an invented id; stage the
|
|
125
|
+
document by its real `attachmentId`.
|
|
@@ -1,16 +1,79 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
**Stop here if the request changes some of the facts, points, bullets, items,
|
|
4
|
-
or paragraphs** — "replace the first 10 facts", "change fact 3", "swap the
|
|
5
|
-
bullets", "reword paragraph 7". That job is `references/replace.md`: call the
|
|
6
|
-
`skill` tool with `name: "word"` and `file: "references/replace.md"` now, and
|
|
7
|
-
do not use anything in this file for it. No Python is written for that job.
|
|
1
|
+
# Reworking an Existing Word Document (python-docx)
|
|
8
2
|
|
|
9
3
|
Change, replace, extend, trim, or rework a `.docx` that is already in this
|
|
10
4
|
chat by running Python through the `exec` tool: stage it as an input, modify
|
|
11
5
|
paragraphs and tables, and save a **new** output such as
|
|
12
6
|
`existing_revised.docx`. Never overwrite the staged input.
|
|
13
7
|
|
|
8
|
+
## Replacing Some Facts, Points, Bullets or Paragraphs: Two Script Calls
|
|
9
|
+
|
|
10
|
+
"Replace the first 10 facts", "change fact 3", "swap these bullets for those",
|
|
11
|
+
"reword paragraph 7" — any request that changes some of the paragraphs and
|
|
12
|
+
keeps the rest — is two `exec` calls that run scripts bundled with this skill.
|
|
13
|
+
**Write no Python for it.** Nothing else in this file applies to that job: no
|
|
14
|
+
`command`, no `Document(...)`, no fingerprint, no loop. `references/paragraphs.md`
|
|
15
|
+
is this same recipe with its error table.
|
|
16
|
+
|
|
17
|
+
The document's `attachmentId` is already in the chat — in the `attachments` of
|
|
18
|
+
the `exec` result that produced it, or on the user's `[Attached file …]` line.
|
|
19
|
+
Copy it into `inputs`; an entry with only a `path` stages an image, not a
|
|
20
|
+
document.
|
|
21
|
+
|
|
22
|
+
Step 1 — list the paragraphs (no `outputs`):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"language": "python",
|
|
27
|
+
"packages": ["python-docx==1.2.0"],
|
|
28
|
+
"inputs": [{ "attachmentId": "<real id>", "path": "existing.docx" }],
|
|
29
|
+
"skill": "word",
|
|
30
|
+
"script": "scripts/list_paragraphs.py",
|
|
31
|
+
"scriptArgs": ["existing.docx"]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It prints one line per paragraph — `index`, style, text — then a count line.
|
|
36
|
+
Only body paragraphs (`Normal`, `List Bullet`, `List Number`) are facts, points,
|
|
37
|
+
or bullets; `Title`, `Heading N`, and an intro sentence are never counted as
|
|
38
|
+
one. "The first 10 facts" = the first 10 body-paragraph indexes after the
|
|
39
|
+
heading or sentence that introduces them — not indexes 0–9.
|
|
40
|
+
|
|
41
|
+
Step 2 — replace exactly those paragraphs (one `outputs` entry). `scriptArgs`
|
|
42
|
+
is the input name, the output name, then one `index, new text` pair per
|
|
43
|
+
replaced paragraph — "the first 10 facts" is 10 pairs, each a different
|
|
44
|
+
complete sentence:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"language": "python",
|
|
49
|
+
"packages": ["python-docx==1.2.0"],
|
|
50
|
+
"inputs": [{ "attachmentId": "<real id>", "path": "existing.docx" }],
|
|
51
|
+
"outputs": ["existing_revised.docx"],
|
|
52
|
+
"skill": "word",
|
|
53
|
+
"script": "scripts/replace_paragraphs.py",
|
|
54
|
+
"scriptArgs": [
|
|
55
|
+
"existing.docx", "existing_revised.docx",
|
|
56
|
+
"3", "Dogs have about 1,700 taste buds.",
|
|
57
|
+
"4", "A dog's nose print is unique, like a fingerprint."
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Wrong, for this job — a `command` instead of a `script`:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{ "command": "from docx import Document\ndoc = Document(\"existing.docx\")\nfor i in range(1, 11): ..." }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`exitCode 0` plus an attachment = done: reply with the file name and the
|
|
69
|
+
printed `K of N paragraphs replaced`, and do not call `exec` again. A script
|
|
70
|
+
error names the fix (a heading index, an index out of range, unchanged text,
|
|
71
|
+
missing pairs, a missing `outputs` for the revised name); correct the
|
|
72
|
+
arguments and rerun the **same script**.
|
|
73
|
+
|
|
74
|
+
Everything below is for the other edits: extending a document, trimming it,
|
|
75
|
+
rewriting a whole section, resizing its text, embedding an image.
|
|
76
|
+
|
|
14
77
|
## Staging the Document
|
|
15
78
|
|
|
16
79
|
Stage the document as an input **by its `attachmentId`** and open it with
|
|
@@ -163,9 +226,9 @@ formatting-loss rule below.
|
|
|
163
226
|
The `add_heading`/`add_paragraph` pair above appends an **Appendix** because
|
|
164
227
|
that is what the sample edit asks for. Copy that shape only when the user
|
|
165
228
|
genuinely wants new content at the end. Substituting content that is already
|
|
166
|
-
in the document
|
|
167
|
-
|
|
168
|
-
|
|
229
|
+
in the document is a different job with its own guards: some of the points —
|
|
230
|
+
"change the first five points" — is the two script calls at the top of this
|
|
231
|
+
file; a whole section — "rewrite section 2" — is Rewriting Whole Sections.
|
|
169
232
|
|
|
170
233
|
When an edit adds substantial new content — new sections, formatted runs,
|
|
171
234
|
bulleted lists, whole tables — the writing rules apply unchanged: load
|
|
@@ -193,11 +256,11 @@ bullet = "List Bullet" if "List Bullet" in names else None
|
|
|
193
256
|
doc.add_paragraph("point one", style=bullet) # style=None → Normal
|
|
194
257
|
```
|
|
195
258
|
|
|
196
|
-
##
|
|
259
|
+
## Rewriting Whole Sections
|
|
197
260
|
|
|
198
|
-
This recipe is for whole
|
|
199
|
-
of the facts, points, bullets, or paragraphs is
|
|
200
|
-
never hand-write a loop for that.
|
|
261
|
+
This recipe is for whole _sections_ (a heading plus its body). Changing some
|
|
262
|
+
of the facts, points, bullets, or paragraphs is the two script calls at the
|
|
263
|
+
top of this file — never hand-write a loop for that.
|
|
201
264
|
|
|
202
265
|
`add_paragraph`, `add_heading`, and `add_picture` **always append at the end of
|
|
203
266
|
the document.** None of them takes a position. "Change the first five points",
|