test: add web tool output formatting
This commit is contained in:
53
.pi/agent/extensions/web-search/src/format.test.ts
Normal file
53
.pi/agent/extensions/web-search/src/format.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { formatFetchOutput, formatSearchOutput, truncateText } from "./format.ts";
|
||||
|
||||
test("formatSearchOutput renders a compact metadata-only list", () => {
|
||||
const output = formatSearchOutput({
|
||||
providerName: "exa-main",
|
||||
results: [
|
||||
{
|
||||
title: "Exa Docs",
|
||||
url: "https://exa.ai/docs",
|
||||
publishedDate: "2026-04-09",
|
||||
author: "Exa",
|
||||
score: 0.98,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.match(output, /Found 1 web result via exa-main:/);
|
||||
assert.match(output, /Exa Docs/);
|
||||
assert.match(output, /https:\/\/exa.ai\/docs/);
|
||||
});
|
||||
|
||||
test("truncateText shortens long fetch bodies with an ellipsis", () => {
|
||||
assert.equal(truncateText("abcdef", 4), "abc…");
|
||||
assert.equal(truncateText("abc", 10), "abc");
|
||||
});
|
||||
|
||||
test("formatFetchOutput includes both successful and failed URLs", () => {
|
||||
const output = formatFetchOutput(
|
||||
{
|
||||
providerName: "exa-main",
|
||||
results: [
|
||||
{
|
||||
url: "https://good.example",
|
||||
title: "Good",
|
||||
text: "This is a very long body that should be truncated in the final output.",
|
||||
},
|
||||
{
|
||||
url: "https://bad.example",
|
||||
title: null,
|
||||
error: "429 rate limited",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ maxCharactersPerResult: 20 },
|
||||
);
|
||||
|
||||
assert.match(output, /Status: ok/);
|
||||
assert.match(output, /Status: failed/);
|
||||
assert.match(output, /429 rate limited/);
|
||||
assert.match(output, /This is a very long…/);
|
||||
});
|
||||
76
.pi/agent/extensions/web-search/src/format.ts
Normal file
76
.pi/agent/extensions/web-search/src/format.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { NormalizedFetchResponse, NormalizedSearchResponse } from "./providers/types.ts";
|
||||
|
||||
export function truncateText(text: string, maxCharacters = 4000) {
|
||||
if (text.length <= maxCharacters) {
|
||||
return text;
|
||||
}
|
||||
return `${text.slice(0, Math.max(0, maxCharacters - 1))}…`;
|
||||
}
|
||||
|
||||
export function formatSearchOutput(response: NormalizedSearchResponse) {
|
||||
if (response.results.length === 0) {
|
||||
return `No web results via ${response.providerName}.`;
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`Found ${response.results.length} web result${response.results.length === 1 ? "" : "s"} via ${response.providerName}:`,
|
||||
];
|
||||
|
||||
for (const [index, result] of response.results.entries()) {
|
||||
lines.push(`${index + 1}. ${result.title ?? "(untitled)"}`);
|
||||
lines.push(` URL: ${result.url}`);
|
||||
|
||||
const meta = [result.publishedDate, result.author].filter(Boolean);
|
||||
if (meta.length > 0) {
|
||||
lines.push(` Meta: ${meta.join(" • ")}`);
|
||||
}
|
||||
|
||||
if (typeof result.score === "number") {
|
||||
lines.push(` Score: ${result.score}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export interface FetchFormatOptions {
|
||||
maxCharactersPerResult?: number;
|
||||
}
|
||||
|
||||
export function formatFetchOutput(response: NormalizedFetchResponse, options: FetchFormatOptions = {}) {
|
||||
const maxCharactersPerResult = options.maxCharactersPerResult ?? 4000;
|
||||
const lines = [
|
||||
`Fetched ${response.results.length} URL${response.results.length === 1 ? "" : "s"} via ${response.providerName}:`,
|
||||
];
|
||||
|
||||
for (const result of response.results) {
|
||||
lines.push("");
|
||||
lines.push(`URL: ${result.url}`);
|
||||
|
||||
if (result.error) {
|
||||
lines.push("Status: failed");
|
||||
lines.push(`Error: ${result.error}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push("Status: ok");
|
||||
if (result.title) {
|
||||
lines.push(`Title: ${result.title}`);
|
||||
}
|
||||
if (result.summary) {
|
||||
lines.push(`Summary: ${result.summary}`);
|
||||
}
|
||||
if (result.highlights?.length) {
|
||||
lines.push("Highlights:");
|
||||
for (const highlight of result.highlights) {
|
||||
lines.push(`- ${highlight}`);
|
||||
}
|
||||
}
|
||||
if (result.text) {
|
||||
lines.push("Text:");
|
||||
lines.push(truncateText(result.text, maxCharactersPerResult));
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -737,7 +737,7 @@ git -C /home/alex/dotfiles commit -m "test: add exa web provider adapter"
|
||||
- Create: `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.ts`
|
||||
- Test: `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing formatter tests**
|
||||
- [x] **Step 1: Write the failing formatter tests**
|
||||
|
||||
Create `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.test.ts` with this content:
|
||||
|
||||
@@ -797,7 +797,7 @@ test("formatFetchOutput includes both successful and failed URLs", () => {
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the formatter tests to verify they fail**
|
||||
- [x] **Step 2: Run the formatter tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -808,7 +808,7 @@ npx tsx --test src/format.test.ts
|
||||
|
||||
Expected: `FAIL` with an `ERR_MODULE_NOT_FOUND` error for `./format.ts`.
|
||||
|
||||
- [ ] **Step 3: Write the minimal formatter implementation**
|
||||
- [x] **Step 3: Write the minimal formatter implementation**
|
||||
|
||||
Create `/home/alex/dotfiles/.pi/agent/extensions/web-search/src/format.ts` with this content:
|
||||
|
||||
@@ -891,7 +891,7 @@ export function formatFetchOutput(response: NormalizedFetchResponse, options: Fe
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the formatter tests to verify they pass**
|
||||
- [x] **Step 4: Run the formatter tests to verify they pass**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -902,7 +902,7 @@ npx tsx --test src/format.test.ts
|
||||
|
||||
Expected: `PASS` for all three tests.
|
||||
|
||||
- [ ] **Step 5: Commit the formatter layer**
|
||||
- [x] **Step 5: Commit the formatter layer**
|
||||
|
||||
Run:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user