@yandy0725/pi-web-tools 0.1.0 → 0.3.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 +2 -71
- package/index.ts +1 -160
- package/package.json +7 -5
- package/src/config.ts +0 -2
- package/src/deep_search/aliyun.ts +0 -48
- package/src/deep_search/index.ts +0 -1
- package/src/deep_search/types.ts +0 -16
- package/src/image_search/aliyun.ts +0 -87
- package/src/image_search/index.ts +0 -1
- package/src/image_search/types.ts +0 -15
- package/src/openai_client.ts +0 -9
- package/src/provider.ts +0 -69
package/README.md
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# pi-web-tools
|
|
2
2
|
|
|
3
|
-
A [pi](https://pi.dev/docs/latest/packages) package providing web and
|
|
3
|
+
A [pi](https://pi.dev/docs/latest/packages) package providing web search and web fetch tools for coding agents.
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
7
|
| Tool | Description | Source |
|
|
8
8
|
|------|-------------|--------|
|
|
9
9
|
| `web_search` | Pure web search, returns raw results (titles, URLs, snippets) | Exa (REST + MCP free tier) |
|
|
10
|
-
| `deep_search` | Deep research with LLM-synthesized answers | Aliyun (Bailian) Chat Completions API |
|
|
11
|
-
| `image_search` | Search images by text or find similar images by URL | Aliyun (Bailian) Responses API |
|
|
12
10
|
| `web_fetch` | Fetch and convert web pages to text, markdown, or raw HTML | — |
|
|
13
11
|
|
|
14
12
|
## Quick Start
|
|
@@ -24,53 +22,16 @@ pi -e ./index.ts
|
|
|
24
22
|
### Prerequisites
|
|
25
23
|
|
|
26
24
|
- `web_search`: No config needed — Exa MCP free tier (150 calls/day). Set `EXA_API_KEY` for higher limits.
|
|
27
|
-
- `deep_search` / `image_search`: Set `ALIYUN_API_KEY` or use `/login` in pi to authenticate with Aliyun.
|
|
28
25
|
|
|
29
26
|
## Configuration
|
|
30
27
|
|
|
31
|
-
Configuration uses
|
|
28
|
+
Configuration uses environment variables for API keys.
|
|
32
29
|
|
|
33
30
|
### API Keys (environment variables only)
|
|
34
31
|
|
|
35
32
|
| Variable | Description | Default |
|
|
36
33
|
|----------|-------------|---------|
|
|
37
34
|
| `EXA_API_KEY` | Exa API key. If not set, uses MCP free tier (150 calls/day) | — |
|
|
38
|
-
| `ALIYUN_API_KEY` | Aliyun (Bailian) API key | — |
|
|
39
|
-
| `ALIYUN_BASE_URL` | Aliyun API base URL | `https://dashscope.aliyuncs.com/compatible-mode/v1` |
|
|
40
|
-
| `ALIYUN_DEEP_SEARCH_MODEL` | Model for deep_search | `deepseek-v4-flash` |
|
|
41
|
-
| `ALIYUN_IMAGE_SEARCH_MODEL` | Model for image_search | `qwen3.7-plus` |
|
|
42
|
-
|
|
43
|
-
Aliyun also supports key resolution via pi's `/login` — if you've logged into Aliyun through pi, no env var needed.
|
|
44
|
-
|
|
45
|
-
### Project Config (`.pi/agent/web-tools.json`)
|
|
46
|
-
|
|
47
|
-
Create `.pi/agent/web-tools.json` in your project root for per-project settings:
|
|
48
|
-
|
|
49
|
-
```json
|
|
50
|
-
{
|
|
51
|
-
"aliyun": {
|
|
52
|
-
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
53
|
-
"aliyunProviderKey": "aliyun",
|
|
54
|
-
"deepSearchModel": "deepseek-v4-flash",
|
|
55
|
-
"imageSearchModel": "qwen3.7-plus"
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Environment variables take precedence over the config file.
|
|
61
|
-
|
|
62
|
-
| Config Key | Env Variable (overrides) | Default | Description |
|
|
63
|
-
|------------|--------------------------|---------|-------------|
|
|
64
|
-
| `aliyun.baseUrl` | `ALIYUN_BASE_URL` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | Aliyun API base URL |
|
|
65
|
-
| `aliyun.aliyunProviderKey` | — | `aliyun` | Pi provider name to extract apiKey/baseUrl from |
|
|
66
|
-
| `aliyun.deepSearchModel` | `ALIYUN_DEEP_SEARCH_MODEL` | `deepseek-v4-flash` | Model for deep_search |
|
|
67
|
-
| `aliyun.imageSearchModel` | `ALIYUN_IMAGE_SEARCH_MODEL` | `qwen3.7-plus` | Model for image_search |
|
|
68
|
-
|
|
69
|
-
**aliyunProviderKey:** deep_search and image_search will extract apiKey and baseUrl from the corresponding pi provider (via `modelRegistry`). Defaults to `"aliyun"`. Environment variables take precedence over provider values. If the provider is not found, falls back to `aliyun.baseUrl` config or default.
|
|
70
|
-
|
|
71
|
-
> **Note:** deep_search uses Chat Completions API and does not return structured sources. image_search uses Responses API.
|
|
72
|
-
|
|
73
|
-
> **Security:** API keys are NEVER read from config files — only from environment variables or pi's built-in credential store (`/login`).
|
|
74
35
|
|
|
75
36
|
## Tools Reference
|
|
76
37
|
|
|
@@ -88,36 +49,6 @@ Search the web with automatic source fallback.
|
|
|
88
49
|
|
|
89
50
|
**Source:** **Exa** — AI-native search API. With `EXA_API_KEY`: full REST API. Without: MCP free tier (150 calls/day, 3 QPS). Always available, no key needed for basic usage.
|
|
90
51
|
|
|
91
|
-
### deep_search
|
|
92
|
-
|
|
93
|
-
Deep research using Aliyun's LLM-powered search with web content extraction. The model searches the web, extracts page content, and synthesizes a comprehensive answer.
|
|
94
|
-
|
|
95
|
-
**Parameters:**
|
|
96
|
-
|
|
97
|
-
| Parameter | Type | Required | Default | Description |
|
|
98
|
-
|-----------|------|----------|---------|-------------|
|
|
99
|
-
| `query` | string | yes | — | Research question |
|
|
100
|
-
| `enableSearchExtension` | boolean | no | false | Enable vertical domain search |
|
|
101
|
-
| `freshness` | number | no | — | Time range: 7/30/180/365 days |
|
|
102
|
-
| `assignedSiteList` | string[] | no | — | Restrict search to specific sites |
|
|
103
|
-
| `enableImageOutput` | boolean | no | false | Enable mixed text-image output |
|
|
104
|
-
|
|
105
|
-
> Requires `ALIYUN_API_KEY` or `aliyunProviderKey` config. Uses Chat Completions API with forced search (turbo strategy). Sources are not returned.
|
|
106
|
-
|
|
107
|
-
### image_search
|
|
108
|
-
|
|
109
|
-
Search images by text description or find visually similar images by URL.
|
|
110
|
-
|
|
111
|
-
**Parameters:**
|
|
112
|
-
|
|
113
|
-
| Parameter | Type | Required | Description |
|
|
114
|
-
|-----------|------|----------|-------------|
|
|
115
|
-
| `query` | string | no | Text description for text-to-image search |
|
|
116
|
-
| `imageUrl` | string | no | Public image URL for image-to-image search |
|
|
117
|
-
|
|
118
|
-
> At least one of `query` or `imageUrl` must be provided. Both can be combined.
|
|
119
|
-
> The image URL must be publicly accessible. Requires `ALIYUN_API_KEY`.
|
|
120
|
-
|
|
121
52
|
### web_fetch
|
|
122
53
|
|
|
123
54
|
Fetch content from a URL and return as text, markdown, or raw HTML.
|
package/index.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { loadConfig } from "./src/config";
|
|
5
|
-
import { deepSearch } from "./src/deep_search/index";
|
|
6
|
-
import { imageSearch } from "./src/image_search/index";
|
|
7
4
|
import { webFetch } from "./src/web_fetch";
|
|
8
5
|
import { search } from "./src/web_search/index";
|
|
9
6
|
|
|
@@ -80,162 +77,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
80
77
|
},
|
|
81
78
|
});
|
|
82
79
|
|
|
83
|
-
// -------------------------------------------------------------------
|
|
84
|
-
// deep_search
|
|
85
|
-
// -------------------------------------------------------------------
|
|
86
|
-
pi.registerTool({
|
|
87
|
-
name: "deep_search",
|
|
88
|
-
label: "Deep Search",
|
|
89
|
-
description:
|
|
90
|
-
"Deep search powered by Aliyun (Bailian) using Chat Completions API with web search. The model searches the web and synthesizes a comprehensive answer. Supports vertical domain search, time range filtering, site restriction, and mixed image output.",
|
|
91
|
-
promptSnippet:
|
|
92
|
-
"deep_search: Aliyun-powered deep search that synthesizes web results into a comprehensive answer. Supports vertical domain search, time range filtering, site restriction, and mixed image output.",
|
|
93
|
-
promptGuidelines: [
|
|
94
|
-
"Use deep_search for complex research questions that benefit from web search synthesis.",
|
|
95
|
-
"deep_search is powered by Aliyun Chat Completions API. Configure ALIYUN_API_KEY or use aliyunProviderKey in config.",
|
|
96
|
-
],
|
|
97
|
-
parameters: Type.Object({
|
|
98
|
-
query: Type.String({ minLength: 2, description: "The search query." }),
|
|
99
|
-
enableSearchExtension: Type.Optional(
|
|
100
|
-
Type.Boolean({ description: "Enable vertical domain search for more precise results." }),
|
|
101
|
-
),
|
|
102
|
-
freshness: Type.Optional(
|
|
103
|
-
Type.Number({
|
|
104
|
-
enum: [7, 30, 180, 365],
|
|
105
|
-
description: "Time range filter: 7/30/180/365 days. Only effective with turbo strategy.",
|
|
106
|
-
}),
|
|
107
|
-
),
|
|
108
|
-
assignedSiteList: Type.Optional(
|
|
109
|
-
Type.Array(Type.String(), {
|
|
110
|
-
description: 'Restrict search to specific sites (e.g. ["baidu.com", "sina.cn"]).',
|
|
111
|
-
}),
|
|
112
|
-
),
|
|
113
|
-
enableImageOutput: Type.Optional(Type.Boolean({ description: "Enable mixed text-image output in the response." })),
|
|
114
|
-
}),
|
|
115
|
-
renderCall(args, theme) {
|
|
116
|
-
const p = args as { query: string };
|
|
117
|
-
return new Text(
|
|
118
|
-
theme.fg("toolTitle", theme.bold("deep_search ")) + theme.fg("accent", `"${p.query || "..."}"`),
|
|
119
|
-
0,
|
|
120
|
-
0,
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
renderResult(result, { expanded }, theme) {
|
|
124
|
-
const text = result.content?.[0];
|
|
125
|
-
const body = text?.type === "text" ? text.text : "";
|
|
126
|
-
const lines = body.split("\n");
|
|
127
|
-
if (!expanded) {
|
|
128
|
-
const preview = lines.slice(0, 6);
|
|
129
|
-
if (lines.length > 6) preview.push(theme.fg("dim", `... ${lines.length - 6} more lines · ctrl+o to expand`));
|
|
130
|
-
return new Text(preview.join("\n"), 0, 0);
|
|
131
|
-
}
|
|
132
|
-
return new Text(body, 0, 0);
|
|
133
|
-
},
|
|
134
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
135
|
-
const p = params as {
|
|
136
|
-
query: string;
|
|
137
|
-
enableSearchExtension?: boolean;
|
|
138
|
-
freshness?: number;
|
|
139
|
-
assignedSiteList?: string[];
|
|
140
|
-
enableImageOutput?: boolean;
|
|
141
|
-
};
|
|
142
|
-
const query = p.query?.trim();
|
|
143
|
-
if (!query) {
|
|
144
|
-
return { content: [{ type: "text", text: "Error: query is required." }], details: {}, isError: true };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
onUpdate?.({ content: [{ type: "text", text: "Deep searching..." }], details: {} });
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const cfg = loadConfig(ctx.cwd);
|
|
151
|
-
const result = await deepSearch(query, signal, cfg.aliyun, ctx, {
|
|
152
|
-
enableSearchExtension: p.enableSearchExtension,
|
|
153
|
-
freshness: p.freshness,
|
|
154
|
-
assignedSiteList: p.assignedSiteList,
|
|
155
|
-
enableImageOutput: p.enableImageOutput,
|
|
156
|
-
});
|
|
157
|
-
const sourcesText = result.sources.length
|
|
158
|
-
? `\n\nSources:\n${result.sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")}`
|
|
159
|
-
: "";
|
|
160
|
-
return {
|
|
161
|
-
content: [{ type: "text", text: result.answer + sourcesText }],
|
|
162
|
-
details: { sources: result.sources },
|
|
163
|
-
};
|
|
164
|
-
} catch (error) {
|
|
165
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
-
return { content: [{ type: "text", text: `Deep search failed: ${message}` }], details: {}, isError: true };
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// -------------------------------------------------------------------
|
|
172
|
-
// image_search
|
|
173
|
-
// -------------------------------------------------------------------
|
|
174
|
-
pi.registerTool({
|
|
175
|
-
name: "image_search",
|
|
176
|
-
label: "Image Search",
|
|
177
|
-
description:
|
|
178
|
-
"Search for images by text description or find similar images by URL. Powered by Aliyun (Bailian). Returns image results and model analysis.",
|
|
179
|
-
promptSnippet: "image_search: search images by text or find similar images by URL. Powered by Aliyun (Bailian).",
|
|
180
|
-
promptGuidelines: [
|
|
181
|
-
"Use image_search to find images matching a text description (provide query).",
|
|
182
|
-
"Use image_search to find visually similar images (provide imageUrl, the image must be a publicly accessible URL).",
|
|
183
|
-
"Both query and imageUrl can be provided together for combined search.",
|
|
184
|
-
],
|
|
185
|
-
parameters: Type.Object({
|
|
186
|
-
query: Type.Optional(Type.String({ minLength: 2, description: "Text description of the image to search for." })),
|
|
187
|
-
imageUrl: Type.Optional(Type.String({ description: "Public URL of the image to find similar images." })),
|
|
188
|
-
}),
|
|
189
|
-
renderCall(args, theme) {
|
|
190
|
-
const p = args as { query?: string; imageUrl?: string };
|
|
191
|
-
const label = theme.fg("toolTitle", theme.bold("image_search "));
|
|
192
|
-
if (p.imageUrl) return new Text(label + theme.fg("accent", `[image: ${p.imageUrl}]`), 0, 0);
|
|
193
|
-
return new Text(label + theme.fg("accent", `"${p.query || "..."}"`), 0, 0);
|
|
194
|
-
},
|
|
195
|
-
renderResult(result, { expanded }, theme) {
|
|
196
|
-
const text = result.content?.[0];
|
|
197
|
-
const body = text?.type === "text" ? text.text : "";
|
|
198
|
-
const lines = body.split("\n");
|
|
199
|
-
if (!expanded) {
|
|
200
|
-
const preview = lines.slice(0, 6);
|
|
201
|
-
if (lines.length > 6) preview.push(theme.fg("dim", `... ${lines.length - 6} more lines · ctrl+o to expand`));
|
|
202
|
-
return new Text(preview.join("\n"), 0, 0);
|
|
203
|
-
}
|
|
204
|
-
return new Text(body, 0, 0);
|
|
205
|
-
},
|
|
206
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx: ExtensionContext) {
|
|
207
|
-
const p = params as { query?: string; imageUrl?: string };
|
|
208
|
-
if (!p.query && !p.imageUrl) {
|
|
209
|
-
return {
|
|
210
|
-
content: [{ type: "text", text: "Error: at least one of query or imageUrl is required." }],
|
|
211
|
-
details: {},
|
|
212
|
-
isError: true,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
onUpdate?.({ content: [{ type: "text", text: "Searching images..." }], details: {} });
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
const cfg = loadConfig(ctx.cwd);
|
|
220
|
-
const result = await imageSearch({ query: p.query, imageUrl: p.imageUrl }, signal, cfg.aliyun, ctx);
|
|
221
|
-
const imagesText = result.images.length
|
|
222
|
-
? "\n\nImages:\n" + result.images.map((img) => `${img.index}. [${img.title}](${img.url})`).join("\n")
|
|
223
|
-
: "";
|
|
224
|
-
return {
|
|
225
|
-
content: [{ type: "text", text: result.answer + imagesText }],
|
|
226
|
-
details: { images: result.images },
|
|
227
|
-
};
|
|
228
|
-
} catch (error) {
|
|
229
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
-
return {
|
|
231
|
-
content: [{ type: "text", text: `Image search failed: ${message}` }],
|
|
232
|
-
details: {},
|
|
233
|
-
isError: true,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
|
|
239
80
|
// -------------------------------------------------------------------
|
|
240
81
|
// web_fetch
|
|
241
82
|
// -------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
7
|
-
"description": "pi package providing web_search
|
|
6
|
+
"version": "0.3.0",
|
|
7
|
+
"description": "pi package providing web_search and web_fetch tools",
|
|
8
8
|
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/yandy/pi-packages",
|
|
12
|
+
"directory": "pi-web-tools"
|
|
13
|
+
},
|
|
9
14
|
"type": "module",
|
|
10
15
|
"keywords": [
|
|
11
16
|
"pi-package"
|
|
@@ -27,9 +32,6 @@
|
|
|
27
32
|
"./index.ts"
|
|
28
33
|
]
|
|
29
34
|
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"openai": "^6.26.0"
|
|
32
|
-
},
|
|
33
35
|
"peerDependencies": {
|
|
34
36
|
"@earendil-works/pi-coding-agent": ">=0.74.0"
|
|
35
37
|
},
|
package/src/config.ts
CHANGED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type OpenAI from "openai";
|
|
3
|
-
import { resolveSetting } from "../config";
|
|
4
|
-
import { createAliyunClient } from "../openai_client";
|
|
5
|
-
import { resolveAliyunProvider } from "../provider";
|
|
6
|
-
import type { DeepSearchOptions, DeepSearchResponse } from "./types";
|
|
7
|
-
|
|
8
|
-
const DEFAULT_DEEP_SEARCH_MODEL = "deepseek-v4-flash";
|
|
9
|
-
const TIMEOUT_MS = 120_000;
|
|
10
|
-
|
|
11
|
-
export async function aliyunDeepSearch(
|
|
12
|
-
query: string,
|
|
13
|
-
signal?: AbortSignal,
|
|
14
|
-
config?: { baseUrl?: string; aliyunProviderKey?: string; deepSearchModel?: string },
|
|
15
|
-
ctx?: ExtensionContext,
|
|
16
|
-
searchOpts?: DeepSearchOptions,
|
|
17
|
-
): Promise<DeepSearchResponse> {
|
|
18
|
-
const { apiKey, baseUrl } = await resolveAliyunProvider({ ctx, config });
|
|
19
|
-
const model = resolveSetting(process.env.ALIYUN_DEEP_SEARCH_MODEL, config?.deepSearchModel, DEFAULT_DEEP_SEARCH_MODEL);
|
|
20
|
-
const client = createAliyunClient({ apiKey, baseUrl });
|
|
21
|
-
|
|
22
|
-
const s = signal ? AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT_MS)]) : AbortSignal.timeout(TIMEOUT_MS);
|
|
23
|
-
|
|
24
|
-
const { enableSearchExtension, freshness, assignedSiteList, enableImageOutput } = searchOpts ?? {};
|
|
25
|
-
|
|
26
|
-
const searchOptions: Record<string, unknown> = {
|
|
27
|
-
search_strategy: "turbo",
|
|
28
|
-
forced_search: true,
|
|
29
|
-
...(enableSearchExtension && { enable_search_extension: true }),
|
|
30
|
-
...(freshness && { freshness }),
|
|
31
|
-
...(assignedSiteList?.length && { assigned_site_list: assignedSiteList }),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const completion = await client.chat.completions.create(
|
|
35
|
-
{
|
|
36
|
-
model,
|
|
37
|
-
messages: [{ role: "user", content: query }],
|
|
38
|
-
stream: false,
|
|
39
|
-
enable_search: true,
|
|
40
|
-
search_options: searchOptions,
|
|
41
|
-
...(enableImageOutput && { enable_text_image_mixed: true }),
|
|
42
|
-
} as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,
|
|
43
|
-
{ signal: s },
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const answer = completion.choices[0]?.message?.content || "No results";
|
|
47
|
-
return { answer, sources: [] };
|
|
48
|
-
}
|
package/src/deep_search/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { aliyunDeepSearch as deepSearch } from "./aliyun";
|
package/src/deep_search/types.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface DeepSearchSource {
|
|
2
|
-
title: string;
|
|
3
|
-
url: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface DeepSearchResponse {
|
|
7
|
-
answer: string;
|
|
8
|
-
sources: DeepSearchSource[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface DeepSearchOptions {
|
|
12
|
-
enableSearchExtension?: boolean;
|
|
13
|
-
freshness?: number;
|
|
14
|
-
assignedSiteList?: string[];
|
|
15
|
-
enableImageOutput?: boolean;
|
|
16
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type OpenAI from "openai";
|
|
3
|
-
import { resolveSetting } from "../config";
|
|
4
|
-
import { createAliyunClient } from "../openai_client";
|
|
5
|
-
import { resolveAliyunProvider } from "../provider";
|
|
6
|
-
import type { ImageResult, ImageSearchParams, ImageSearchResponse } from "./types";
|
|
7
|
-
|
|
8
|
-
const DEFAULT_IMAGE_SEARCH_MODEL = "qwen3.7-plus";
|
|
9
|
-
const TIMEOUT_MS = 120_000;
|
|
10
|
-
|
|
11
|
-
export async function aliyunImageSearch(
|
|
12
|
-
params: ImageSearchParams,
|
|
13
|
-
signal?: AbortSignal,
|
|
14
|
-
config?: { baseUrl?: string; aliyunProviderKey?: string; imageSearchModel?: string },
|
|
15
|
-
ctx?: ExtensionContext,
|
|
16
|
-
): Promise<ImageSearchResponse> {
|
|
17
|
-
const { query, imageUrl } = params;
|
|
18
|
-
|
|
19
|
-
if (!query && !imageUrl) {
|
|
20
|
-
throw new Error("At least one of query or imageUrl must be provided");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const { apiKey, baseUrl } = await resolveAliyunProvider({ ctx, config });
|
|
24
|
-
const model = resolveSetting(
|
|
25
|
-
process.env.ALIYUN_IMAGE_SEARCH_MODEL,
|
|
26
|
-
config?.imageSearchModel,
|
|
27
|
-
DEFAULT_IMAGE_SEARCH_MODEL,
|
|
28
|
-
);
|
|
29
|
-
const client = createAliyunClient({ apiKey, baseUrl });
|
|
30
|
-
|
|
31
|
-
const s = signal ? AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT_MS)]) : AbortSignal.timeout(TIMEOUT_MS);
|
|
32
|
-
|
|
33
|
-
let input: unknown;
|
|
34
|
-
let tools: Array<{ type: string }>;
|
|
35
|
-
|
|
36
|
-
if (imageUrl) {
|
|
37
|
-
tools = [{ type: "image_search" }];
|
|
38
|
-
const content: Array<{ type: string; [key: string]: unknown }> = [];
|
|
39
|
-
if (query) {
|
|
40
|
-
content.push({ type: "input_text", text: query });
|
|
41
|
-
}
|
|
42
|
-
content.push({ type: "input_image", image_url: imageUrl });
|
|
43
|
-
input = [{ role: "user", content }];
|
|
44
|
-
} else {
|
|
45
|
-
tools = [{ type: "web_search_image" }];
|
|
46
|
-
input = query;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const response = (await client.responses.create(
|
|
50
|
-
{ model, input, tools } as unknown as OpenAI.Responses.ResponseCreateParams,
|
|
51
|
-
{ signal: s },
|
|
52
|
-
)) as unknown as AliyunImageResponse;
|
|
53
|
-
|
|
54
|
-
const images = parseImages(response.output);
|
|
55
|
-
const answer = parseAnswer(response.output);
|
|
56
|
-
|
|
57
|
-
return { answer, images };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface AliyunImageResponse {
|
|
61
|
-
output?: Array<{
|
|
62
|
-
type: string;
|
|
63
|
-
output?: string;
|
|
64
|
-
content?: Array<{ type?: string; text?: string }>;
|
|
65
|
-
}>;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseImages(output: AliyunImageResponse["output"] = []): ImageResult[] {
|
|
69
|
-
for (const item of output) {
|
|
70
|
-
if (item.type === "web_search_image_call" || item.type === "image_search_call") {
|
|
71
|
-
try {
|
|
72
|
-
return JSON.parse(item.output || "[]") as ImageResult[];
|
|
73
|
-
} catch {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function parseAnswer(output: AliyunImageResponse["output"] = []): string {
|
|
82
|
-
const messages = output.filter((item) => item.type === "message");
|
|
83
|
-
const texts = messages.flatMap((m) =>
|
|
84
|
-
(m.content || []).filter((c) => c.type === "output_text").map((c) => c.text || ""),
|
|
85
|
-
);
|
|
86
|
-
return texts.join("\n") || "No results";
|
|
87
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { aliyunImageSearch as imageSearch } from "./aliyun";
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface ImageResult {
|
|
2
|
-
index: number;
|
|
3
|
-
title: string;
|
|
4
|
-
url: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface ImageSearchResponse {
|
|
8
|
-
answer: string;
|
|
9
|
-
images: ImageResult[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ImageSearchParams {
|
|
13
|
-
query?: string;
|
|
14
|
-
imageUrl?: string;
|
|
15
|
-
}
|
package/src/openai_client.ts
DELETED
package/src/provider.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
4
|
-
const DEFAULT_PROVIDER_KEY = "aliyun";
|
|
5
|
-
|
|
6
|
-
interface ProviderConfig {
|
|
7
|
-
baseUrl?: string;
|
|
8
|
-
aliyunProviderKey?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface ResolvedProvider {
|
|
12
|
-
apiKey: string;
|
|
13
|
-
baseUrl: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function resolveAliyunProvider(opts: {
|
|
17
|
-
ctx?: ExtensionContext;
|
|
18
|
-
config?: ProviderConfig;
|
|
19
|
-
}): Promise<ResolvedProvider> {
|
|
20
|
-
const { ctx, config } = opts;
|
|
21
|
-
const providerKey = config?.aliyunProviderKey ?? DEFAULT_PROVIDER_KEY;
|
|
22
|
-
|
|
23
|
-
// --- apiKey ---
|
|
24
|
-
let apiKey: string | undefined;
|
|
25
|
-
|
|
26
|
-
const envApiKey = process.env.ALIYUN_API_KEY;
|
|
27
|
-
if (envApiKey) {
|
|
28
|
-
apiKey = envApiKey;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (!apiKey && providerKey && ctx) {
|
|
32
|
-
const providerKeyResult = await ctx.modelRegistry.getApiKeyForProvider(providerKey);
|
|
33
|
-
if (providerKeyResult) {
|
|
34
|
-
apiKey = providerKeyResult;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!apiKey) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
"ALIYUN_API_KEY not configured. Set ALIYUN_API_KEY or configure aliyunProviderKey with a valid pi provider.",
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// --- baseUrl ---
|
|
45
|
-
let baseUrl: string | undefined;
|
|
46
|
-
|
|
47
|
-
const envBaseUrl = process.env.ALIYUN_BASE_URL;
|
|
48
|
-
if (envBaseUrl) {
|
|
49
|
-
baseUrl = envBaseUrl;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!baseUrl && providerKey && ctx) {
|
|
53
|
-
const allModels = ctx.modelRegistry.getAll();
|
|
54
|
-
const matchingModel = allModels.find((m) => m.provider === providerKey);
|
|
55
|
-
if (matchingModel?.baseUrl) {
|
|
56
|
-
baseUrl = matchingModel.baseUrl;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!baseUrl && config?.baseUrl) {
|
|
61
|
-
baseUrl = config.baseUrl;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!baseUrl) {
|
|
65
|
-
baseUrl = DEFAULT_BASE_URL;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { apiKey, baseUrl };
|
|
69
|
-
}
|