pi-agnes-tools 0.1.0
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/README.md +62 -0
- package/extensions/agnes-tools.ts +292 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# pi-agnes-tools
|
|
2
|
+
|
|
3
|
+
Pi extension that exposes **Agnes AI** image/video generation as callable tools — no model switch required. Pairs with [`pi-agnes`](https://pi.dev/packages/pi-agnes) for model registration / auth.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install path:/path/to/pi-agnes-tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it registers
|
|
12
|
+
|
|
13
|
+
| Tool | Endpoint | What it does |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `agnes_image` | `POST /v1/images/generations` | Generate an image. Saves to `.pi/generated-images/`. Returns the local path and (possibly expiring) remote URL. |
|
|
16
|
+
| `agnes_video` | `POST /v1/videos` + poll | Generate a video. Polls every 5s (up to 30 min). Saves `.mp4` to `.pi/generated-videos/`. Supports image-to-video (1 ref image) and keyframes (>1 image). |
|
|
17
|
+
|
|
18
|
+
## Parameters
|
|
19
|
+
|
|
20
|
+
### `agnes_image`
|
|
21
|
+
- `prompt` (required): text prompt
|
|
22
|
+
- `model` (default `agnes-image-2.1-flash`): any `agnes-image-*` id
|
|
23
|
+
- `endpoint` (`agnes` | `agnes-cn`, default `agnes`): which Agnes region
|
|
24
|
+
- `images` (optional): array of base64 data URIs for reference/conditioning
|
|
25
|
+
- `response_format` (default `png`)
|
|
26
|
+
|
|
27
|
+
### `agnes_video`
|
|
28
|
+
- `prompt` (required): text prompt
|
|
29
|
+
- `model` (default `agnes-video-2.5-flash`): any `agnes-video-*` id
|
|
30
|
+
- `endpoint` (default `agnes`)
|
|
31
|
+
- `images` (optional): 1 image → image-to-video, >1 → keyframes mode
|
|
32
|
+
- `num_frames` (default 121), `frame_rate` (default 24)
|
|
33
|
+
|
|
34
|
+
## Auth
|
|
35
|
+
|
|
36
|
+
Key resolution order:
|
|
37
|
+
|
|
38
|
+
1. `AGNES_API_KEY` / `AGNES_CN_API_KEY` env vars (same as `pi-agnes`)
|
|
39
|
+
2. `/login`-stored key from `~/.pi/agent/auth.json` (`agnes` provider)
|
|
40
|
+
|
|
41
|
+
| Provider | Endpoint | Env var |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `agnes` | `https://apihub.agnes-ai.com/v1` | `AGNES_API_KEY` |
|
|
44
|
+
| `agnes-cn` | `https://api.agnes-ai.cn/v1` | `AGNES_CN_API_KEY` |
|
|
45
|
+
|
|
46
|
+
## Example (what the LLM sees / can call)
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
// Text-to-image
|
|
50
|
+
{"name": "agnes_image", "arguments": {"prompt": "a watercolor painting of a fox in a misty pine forest"}}
|
|
51
|
+
|
|
52
|
+
// Image-to-video
|
|
53
|
+
{"name": "agnes_video", "arguments": {"prompt": "the fox slowly turns its head", "images": ["data:image/png;base64,..."]}}
|
|
54
|
+
|
|
55
|
+
// Multi-frame keyframes
|
|
56
|
+
{"name": "agnes_video", "arguments": {"prompt": "fly through a canyon", "images": ["data:...frame1", "data:...frame2"], "model": "agnes-video-2.5"}}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- Output paths are project-relative (`.pi/generated-images/`, `.pi/generated-videos/`) — matches where `pi-agnes` model-based generation saves.
|
|
62
|
+
- `executionMode: parallel` for image (fast, stateless), `sequential` for video (long-running task, poll loop).
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-agnes-tools
|
|
3
|
+
*
|
|
4
|
+
* Exposes Agnes AI image/video generation as callable tools, so you don't
|
|
5
|
+
* have to switch models just to generate media.
|
|
6
|
+
*
|
|
7
|
+
* • agnes_image — POST /v1/images/generations (agnes-image-2.1-flash etc.)
|
|
8
|
+
* • agnes_video — POST /v1/videos + poll until done (agnes-video-2.5-flash etc.)
|
|
9
|
+
*
|
|
10
|
+
* Auth: reuses AGNES_API_KEY / AGNES_CN_API_KEY (same env vars as pi-agnes).
|
|
11
|
+
* Saves: .pi/generated-images/ and .pi/generated-videos/ (project-relative).
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
18
|
+
|
|
19
|
+
// typebox: prefer the pi-bundled copy when available (compiled binary / SEA),
|
|
20
|
+
// fall back to a bare import when running as plain ESM (dev / jiti).
|
|
21
|
+
import * as _typebox from "typebox";
|
|
22
|
+
const Type = _typebox.Type;
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Shared helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const ENDPOINTS = {
|
|
29
|
+
agnes: { baseUrl: "https://apihub.agnes-ai.com/v1", apiKeyEnv: "AGNES_API_KEY", authKey: "agnes" },
|
|
30
|
+
"agnes-cn": { baseUrl: "https://api.agnes-ai.cn/v1", apiKeyEnv: "AGNES_CN_API_KEY", authKey: "agnes-cn" },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const IMAGE_MODELS = new Set(["agnes-image-2.0-flash", "agnes-image-2.1-flash"]);
|
|
34
|
+
const VIDEO_MODELS = new Set(["agnes-video-v2.0", "agnes-video-2.5", "agnes-video-2.5-flash"]);
|
|
35
|
+
|
|
36
|
+
function fileLink(p, label = p) {
|
|
37
|
+
return "[" + label + "](" + pathToFileURL(p).href + ")";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveApiKey(endpoint) {
|
|
41
|
+
const cfg = ENDPOINTS[endpoint];
|
|
42
|
+
// 1) Environment variable (fastest, always works)
|
|
43
|
+
const envKey = process.env[cfg.apiKeyEnv];
|
|
44
|
+
if (envKey) return envKey;
|
|
45
|
+
// 2) /login-stored key in pi's auth store
|
|
46
|
+
try {
|
|
47
|
+
const authPath = join(homedir(), ".pi", "agent", "auth.json");
|
|
48
|
+
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
49
|
+
// Primary key: 'agnes' for both endpoints (pi-agnes registers one key per
|
|
50
|
+
// provider id, and the CN endpoint uses the same account/key in most setups).
|
|
51
|
+
const entry = auth[cfg.authKey] || auth["agnes"];
|
|
52
|
+
if (entry && entry.key) return entry.key;
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore read/parse failures; fall through
|
|
55
|
+
}
|
|
56
|
+
throw new Error(
|
|
57
|
+
"No API key found for " + cfg.apiKeyEnv + ". Set the " + cfg.apiKeyEnv + " environment variable, or run /login with the pi-agnes provider."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getEndpoint(endpoint) {
|
|
62
|
+
const cfg = ENDPOINTS[endpoint];
|
|
63
|
+
const apiKey = resolveApiKey(endpoint);
|
|
64
|
+
return { baseUrl: cfg.baseUrl, apiKey, headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkImageModel(model) {
|
|
68
|
+
if (!IMAGE_MODELS.has(model) && !model.startsWith("agnes-image-")) {
|
|
69
|
+
throw new Error("Unknown Agnes image model: " + model + ". Known models: " + [...IMAGE_MODELS].join(", "));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function checkVideoModel(model) {
|
|
74
|
+
if (!VIDEO_MODELS.has(model) && !model.startsWith("agnes-video-")) {
|
|
75
|
+
throw new Error("Unknown Agnes video model: " + model + ". Known models: " + [...VIDEO_MODELS].join(", "));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// agnes_image tool
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
const imageParams = Type.Object({
|
|
84
|
+
prompt: { type: "string", description: "Text prompt describing the image to generate." },
|
|
85
|
+
model: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "Agnes image model id. One of: " + [...IMAGE_MODELS].join(", ") + ". Default: agnes-image-2.1-flash.",
|
|
88
|
+
},
|
|
89
|
+
endpoint: {
|
|
90
|
+
type: "string",
|
|
91
|
+
enum: ["agnes", "agnes-cn"],
|
|
92
|
+
description: "Which Agnes endpoint to use. Default: agnes (apihub.agnes-ai.com).",
|
|
93
|
+
},
|
|
94
|
+
images: {
|
|
95
|
+
type: "array",
|
|
96
|
+
items: { type: "string" },
|
|
97
|
+
description:
|
|
98
|
+
"Optional list of base64-encoded image data URIs (data:<mime>;base64,<data>) to use as reference/conditioning images.",
|
|
99
|
+
},
|
|
100
|
+
response_format: {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Response image format. Default: png.",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
async function executeImage(_toolCallId, params) {
|
|
107
|
+
const prompt = params.prompt;
|
|
108
|
+
const rawModel = params.model || "agnes-image-2.1-flash";
|
|
109
|
+
const endpointId = params.endpoint || "agnes";
|
|
110
|
+
const images = params.images || [];
|
|
111
|
+
const response_format = params.response_format || "png";
|
|
112
|
+
|
|
113
|
+
checkImageModel(rawModel);
|
|
114
|
+
const { baseUrl, headers } = getEndpoint(endpointId);
|
|
115
|
+
|
|
116
|
+
const body = { model: rawModel, prompt, response_format };
|
|
117
|
+
if (images.length > 0) {
|
|
118
|
+
body.image = images;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const response = await fetch(baseUrl + "/images/generations", {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers,
|
|
124
|
+
body: JSON.stringify(body),
|
|
125
|
+
});
|
|
126
|
+
const payload = await response.json().catch(() => null);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error((payload && payload.error && payload.error.message) || "Agnes image API HTTP " + response.status);
|
|
129
|
+
}
|
|
130
|
+
const image = payload && payload.data && payload.data[0];
|
|
131
|
+
if (!image) throw new Error("Agnes image API returned no image data");
|
|
132
|
+
|
|
133
|
+
const directory = join(process.cwd(), ".pi", "generated-images");
|
|
134
|
+
await mkdir(directory, { recursive: true });
|
|
135
|
+
const mime = image.mime_type || "image/png";
|
|
136
|
+
const ext =
|
|
137
|
+
mime.includes("png") ? "png" : mime.includes("jpeg") ? "jpg" : mime.includes("webp") ? "webp" : mime.includes("gif") ? "gif" : "png";
|
|
138
|
+
const filePath = join(directory, rawModel + "-" + Date.now() + "." + ext);
|
|
139
|
+
if (image.b64_json) {
|
|
140
|
+
await writeFile(filePath, Buffer.from(image.b64_json, "base64"));
|
|
141
|
+
} else if (image.url) {
|
|
142
|
+
const imgRes = await fetch(image.url);
|
|
143
|
+
if (!imgRes.ok) throw new Error("Unable to download image: HTTP " + imgRes.status);
|
|
144
|
+
await writeFile(filePath, Buffer.from(await imgRes.arrayBuffer()));
|
|
145
|
+
} else {
|
|
146
|
+
throw new Error("Agnes image API returned no url or b64_json");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const text = image.url
|
|
150
|
+
? "\n\nSaved local copy: " + fileLink(filePath) + "\n\nImage URL may expire according to Agnes retention policy."
|
|
151
|
+
: "Generated image saved to: " + fileLink(filePath);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text }],
|
|
155
|
+
details: { filePath, model: rawModel, endpoint: endpointId, remoteUrl: image.url || null },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// agnes_video tool
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
const videoParams = Type.Object({
|
|
164
|
+
prompt: { type: "string", description: "Text prompt describing the video to generate." },
|
|
165
|
+
model: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: "Agnes video model id. One of: " + [...VIDEO_MODELS].join(", ") + ". Default: agnes-video-2.5-flash.",
|
|
168
|
+
},
|
|
169
|
+
endpoint: {
|
|
170
|
+
type: "string",
|
|
171
|
+
enum: ["agnes", "agnes-cn"],
|
|
172
|
+
description: "Which Agnes endpoint to use. Default: agnes (apihub.agnes-ai.com).",
|
|
173
|
+
},
|
|
174
|
+
images: {
|
|
175
|
+
type: "array",
|
|
176
|
+
items: { type: "string" },
|
|
177
|
+
description: "Optional reference image(s) as base64 data URIs. 1 image = image-to-video; >1 = keyframes mode.",
|
|
178
|
+
},
|
|
179
|
+
num_frames: { type: "integer", description: "Number of frames. Default: 121." },
|
|
180
|
+
frame_rate: { type: "integer", description: "Frames per second. Default: 24." },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
async function pollVideo(baseUrl, videoId, apiKey, signal) {
|
|
184
|
+
const deadline = Date.now() + 30 * 60 * 1000;
|
|
185
|
+
while (Date.now() < deadline) {
|
|
186
|
+
if (signal && signal.aborted) throw new Error("Video generation aborted");
|
|
187
|
+
await new Promise((resolve, reject) => {
|
|
188
|
+
const timer = setTimeout(resolve, 5000);
|
|
189
|
+
if (signal) {
|
|
190
|
+
signal.addEventListener(
|
|
191
|
+
"abort",
|
|
192
|
+
() => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
reject(new Error("Video generation aborted"));
|
|
195
|
+
},
|
|
196
|
+
{ once: true }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const apiRoot = baseUrl.replace(/\/v1\/?$/, "");
|
|
201
|
+
const response = await fetch(apiRoot + "/agnesapi?video_id=" + encodeURIComponent(videoId), {
|
|
202
|
+
headers: { Authorization: "Bearer " + apiKey },
|
|
203
|
+
signal,
|
|
204
|
+
});
|
|
205
|
+
const payload = await response.json().catch(() => null);
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
throw new Error((payload && payload.error && payload.error.message) || "Agnes video status HTTP " + response.status);
|
|
208
|
+
}
|
|
209
|
+
if (payload.status === "completed") return payload;
|
|
210
|
+
if (payload.status === "failed") {
|
|
211
|
+
throw new Error((payload && payload.error && payload.error.message) || "Agnes video generation failed");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Agnes video generation timed out after 30 minutes");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function executeVideo(_toolCallId, params, signal) {
|
|
218
|
+
const prompt = params.prompt;
|
|
219
|
+
const rawModel = params.model || "agnes-video-2.5-flash";
|
|
220
|
+
const endpointId = params.endpoint || "agnes";
|
|
221
|
+
const images = params.images || [];
|
|
222
|
+
const num_frames = params.num_frames || 121;
|
|
223
|
+
const frame_rate = params.frame_rate || 24;
|
|
224
|
+
|
|
225
|
+
checkVideoModel(rawModel);
|
|
226
|
+
const { baseUrl, apiKey, headers } = getEndpoint(endpointId);
|
|
227
|
+
|
|
228
|
+
const body = { model: rawModel, prompt, num_frames, frame_rate };
|
|
229
|
+
if (images.length === 1) body.image = images[0];
|
|
230
|
+
if (images.length > 1) body.extra_body = { image: images, mode: "keyframes" };
|
|
231
|
+
|
|
232
|
+
const response = await fetch(baseUrl + "/videos", {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers,
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
signal,
|
|
237
|
+
});
|
|
238
|
+
const task = await response.json().catch(() => null);
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new Error((task && task.error && task.error.message) || "Agnes video API HTTP " + response.status);
|
|
241
|
+
}
|
|
242
|
+
const videoId = (task && (task.video_id || task.id || task.task_id)) || null;
|
|
243
|
+
if (!videoId) throw new Error("Agnes video API returned no video_id");
|
|
244
|
+
|
|
245
|
+
const result = task.status === "completed" ? task : await pollVideo(baseUrl, videoId, apiKey, signal);
|
|
246
|
+
const url = result && result.metadata && result.metadata.url;
|
|
247
|
+
if (!url) throw new Error("Agnes video API returned no metadata.url");
|
|
248
|
+
|
|
249
|
+
const directory = join(process.cwd(), ".pi", "generated-videos");
|
|
250
|
+
await mkdir(directory, { recursive: true });
|
|
251
|
+
const filePath = join(directory, rawModel + "-" + Date.now() + ".mp4");
|
|
252
|
+
const videoRes = await fetch(url);
|
|
253
|
+
if (!videoRes.ok) throw new Error("Unable to download generated video: HTTP " + videoRes.status);
|
|
254
|
+
await writeFile(filePath, Buffer.from(await videoRes.arrayBuffer()));
|
|
255
|
+
|
|
256
|
+
const text = "Generated video saved to: " + fileLink(filePath) + "\n\nVideo URL: " + url;
|
|
257
|
+
return {
|
|
258
|
+
content: [{ type: "text", text }],
|
|
259
|
+
details: { filePath, model: rawModel, endpoint: endpointId, remoteUrl: url },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Extension entry
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
export default function (pi) {
|
|
268
|
+
pi.registerTool({
|
|
269
|
+
name: "agnes_image",
|
|
270
|
+
label: "Agnes Image",
|
|
271
|
+
description:
|
|
272
|
+
"Generate an image via the Agnes AI API (no model switch needed). " +
|
|
273
|
+
"Saves a local copy under .pi/generated-images/ and returns the saved path plus the (possibly expiring) remote URL. " +
|
|
274
|
+
"Auth: AGNES_API_KEY (default endpoint) or AGNES_CN_API_KEY (endpoint=agnes-cn).",
|
|
275
|
+
parameters: imageParams,
|
|
276
|
+
executionMode: "parallel",
|
|
277
|
+
execute: executeImage,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
pi.registerTool({
|
|
281
|
+
name: "agnes_video",
|
|
282
|
+
label: "Agnes Video",
|
|
283
|
+
description:
|
|
284
|
+
"Generate a video via the Agnes AI API (no model switch needed). " +
|
|
285
|
+
"Async: creates a task, polls every 5s until done (up to 30 min), then downloads the .mp4 to .pi/generated-videos/. " +
|
|
286
|
+
"Supports image-to-video (1 reference image) and keyframes mode (>1 image). " +
|
|
287
|
+
"Auth: AGNES_API_KEY (default endpoint) or AGNES_CN_API_KEY (endpoint=agnes-cn).",
|
|
288
|
+
parameters: videoParams,
|
|
289
|
+
executionMode: "sequential",
|
|
290
|
+
execute: executeVideo,
|
|
291
|
+
});
|
|
292
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-agnes-tools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension that exposes Agnes AI image/video generation as callable tools (no model switch needed). Pairs with pi-agnes for auth.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi",
|
|
8
|
+
"extension",
|
|
9
|
+
"pi-package",
|
|
10
|
+
"agnes",
|
|
11
|
+
"agnes-ai",
|
|
12
|
+
"image-generation",
|
|
13
|
+
"video-generation",
|
|
14
|
+
"tool"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/DraconDev/pi-agnes-tools.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"extensions",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
30
|
+
"typebox": "*"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {},
|
|
33
|
+
"pi": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./extensions"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|