@aliou/pi-synthetic 0.7.0 → 0.8.1
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/package.json +4 -1
- package/src/tools/search.ts +134 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
"@mariozechner/pi-coding-agent": ">=0.52.7",
|
|
31
31
|
"@mariozechner/pi-tui": ">=0.51.0"
|
|
32
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@aliou/pi-utils-ui": "^0.1.2"
|
|
35
|
+
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@aliou/biome-plugins": "^0.3.2",
|
|
35
38
|
"@biomejs/biome": "^2.4.2",
|
package/src/tools/search.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
|
|
1
2
|
import type {
|
|
2
3
|
AgentToolResult,
|
|
3
4
|
ExtensionAPI,
|
|
@@ -5,7 +6,8 @@ import type {
|
|
|
5
6
|
Theme,
|
|
6
7
|
ToolRenderResultOptions,
|
|
7
8
|
} from "@mariozechner/pi-coding-agent";
|
|
8
|
-
import {
|
|
9
|
+
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { Container, Markdown, Text } from "@mariozechner/pi-tui";
|
|
9
11
|
import { type Static, Type } from "@sinclair/typebox";
|
|
10
12
|
|
|
11
13
|
export const SYNTHETIC_WEB_SEARCH_TOOL = "synthetic_web_search" as const;
|
|
@@ -24,8 +26,6 @@ interface SyntheticSearchResponse {
|
|
|
24
26
|
interface WebSearchDetails {
|
|
25
27
|
results?: SyntheticSearchResult[];
|
|
26
28
|
query?: string;
|
|
27
|
-
error?: string;
|
|
28
|
-
isError?: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const SearchParams = Type.Object({
|
|
@@ -58,139 +58,168 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
58
58
|
details: { query: params.query },
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
content: [{ type: "text", text: `Error: ${error}` }],
|
|
67
|
-
details: { error, isError: true },
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const response = await fetch("https://api.synthetic.new/v2/search", {
|
|
72
|
-
method: "POST",
|
|
73
|
-
headers: {
|
|
74
|
-
Authorization: `Bearer ${apiKey}`,
|
|
75
|
-
"Content-Type": "application/json",
|
|
76
|
-
},
|
|
77
|
-
body: JSON.stringify({ query: params.query }),
|
|
78
|
-
signal,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
const errorText = await response.text();
|
|
83
|
-
const error = `Search API error: ${response.status} ${errorText}`;
|
|
84
|
-
return {
|
|
85
|
-
content: [{ type: "text", text: `Error: ${error}` }],
|
|
86
|
-
details: { error, isError: true },
|
|
87
|
-
};
|
|
88
|
-
}
|
|
61
|
+
const apiKey = process.env.SYNTHETIC_API_KEY;
|
|
62
|
+
if (!apiKey) {
|
|
63
|
+
throw new Error("SYNTHETIC_API_KEY is not configured");
|
|
64
|
+
}
|
|
89
65
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
content: [{ type: "text", text: `Error: ${error}` }],
|
|
100
|
-
details: { error, isError: true },
|
|
101
|
-
};
|
|
102
|
-
}
|
|
66
|
+
const response = await fetch("https://api.synthetic.new/v2/search", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${apiKey}`,
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ query: params.query }),
|
|
73
|
+
signal,
|
|
74
|
+
});
|
|
103
75
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
content += `Published: ${result.published}\n`;
|
|
109
|
-
content += `\n${result.text}\n`;
|
|
110
|
-
content += "\n---\n\n";
|
|
111
|
-
}
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorText = await response.text();
|
|
78
|
+
throw new Error(`Search API error: ${response.status} ${errorText}`);
|
|
79
|
+
}
|
|
112
80
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
content: [{ type: "text", text: "Search cancelled" }],
|
|
124
|
-
details: { query: params.query },
|
|
125
|
-
};
|
|
126
|
-
}
|
|
81
|
+
let data: SyntheticSearchResponse;
|
|
82
|
+
try {
|
|
83
|
+
data = await response.json();
|
|
84
|
+
} catch (parseError) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
parseError instanceof Error
|
|
87
|
+
? `Failed to parse search results: ${parseError.message}`
|
|
88
|
+
: "Failed to parse search results",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
127
91
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
92
|
+
let content = `Found ${data.results.length} result(s):\n\n`;
|
|
93
|
+
for (const result of data.results) {
|
|
94
|
+
content += `## ${result.title}\n`;
|
|
95
|
+
content += `URL: ${result.url}\n`;
|
|
96
|
+
content += `Published: ${result.published}\n`;
|
|
97
|
+
content += `\n${result.text}\n`;
|
|
98
|
+
content += "\n---\n\n";
|
|
134
99
|
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: content }],
|
|
103
|
+
details: {
|
|
104
|
+
results: data.results,
|
|
105
|
+
query: params.query,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
135
108
|
},
|
|
136
109
|
|
|
137
|
-
renderCall(args: SearchParamsType, theme: Theme)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
110
|
+
renderCall(args: SearchParamsType, theme: Theme) {
|
|
111
|
+
return new ToolCallHeader(
|
|
112
|
+
{
|
|
113
|
+
toolName: "Synthetic: WebSearch",
|
|
114
|
+
mainArg: `"${args.query}"`,
|
|
115
|
+
showColon: true,
|
|
116
|
+
},
|
|
117
|
+
theme,
|
|
118
|
+
);
|
|
141
119
|
},
|
|
142
120
|
|
|
143
121
|
renderResult(
|
|
144
122
|
result: AgentToolResult<WebSearchDetails>,
|
|
145
123
|
options: ToolRenderResultOptions,
|
|
146
124
|
theme: Theme,
|
|
147
|
-
)
|
|
125
|
+
) {
|
|
148
126
|
const { expanded, isPartial } = options;
|
|
127
|
+
const SNIPPET_LINES = 5;
|
|
149
128
|
|
|
150
129
|
if (isPartial) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
130
|
+
return new Text(
|
|
131
|
+
theme.fg("muted", "Synthetic: WebSearch: fetching..."),
|
|
132
|
+
0,
|
|
133
|
+
0,
|
|
134
|
+
);
|
|
156
135
|
}
|
|
157
136
|
|
|
158
137
|
const details = result.details;
|
|
159
|
-
|
|
138
|
+
const results = details?.results || [];
|
|
139
|
+
const container = new Container();
|
|
140
|
+
|
|
141
|
+
// When the tool throws, the framework calls renderResult with
|
|
142
|
+
// details={} (empty object) and the error message in content.
|
|
143
|
+
// Detect this by checking for missing results in details.
|
|
144
|
+
if (!details?.results) {
|
|
145
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
160
146
|
const errorMsg =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
147
|
+
(textBlock?.type === "text" && textBlock.text) || "Search failed";
|
|
148
|
+
container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
|
|
149
|
+
return container;
|
|
165
150
|
}
|
|
166
151
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
152
|
+
if (results.length === 0) {
|
|
153
|
+
container.addChild(
|
|
154
|
+
new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
|
|
155
|
+
);
|
|
156
|
+
} else if (!expanded) {
|
|
157
|
+
// Collapsed: show result count + first result title
|
|
158
|
+
let text = theme.fg("success", `Found ${results.length} result(s)`);
|
|
171
159
|
const first = results[0];
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
160
|
+
if (first) {
|
|
161
|
+
text += `\n ${theme.fg("dim", first.title)}`;
|
|
162
|
+
if (results.length > 1) {
|
|
163
|
+
text += theme.fg("dim", ` (+${results.length - 1} more)`);
|
|
164
|
+
}
|
|
175
165
|
}
|
|
176
|
-
text += theme.fg("muted", "
|
|
177
|
-
|
|
166
|
+
text += theme.fg("muted", ` ${keyHint("expandTools", "to expand")}`);
|
|
167
|
+
container.addChild(new Text(text, 0, 0));
|
|
168
|
+
} else {
|
|
169
|
+
// Expanded: show each result with title, URL, date, and snippet
|
|
170
|
+
container.addChild(
|
|
171
|
+
new Text(
|
|
172
|
+
theme.fg("success", `Found ${results.length} result(s)`),
|
|
173
|
+
0,
|
|
174
|
+
0,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
178
177
|
|
|
179
|
-
if (expanded) {
|
|
180
178
|
for (const r of results) {
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
container.addChild(new Text("", 0, 0));
|
|
180
|
+
container.addChild(
|
|
181
|
+
new Text(
|
|
182
|
+
`${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
|
|
183
|
+
0,
|
|
184
|
+
0,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
|
|
188
|
+
if (r.published) {
|
|
189
|
+
container.addChild(
|
|
190
|
+
new Text(
|
|
191
|
+
` ${theme.fg("muted", `Published: ${r.published}`)}`,
|
|
192
|
+
0,
|
|
193
|
+
0,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
183
198
|
if (r.text) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
container.addChild(new Text("", 0, 0));
|
|
200
|
+
const snippet = r.text
|
|
201
|
+
.split("\n")
|
|
202
|
+
.slice(0, SNIPPET_LINES)
|
|
203
|
+
.map((line) => `> ${line}`)
|
|
204
|
+
.join("\n");
|
|
205
|
+
container.addChild(
|
|
206
|
+
new Markdown(snippet, 0, 0, getMarkdownTheme(), {
|
|
207
|
+
color: (text: string) => theme.fg("toolOutput", text),
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
189
210
|
}
|
|
190
211
|
}
|
|
191
212
|
}
|
|
192
213
|
|
|
193
|
-
|
|
214
|
+
container.addChild(new Text("", 0, 0));
|
|
215
|
+
container.addChild(
|
|
216
|
+
new ToolFooter(theme, {
|
|
217
|
+
items: [{ label: "results", value: `${results.length} result(s)` }],
|
|
218
|
+
separator: " | ",
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return container;
|
|
194
223
|
},
|
|
195
224
|
});
|
|
196
225
|
}
|