@zhexin/pi-deepseek 0.0.2 → 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 +1 -1
- package/package.json +1 -1
- package/src/index.ts +129 -11
- package/src/schema.ts +46 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
DeepSeek Enhancement Extension for the [Pi](https://pi.dev) coding agent.
|
|
4
4
|
|
|
5
|
-
Include **web search/fetch** tools. Nothing more.
|
|
5
|
+
Include **web search/fetch**, **OCR**, and **ASR** tools. Nothing more.
|
|
6
6
|
|
|
7
7
|
Most extensions I've tried seem to be compensating for something. Config that fights pi's philosophy, feature lists that look like a spaceship's dashboard, and UIs that assume you wanted a personal assistant.
|
|
8
8
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
ToolNamespace,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
9
|
import type { Static } from "typebox";
|
|
3
10
|
|
|
4
|
-
import
|
|
11
|
+
import * as schema from "./schema.ts";
|
|
5
12
|
|
|
6
13
|
const unauthorized = new Error("Unauthorized. Run `/login deepseek` to authenticate.");
|
|
7
14
|
|
|
15
|
+
const deepseekNamespace: ToolNamespace = {
|
|
16
|
+
name: "deepseek",
|
|
17
|
+
description: "DeepSeek web search/fetch, document/image OCR, and speech-to-text ASR tools",
|
|
18
|
+
};
|
|
19
|
+
|
|
8
20
|
async function resolveDeepSeekRequest(ctx: ExtensionContext, path: string) {
|
|
9
21
|
const authResult = await ctx.modelRegistry.getProviderAuth("deepseek");
|
|
10
22
|
const apiKey = authResult?.auth.apiKey;
|
|
@@ -20,7 +32,6 @@ async function resolveDeepSeekRequest(ctx: ExtensionContext, path: string) {
|
|
|
20
32
|
headers: {
|
|
21
33
|
...authResult?.auth.headers,
|
|
22
34
|
Authorization: `Bearer ${apiKey}`,
|
|
23
|
-
"Content-Type": "application/json",
|
|
24
35
|
},
|
|
25
36
|
};
|
|
26
37
|
}
|
|
@@ -29,15 +40,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
29
40
|
pi.registerTool({
|
|
30
41
|
name: "web-search",
|
|
31
42
|
label: "DeepSeek Web Search",
|
|
43
|
+
namespace: deepseekNamespace,
|
|
32
44
|
description: "Search web pages for a query and return structured results.",
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
promptSnippet: "Search the web for current information",
|
|
46
|
+
parameters: schema.searchInput,
|
|
47
|
+
outputSchema: schema.searchOutput,
|
|
35
48
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
36
49
|
const { url, headers } = await resolveDeepSeekRequest(ctx, "/v1/tools/search");
|
|
37
50
|
|
|
38
51
|
const response = await fetch(url, {
|
|
39
52
|
method: "POST",
|
|
40
|
-
headers,
|
|
53
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
41
54
|
body: JSON.stringify(params),
|
|
42
55
|
signal: signal ?? null,
|
|
43
56
|
});
|
|
@@ -51,7 +64,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
51
64
|
);
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
const data = (await response.json()) as Static<typeof searchOutput>;
|
|
67
|
+
const data = (await response.json()) as Static<typeof schema.searchOutput>;
|
|
55
68
|
const formatted = data.results
|
|
56
69
|
.map((r, i) =>
|
|
57
70
|
[
|
|
@@ -75,15 +88,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
75
88
|
pi.registerTool({
|
|
76
89
|
name: "web-fetch",
|
|
77
90
|
label: "DeepSeek Web Fetch",
|
|
91
|
+
namespace: deepseekNamespace,
|
|
78
92
|
description: "Fetch a single web page and return its content as Markdown.",
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
promptSnippet: "Fetch a web page by URL and return its content as Markdown",
|
|
94
|
+
parameters: schema.fetchInput,
|
|
95
|
+
outputSchema: schema.fetchOutput,
|
|
81
96
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
82
97
|
const { url, headers } = await resolveDeepSeekRequest(ctx, "/v1/tools/fetch");
|
|
83
98
|
|
|
84
99
|
const response = await fetch(url, {
|
|
85
100
|
method: "POST",
|
|
86
|
-
headers,
|
|
101
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
87
102
|
body: JSON.stringify(params),
|
|
88
103
|
signal: signal ?? null,
|
|
89
104
|
});
|
|
@@ -97,7 +112,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
97
112
|
);
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
const data = (await response.json()) as Static<typeof fetchOutput>;
|
|
115
|
+
const data = (await response.json()) as Static<typeof schema.fetchOutput>;
|
|
101
116
|
return {
|
|
102
117
|
content: [{ type: "text", text: `Title: ${data.title}\n\n${data.content}` }],
|
|
103
118
|
structuredContent: data,
|
|
@@ -105,4 +120,107 @@ export default function (pi: ExtensionAPI): void {
|
|
|
105
120
|
};
|
|
106
121
|
},
|
|
107
122
|
});
|
|
123
|
+
|
|
124
|
+
pi.registerTool({
|
|
125
|
+
name: "ocr",
|
|
126
|
+
label: "DeepSeek OCR",
|
|
127
|
+
namespace: deepseekNamespace,
|
|
128
|
+
description:
|
|
129
|
+
"Recognize text in a PDF, Word (DOC, DOCX), PowerPoint (PPT, PPTX), or image (PNG, JPEG, WebP, GIF, BMP, TIFF) file page by page.",
|
|
130
|
+
promptSnippet: "Extract text from PDF, Word, PowerPoint, or image files via OCR",
|
|
131
|
+
promptGuidelines: [
|
|
132
|
+
"Use ocr instead of read for .pdf, .doc, .docx, .ppt, and .pptx files, or when extracting dense text from document images.",
|
|
133
|
+
],
|
|
134
|
+
parameters: schema.ocrInput,
|
|
135
|
+
outputSchema: schema.ocrOutput,
|
|
136
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
137
|
+
const { url, headers } = await resolveDeepSeekRequest(ctx, "/v1/tools/ocr");
|
|
138
|
+
const filePath = resolve(ctx.cwd, params.path.replace(/^@/, ""));
|
|
139
|
+
const blob = await openAsBlob(filePath);
|
|
140
|
+
|
|
141
|
+
const formData = new FormData();
|
|
142
|
+
formData.append("file", blob, basename(filePath));
|
|
143
|
+
formData.append("purpose", params.purpose);
|
|
144
|
+
|
|
145
|
+
const response = await fetch(url, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers,
|
|
148
|
+
body: formData,
|
|
149
|
+
signal: signal ?? null,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
if (response.status === 401) throw unauthorized;
|
|
154
|
+
|
|
155
|
+
const errorText = await response.text().catch(() => "");
|
|
156
|
+
throw new Error(
|
|
157
|
+
`DeepSeek OCR API error (status ${response.status}): ${errorText || response.statusText}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = (await response.json()) as
|
|
162
|
+
| Static<typeof schema.ocrOutput>
|
|
163
|
+
| { error: { message?: string; code?: string } };
|
|
164
|
+
|
|
165
|
+
if ("error" in data) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`DeepSeek OCR API error: ${data.error.message || data.error.code || "Recognition failed"}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const formatted = data.pages
|
|
172
|
+
.map((p) => `--- Page ${p.index + 1} ---\n${p.text}`)
|
|
173
|
+
.join("\n\n");
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: formatted || "No text recognized." }],
|
|
177
|
+
structuredContent: data,
|
|
178
|
+
details: {},
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
pi.registerTool({
|
|
184
|
+
name: "asr",
|
|
185
|
+
label: "DeepSeek ASR",
|
|
186
|
+
exposure: "codemode",
|
|
187
|
+
namespace: deepseekNamespace,
|
|
188
|
+
description:
|
|
189
|
+
"Transcribe speech from an audio file (16-bit PCM WAV or Ogg Opus, max 8 MiB, max 120 seconds) into text.",
|
|
190
|
+
promptSnippet: "Transcribe speech from WAV or Ogg Opus audio files into text",
|
|
191
|
+
promptGuidelines: ["Use asr to transcribe speech from .wav or .ogg/.opus audio files."],
|
|
192
|
+
parameters: schema.asrInput,
|
|
193
|
+
outputSchema: schema.asrOutput,
|
|
194
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
195
|
+
const { url, headers } = await resolveDeepSeekRequest(ctx, "/v1/tools/asr");
|
|
196
|
+
const filePath = resolve(ctx.cwd, params.path.replace(/^@/, ""));
|
|
197
|
+
const blob = await openAsBlob(filePath);
|
|
198
|
+
|
|
199
|
+
const formData = new FormData();
|
|
200
|
+
formData.append("file", blob, basename(filePath));
|
|
201
|
+
|
|
202
|
+
const response = await fetch(url, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers,
|
|
205
|
+
body: formData,
|
|
206
|
+
signal: signal ?? null,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
if (response.status === 401) throw unauthorized;
|
|
211
|
+
|
|
212
|
+
const errorText = await response.text().catch(() => "");
|
|
213
|
+
throw new Error(
|
|
214
|
+
`DeepSeek ASR API error (status ${response.status}): ${errorText || response.statusText}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const data = (await response.json()) as Static<typeof schema.asrOutput>;
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: data.text }],
|
|
221
|
+
structuredContent: data,
|
|
222
|
+
details: {},
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
});
|
|
108
226
|
}
|
package/src/schema.ts
CHANGED
|
@@ -63,3 +63,49 @@ export const fetchOutput = Type.Object({
|
|
|
63
63
|
title: Type.String(),
|
|
64
64
|
content: Type.String(),
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
export const ocrInput = Type.Object({
|
|
68
|
+
path: Type.String({
|
|
69
|
+
minLength: 1,
|
|
70
|
+
description:
|
|
71
|
+
"Path to the file to recognize. Supported: PDF, Word (DOC, DOCX), PowerPoint (PPT, PPTX), and images (PNG, JPEG, WebP, GIF, BMP, TIFF).",
|
|
72
|
+
}),
|
|
73
|
+
purpose: Type.Enum(["text_recognition", "rich_layout_recognition"], {
|
|
74
|
+
description:
|
|
75
|
+
"text_recognition extracts text directly from Word and PowerPoint files without images and recognizes other files page by page. rich_layout_recognition recognizes every file page by page.",
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const ocrOutput = Type.Object({
|
|
80
|
+
id: Type.String(),
|
|
81
|
+
object: Type.Literal("ocr.result"),
|
|
82
|
+
created: Type.Integer(),
|
|
83
|
+
purpose: Type.Enum(["text_recognition", "rich_layout_recognition"]),
|
|
84
|
+
page_count: Type.Integer(),
|
|
85
|
+
pages: Type.Array(
|
|
86
|
+
Type.Object({
|
|
87
|
+
index: Type.Integer(),
|
|
88
|
+
text: Type.String(),
|
|
89
|
+
finish_reason: Type.Enum(["stop", "length"]),
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
usage: Type.Object({
|
|
93
|
+
input_tokens: Type.Integer(),
|
|
94
|
+
output_tokens: Type.Integer(),
|
|
95
|
+
total_tokens: Type.Integer(),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const asrInput = Type.Object({
|
|
100
|
+
path: Type.String({
|
|
101
|
+
minLength: 1,
|
|
102
|
+
description:
|
|
103
|
+
"Path to the audio file to transcribe (16-bit PCM WAV or Ogg Opus, max 8 MiB, max 120 seconds).",
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export const asrOutput = Type.Object({
|
|
108
|
+
request_id: Type.String(),
|
|
109
|
+
text: Type.String(),
|
|
110
|
+
duration_ms: Type.Integer(),
|
|
111
|
+
});
|