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`
|
- 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`
|
- 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:
|
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:
|
Run:
|
||||||
|
|
||||||
@@ -808,7 +808,7 @@ npx tsx --test src/format.test.ts
|
|||||||
|
|
||||||
Expected: `FAIL` with an `ERR_MODULE_NOT_FOUND` error for `./format.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:
|
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:
|
Run:
|
||||||
|
|
||||||
@@ -902,7 +902,7 @@ npx tsx --test src/format.test.ts
|
|||||||
|
|
||||||
Expected: `PASS` for all three tests.
|
Expected: `PASS` for all three tests.
|
||||||
|
|
||||||
- [ ] **Step 5: Commit the formatter layer**
|
- [x] **Step 5: Commit the formatter layer**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user