pi-lean-dimension 0.2.0 โ 0.2.2
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 -2
- package/node_modules/pi-lean-portal/AGENTS.md +1 -1
- package/node_modules/pi-lean-portal/README.md +28 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +28 -0
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +237 -181
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +7 -2
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +1 -1
- package/node_modules/pi-lean-portal/contributed/README.md +1 -1
- package/node_modules/pi-lean-portal/core/guides.ts +1 -2
- package/node_modules/pi-lean-portal/core/plugin-config.ts +27 -3
- package/node_modules/pi-lean-portal/core/router.ts +2 -1
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +6 -2
- package/node_modules/pi-lean-portal/package.json +1 -1
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +241 -1
- package/node_modules/pi-lean-search/package.json +1 -1
- package/node_modules/pi-lean-search/web-search-tool.ts +222 -2
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-portal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Pi extension. Interactive web browsing for Pi โ a /web toggle removes the tools from context when switched off, Playwright Chromium/Firefox deliver accessibility-tree snapshots, persistent profiles, cookies, and domain-matched guides; custom/stealth backends (Camoufox) plug in when a site blocks the shipped browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* instance and are not included here.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
9
9
|
import { readSearxngUrl } from "../search-config.js";
|
|
10
10
|
import { webSearchTool } from "../web-search-tool.js";
|
|
11
11
|
|
|
@@ -150,4 +150,244 @@ describe("webSearchTool", () => {
|
|
|
150
150
|
it("has renderResult function", () => {
|
|
151
151
|
expect(typeof webSearchTool.renderResult).toBe("function");
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
describe("renderResult answer badge", () => {
|
|
155
|
+
it("shows ๐ก badge when answerCount > 0 and no results", () => {
|
|
156
|
+
const mockTheme = {
|
|
157
|
+
fg: (_style: string, text: string) => text,
|
|
158
|
+
} as any;
|
|
159
|
+
const result = (webSearchTool.renderResult as any)(
|
|
160
|
+
{
|
|
161
|
+
content: [{ text: "" }],
|
|
162
|
+
details: { answerCount: 2, query: "weather" },
|
|
163
|
+
} as any,
|
|
164
|
+
{ expanded: false, isPartial: false },
|
|
165
|
+
mockTheme,
|
|
166
|
+
{} as any,
|
|
167
|
+
) as any;
|
|
168
|
+
expect(result.text).toContain("๐ก");
|
|
169
|
+
expect(result.text).toContain("2 answer(s)");
|
|
170
|
+
expect(result.text).toContain('"weather"');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("shows ๐ก badge alongside ๐ when both answers and results present", () => {
|
|
174
|
+
const mockTheme = {
|
|
175
|
+
fg: (_style: string, text: string) => text,
|
|
176
|
+
} as any;
|
|
177
|
+
const result = (webSearchTool.renderResult as any)(
|
|
178
|
+
{
|
|
179
|
+
content: [{ text: "" }],
|
|
180
|
+
details: {
|
|
181
|
+
answerCount: 1,
|
|
182
|
+
resultCount: 3,
|
|
183
|
+
query: "test",
|
|
184
|
+
results: [
|
|
185
|
+
{ title: "A", url: "http://a.com", engine: "google" },
|
|
186
|
+
{ title: "B", url: "http://b.com", engine: "google" },
|
|
187
|
+
{ title: "C", url: "http://c.com", engine: "google" },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
} as any,
|
|
191
|
+
{ expanded: false, isPartial: false },
|
|
192
|
+
mockTheme,
|
|
193
|
+
{} as any,
|
|
194
|
+
) as any;
|
|
195
|
+
expect(result.text).toContain("๐ก");
|
|
196
|
+
expect(result.text).toContain("1 answer(s)");
|
|
197
|
+
expect(result.text).toContain("๐");
|
|
198
|
+
expect(result.text).toContain("3 result(s)");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("shows answer text above results when expanded", () => {
|
|
202
|
+
const mockTheme = {
|
|
203
|
+
fg: (_style: string, text: string) => text,
|
|
204
|
+
} as any;
|
|
205
|
+
const result = (webSearchTool.renderResult as any)(
|
|
206
|
+
{
|
|
207
|
+
content: [{ text: "" }],
|
|
208
|
+
details: {
|
|
209
|
+
answerCount: 1,
|
|
210
|
+
resultCount: 1,
|
|
211
|
+
query: "weather berlin",
|
|
212
|
+
answers: [
|
|
213
|
+
{
|
|
214
|
+
template: "answer/weather.html",
|
|
215
|
+
text: "Berlin: 14ยฐC, light rain",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
results: [{ title: "A", url: "http://a.com" }],
|
|
219
|
+
},
|
|
220
|
+
} as any,
|
|
221
|
+
{ expanded: true, isPartial: false },
|
|
222
|
+
mockTheme,
|
|
223
|
+
{} as any,
|
|
224
|
+
) as any;
|
|
225
|
+
const answerIdx = result.text.indexOf("Berlin: 14ยฐC, light rain");
|
|
226
|
+
const resultIdx = result.text.indexOf("A");
|
|
227
|
+
expect(answerIdx).toBeGreaterThan(-1);
|
|
228
|
+
expect(resultIdx).toBeGreaterThan(-1);
|
|
229
|
+
expect(answerIdx).toBeLessThan(resultIdx);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("shows answer text when expanded with no results", () => {
|
|
233
|
+
const mockTheme = {
|
|
234
|
+
fg: (_style: string, text: string) => text,
|
|
235
|
+
} as any;
|
|
236
|
+
const result = (webSearchTool.renderResult as any)(
|
|
237
|
+
{
|
|
238
|
+
content: [{ text: "" }],
|
|
239
|
+
details: {
|
|
240
|
+
answerCount: 1,
|
|
241
|
+
query: "avg 1 2 3",
|
|
242
|
+
answers: [{ template: "answer/legacy.html", text: "2" }],
|
|
243
|
+
},
|
|
244
|
+
} as any,
|
|
245
|
+
{ expanded: true, isPartial: false },
|
|
246
|
+
mockTheme,
|
|
247
|
+
{} as any,
|
|
248
|
+
) as any;
|
|
249
|
+
expect(result.text).toContain("๐ก");
|
|
250
|
+
expect(result.text).toContain("2");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// โโโ Execute answer rendering (mocked fetch) โโโโโโโโโโโโโโโโโ
|
|
256
|
+
|
|
257
|
+
describe("execute answer rendering", () => {
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
vi.clearAllMocks();
|
|
260
|
+
// Make readSearxngUrl return a valid URL
|
|
261
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
262
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
263
|
+
JSON.stringify({ searxng: { url: "http://localhost:8888" } }),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
afterEach(() => {
|
|
268
|
+
vi.unstubAllGlobals();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const mockFetch = (json: unknown) =>
|
|
272
|
+
vi.stubGlobal(
|
|
273
|
+
"fetch",
|
|
274
|
+
vi.fn().mockResolvedValue({
|
|
275
|
+
ok: true,
|
|
276
|
+
status: 200,
|
|
277
|
+
text: async () => JSON.stringify(json),
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
it("legacy answer renders + bypasses empty-results early return", async () => {
|
|
282
|
+
mockFetch({
|
|
283
|
+
results: [],
|
|
284
|
+
answers: [{ template: "answer/legacy.html", answer: "614.24" }],
|
|
285
|
+
suggestions: [],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const result = await webSearchTool.execute(
|
|
289
|
+
"call-1",
|
|
290
|
+
{ query: "614.24" },
|
|
291
|
+
undefined,
|
|
292
|
+
undefined,
|
|
293
|
+
{} as any,
|
|
294
|
+
);
|
|
295
|
+
const r = result as any;
|
|
296
|
+
expect(r.content[0].text).toContain("614.24");
|
|
297
|
+
expect(r.content[0].text).not.toContain("No web search results found");
|
|
298
|
+
expect(r.content[0].text).toContain("No web results");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("weather answer uses server summary", async () => {
|
|
302
|
+
mockFetch({
|
|
303
|
+
results: [],
|
|
304
|
+
answers: [
|
|
305
|
+
{
|
|
306
|
+
template: "answer/weather.html",
|
|
307
|
+
current: {
|
|
308
|
+
location: { name: "Berlin", latitude: 52.52, longitude: 13.405 },
|
|
309
|
+
temperature: { val: 14, unit: "ยฐC" },
|
|
310
|
+
condition: "light rain",
|
|
311
|
+
summary: "Berlin: 14ยฐC, light rain",
|
|
312
|
+
feels_like: { val: 12, unit: "ยฐC" },
|
|
313
|
+
humidity: { val: 80, unit: "%" },
|
|
314
|
+
wind_speed: { val: 18, unit: "km/h" },
|
|
315
|
+
wind_from: { name: "NW" },
|
|
316
|
+
},
|
|
317
|
+
service: "open-meteo",
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
suggestions: [],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const result = await webSearchTool.execute(
|
|
324
|
+
"call-2",
|
|
325
|
+
{ query: "weather berlin" },
|
|
326
|
+
undefined,
|
|
327
|
+
undefined,
|
|
328
|
+
{} as any,
|
|
329
|
+
);
|
|
330
|
+
// Uses server-provided summary
|
|
331
|
+
const r = result as any;
|
|
332
|
+
expect(r.content[0].text).toContain("Berlin: 14ยฐC, light rain");
|
|
333
|
+
// Secondary detail line
|
|
334
|
+
expect(r.content[0].text).toContain("feels like 12ยฐC");
|
|
335
|
+
expect(r.content[0].text).toContain("humidity 80%");
|
|
336
|
+
expect(r.content[0].text).toContain("wind 18km/h");
|
|
337
|
+
// forecasts not rendered
|
|
338
|
+
expect(r.content[0].text).not.toContain("forecast");
|
|
339
|
+
// source
|
|
340
|
+
expect(r.content[0].text).toContain("open-meteo");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("unknown template falls back to legacy heuristic", async () => {
|
|
344
|
+
mockFetch({
|
|
345
|
+
results: [],
|
|
346
|
+
answers: [{ template: "answer/future.html", answer: "x" }],
|
|
347
|
+
suggestions: [],
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const result = await webSearchTool.execute(
|
|
351
|
+
"call-3",
|
|
352
|
+
{ query: "future" },
|
|
353
|
+
undefined,
|
|
354
|
+
undefined,
|
|
355
|
+
{} as any,
|
|
356
|
+
);
|
|
357
|
+
const r = result as any;
|
|
358
|
+
expect(r.content[0].text).toContain("x");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("answers + results both present (primary success path)", async () => {
|
|
362
|
+
mockFetch({
|
|
363
|
+
results: [
|
|
364
|
+
{
|
|
365
|
+
title: "Result 1",
|
|
366
|
+
url: "http://example.com/1",
|
|
367
|
+
content: "First result",
|
|
368
|
+
engine: "google",
|
|
369
|
+
score: 0.95,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
answers: [{ template: "answer/legacy.html", answer: "42" }],
|
|
373
|
+
suggestions: [],
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const result = await webSearchTool.execute(
|
|
377
|
+
"call-5",
|
|
378
|
+
{ query: "answer of life" },
|
|
379
|
+
undefined,
|
|
380
|
+
undefined,
|
|
381
|
+
{} as any,
|
|
382
|
+
);
|
|
383
|
+
// Answer above results
|
|
384
|
+
const r = result as any;
|
|
385
|
+
const answerIdx = r.content[0].text.indexOf("42");
|
|
386
|
+
const resultIdx = r.content[0].text.indexOf("Result 1");
|
|
387
|
+
expect(answerIdx).toBeGreaterThanOrEqual(0);
|
|
388
|
+
expect(resultIdx).toBeGreaterThan(answerIdx);
|
|
389
|
+
// Both present
|
|
390
|
+
expect(r.content[0].text).toContain("42");
|
|
391
|
+
expect(r.content[0].text).toContain("Result 1");
|
|
392
|
+
});
|
|
153
393
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-search",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Pi extension. SearXNG search tool for Pi โ pairs with pi-lean-portal's /web toggle; search-only installs are valid, or add it to a portal install for the full web-tools suite.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -18,12 +18,188 @@ interface SearXNGResult {
|
|
|
18
18
|
score?: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface SearXNGAnswerLegacy {
|
|
22
|
+
template: "answer/legacy.html";
|
|
23
|
+
answer: string;
|
|
24
|
+
url?: string;
|
|
25
|
+
engine?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SearXNGAnswerTranslationItem {
|
|
29
|
+
text: string;
|
|
30
|
+
transliteration?: string;
|
|
31
|
+
definitions?: string[];
|
|
32
|
+
synonyms?: string[];
|
|
33
|
+
examples?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SearXNGAnswerTranslations {
|
|
37
|
+
template: "answer/translations.html";
|
|
38
|
+
translations: SearXNGAnswerTranslationItem[];
|
|
39
|
+
url?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface SearXNGWeatherQuantity {
|
|
43
|
+
val: number | string;
|
|
44
|
+
unit: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface SearXNGWeatherItem {
|
|
48
|
+
location?: { name: string; latitude?: number; longitude?: number };
|
|
49
|
+
temperature?: SearXNGWeatherQuantity;
|
|
50
|
+
condition?: string;
|
|
51
|
+
summary?: string;
|
|
52
|
+
feels_like?: SearXNGWeatherQuantity;
|
|
53
|
+
humidity?: SearXNGWeatherQuantity;
|
|
54
|
+
wind_speed?: SearXNGWeatherQuantity;
|
|
55
|
+
wind_from?: string | { name?: string };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SearXNGAnswerWeather {
|
|
59
|
+
template: "answer/weather.html";
|
|
60
|
+
current: SearXNGWeatherItem;
|
|
61
|
+
forecasts?: SearXNGWeatherItem[];
|
|
62
|
+
service?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type SearXNGAnswer =
|
|
66
|
+
| SearXNGAnswerLegacy
|
|
67
|
+
| SearXNGAnswerTranslations
|
|
68
|
+
| SearXNGAnswerWeather;
|
|
69
|
+
|
|
21
70
|
interface SearXNGResponse {
|
|
22
71
|
results: SearXNGResult[];
|
|
23
|
-
answers:
|
|
72
|
+
answers: SearXNGAnswer[];
|
|
24
73
|
suggestions: string[];
|
|
25
74
|
}
|
|
26
75
|
|
|
76
|
+
// โโโ Answer rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
77
|
+
|
|
78
|
+
// Box width (chars). Title row: `โโ <title> ` + dashes to reach BOX_W.
|
|
79
|
+
const BOX_W = 40;
|
|
80
|
+
|
|
81
|
+
function box(title: string, lines: string[]): string {
|
|
82
|
+
const top = `โโ ${title} ${"โ".repeat(Math.max(0, BOX_W - title.length - 4))}`;
|
|
83
|
+
const bottom = `โ${"โ".repeat(BOX_W - 1)}`;
|
|
84
|
+
return `${top}\n${lines.map((l) => `โ ${l}`).join("\n")}\n${bottom}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function trunc(s: string, max: number): string {
|
|
88
|
+
return s.length > max ? s.slice(0, max) + "[โฆ]" : s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function windLine(c: SearXNGWeatherItem): string {
|
|
92
|
+
if (!c.wind_speed) return "";
|
|
93
|
+
let dir = "";
|
|
94
|
+
if (c.wind_from) {
|
|
95
|
+
dir =
|
|
96
|
+
typeof c.wind_from === "object" ? (c.wind_from?.name ?? "") : c.wind_from;
|
|
97
|
+
}
|
|
98
|
+
return `wind ${c.wind_speed.val}${c.wind_speed.unit}${dir ? ` ${dir}` : ""}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderAnswers(answers: SearXNGAnswer[]): string {
|
|
102
|
+
const blocks: string[] = [];
|
|
103
|
+
|
|
104
|
+
for (const a of answers.slice(0, 3)) {
|
|
105
|
+
let title: string;
|
|
106
|
+
let lines: string[];
|
|
107
|
+
switch (a.template) {
|
|
108
|
+
case "answer/legacy.html": {
|
|
109
|
+
title = "Answer";
|
|
110
|
+
lines = [trunc(a.answer, 500)];
|
|
111
|
+
if (a.url) lines.push(`source: ${a.url}`);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "answer/translations.html": {
|
|
115
|
+
const t = a.translations[0];
|
|
116
|
+
if (!t) continue;
|
|
117
|
+
title = "Translation";
|
|
118
|
+
lines = [t.text];
|
|
119
|
+
if (t.synonyms?.length) {
|
|
120
|
+
lines.push(
|
|
121
|
+
` synonyms: ${t.synonyms.slice(0, 2).join(", ")}${t.synonyms.length > 2 ? ", โฆ" : ""}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const more = a.translations.length - 1;
|
|
125
|
+
if (more > 0) {
|
|
126
|
+
lines.push(
|
|
127
|
+
` ${more} more translation${more > 1 ? "s" : ""}${a.url ? ` (source: ${a.url})` : ""}`,
|
|
128
|
+
);
|
|
129
|
+
} else if (a.url) {
|
|
130
|
+
lines.push(`source: ${a.url}`);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "answer/weather.html": {
|
|
135
|
+
const c = a.current;
|
|
136
|
+
if (!c) continue;
|
|
137
|
+
title = "Weather";
|
|
138
|
+
const line1 =
|
|
139
|
+
c.summary ??
|
|
140
|
+
[
|
|
141
|
+
c.location?.name,
|
|
142
|
+
c.temperature ? `${c.temperature.val}${c.temperature.unit}` : "",
|
|
143
|
+
c.condition,
|
|
144
|
+
]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(": ");
|
|
147
|
+
lines = [line1];
|
|
148
|
+
const details: string[] = [];
|
|
149
|
+
if (c.feels_like)
|
|
150
|
+
details.push(`feels like ${c.feels_like.val}${c.feels_like.unit}`);
|
|
151
|
+
if (c.humidity)
|
|
152
|
+
details.push(`humidity ${c.humidity.val}${c.humidity.unit}`);
|
|
153
|
+
if (c.wind_speed) details.push(windLine(c));
|
|
154
|
+
|
|
155
|
+
if (details.length) lines.push(details.join(" ยท "));
|
|
156
|
+
if (a.service) lines.push(`source: ${a.service}`);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
default: {
|
|
160
|
+
const f = a as unknown as Record<string, unknown>;
|
|
161
|
+
if ("answer" in f && typeof f.answer === "string") {
|
|
162
|
+
title = "Answer";
|
|
163
|
+
lines = [trunc(f.answer as string, 500)];
|
|
164
|
+
} else {
|
|
165
|
+
blocks.push(`[answer: ${String(f.template ?? "?")}]`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
blocks.push(box(title, lines));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return blocks.join("\n\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function answerDetail(a: SearXNGAnswer): {
|
|
177
|
+
template: string;
|
|
178
|
+
text: string;
|
|
179
|
+
url?: string;
|
|
180
|
+
} {
|
|
181
|
+
switch (a.template) {
|
|
182
|
+
case "answer/legacy.html":
|
|
183
|
+
return {
|
|
184
|
+
template: a.template,
|
|
185
|
+
text: a.answer,
|
|
186
|
+
...(a.url ? { url: a.url } : {}),
|
|
187
|
+
};
|
|
188
|
+
case "answer/translations.html":
|
|
189
|
+
return {
|
|
190
|
+
template: a.template,
|
|
191
|
+
text: a.translations?.[0]?.text ?? "",
|
|
192
|
+
...(a.url ? { url: a.url } : {}),
|
|
193
|
+
};
|
|
194
|
+
case "answer/weather.html":
|
|
195
|
+
return { template: a.template, text: a.current?.summary ?? "" };
|
|
196
|
+
default: {
|
|
197
|
+
const f = a as unknown as Record<string, unknown>;
|
|
198
|
+
return { template: String(f.template ?? "?"), text: "" };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
27
203
|
// โโโ URL building โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
28
204
|
|
|
29
205
|
function buildSearchUrl(
|
|
@@ -70,6 +246,7 @@ export const webSearchTool = defineTool({
|
|
|
70
246
|
promptGuidelines: [
|
|
71
247
|
'Use when you need recent/current information not already known. Increase `count` for broad research; keep it small for quick lookups. Filter by time_range="day" for breaking news, category="news" for journalism. Set language to match the query (e.g. "de" for German, "es" for Spanish).',
|
|
72
248
|
"Use `pageno` (1-indexed) to fetch deeper pages when `count` results are not enough.",
|
|
249
|
+
"Instant answers (calculator, unit convert, random uuid, hashes, weather, translations) appear above results \u2014 phrase queries like `avg 1 2 3` or `weather berlin` to trigger them.",
|
|
73
250
|
],
|
|
74
251
|
|
|
75
252
|
parameters: Type.Object({
|
|
@@ -317,7 +494,27 @@ export const webSearchTool = defineTool({
|
|
|
317
494
|
// Slice to requested count
|
|
318
495
|
const results = sortedResults.slice(0, Math.min(count, 100));
|
|
319
496
|
|
|
497
|
+
// Render answer blocks (before empty-results check so answers show even with zero web results)
|
|
498
|
+
const renderedAnswers = (data.answers ?? []).slice(0, 3);
|
|
499
|
+
const answerBlocks = renderAnswers(renderedAnswers);
|
|
500
|
+
|
|
320
501
|
if (results.length === 0) {
|
|
502
|
+
if (answerBlocks) {
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: "text" as const,
|
|
507
|
+
text: answerBlocks + `\n\nNo web results for "${query}".`,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
details: {
|
|
511
|
+
results: [],
|
|
512
|
+
query,
|
|
513
|
+
answers: renderedAnswers.map(answerDetail),
|
|
514
|
+
answerCount: renderedAnswers.length,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
}
|
|
321
518
|
return {
|
|
322
519
|
content: [
|
|
323
520
|
{
|
|
@@ -331,7 +528,7 @@ export const webSearchTool = defineTool({
|
|
|
331
528
|
|
|
332
529
|
// Adaptive output formatting
|
|
333
530
|
const maxSnippetLen = count <= 3 ? 300 : 150;
|
|
334
|
-
let output = "";
|
|
531
|
+
let output = answerBlocks ? answerBlocks + "\n\n" : "";
|
|
335
532
|
for (const [i, r] of results.entries()) {
|
|
336
533
|
output += `${i + 1}. ${r.title}\n`;
|
|
337
534
|
output += ` ${r.url}\n`;
|
|
@@ -370,6 +567,8 @@ export const webSearchTool = defineTool({
|
|
|
370
567
|
engine: r.engine,
|
|
371
568
|
score: r.score,
|
|
372
569
|
})),
|
|
570
|
+
answers: renderedAnswers.map(answerDetail),
|
|
571
|
+
answerCount: renderedAnswers.length,
|
|
373
572
|
},
|
|
374
573
|
};
|
|
375
574
|
} catch (unexpectedErr) {
|
|
@@ -438,16 +637,37 @@ export const webSearchTool = defineTool({
|
|
|
438
637
|
| undefined;
|
|
439
638
|
const resultCount = details?.resultCount as number | undefined;
|
|
440
639
|
const query = details?.query as string | undefined;
|
|
640
|
+
const answerCount = details?.answerCount as number | undefined;
|
|
641
|
+
const answers = details?.answers as
|
|
642
|
+
| Array<{ template: string; text: string; url?: string }>
|
|
643
|
+
| undefined;
|
|
644
|
+
|
|
645
|
+
const answerTexts =
|
|
646
|
+
expanded && answers?.length
|
|
647
|
+
? answers.map((a) => theme.fg("accent", a.text)).join("\n")
|
|
648
|
+
: "";
|
|
649
|
+
|
|
650
|
+
if (answerCount && (!results || results.length === 0)) {
|
|
651
|
+
let msg = `${theme.fg("muted", `๐ก ${answerCount} answer(s) for `)}${theme.fg("accent", `"${query ?? "?"}"`)}`;
|
|
652
|
+
if (answerTexts) msg += `\n${answerTexts}`;
|
|
653
|
+
return new Text(msg, 0, 0);
|
|
654
|
+
}
|
|
441
655
|
|
|
442
656
|
if (!results || results.length === 0) {
|
|
443
657
|
const msg = query ? `No results for "${query}"` : "No results";
|
|
444
658
|
return new Text(theme.fg("dim", msg), 0, 0);
|
|
445
659
|
}
|
|
446
660
|
|
|
661
|
+
const answerBadge = answerCount
|
|
662
|
+
? theme.fg("muted", `๐ก ${answerCount} answer(s) ยท `)
|
|
663
|
+
: "";
|
|
447
664
|
let text =
|
|
665
|
+
answerBadge +
|
|
448
666
|
theme.fg("muted", `๐ ${resultCount ?? results.length} result(s) for `) +
|
|
449
667
|
theme.fg("accent", `"${query ?? "?"}"`);
|
|
450
668
|
|
|
669
|
+
if (answerTexts) text += `\n${answerTexts}`;
|
|
670
|
+
|
|
451
671
|
const display = expanded ? results : results.slice(0, 5);
|
|
452
672
|
|
|
453
673
|
for (const r of display) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lean-dimension",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Pi extension. The full Pi web-tools suite in one install โ interactive browsing (pi-lean-portal) plus SearXNG search (pi-lean-search), unified under a /web toggle that removes the tools from context when switched off.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"pi-lean-portal": "^0.2.
|
|
36
|
-
"pi-lean-search": "^0.2.
|
|
35
|
+
"pi-lean-portal": "^0.2.2",
|
|
36
|
+
"pi-lean-search": "^0.2.2"
|
|
37
37
|
},
|
|
38
38
|
"bundledDependencies": [
|
|
39
39
|
"pi-lean-portal",
|