@demigodmode/pi-web-agent 0.2.1 → 0.3.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/README.md +65 -145
- package/dist/commands/web-agent-config.d.ts +23 -0
- package/dist/commands/web-agent-config.js +254 -0
- package/dist/extension.js +113 -4
- package/dist/presentation/config-store.d.ts +23 -0
- package/dist/presentation/config-store.js +64 -0
- package/dist/presentation/config.d.ts +7 -0
- package/dist/presentation/config.js +44 -0
- package/dist/presentation/explore-presentation.d.ts +3 -0
- package/dist/presentation/explore-presentation.js +34 -0
- package/dist/presentation/fetch-presentation.d.ts +5 -0
- package/dist/presentation/fetch-presentation.js +40 -0
- package/dist/presentation/search-presentation.d.ts +3 -0
- package/dist/presentation/search-presentation.js +30 -0
- package/dist/presentation/select-view.d.ts +2 -0
- package/dist/presentation/select-view.js +12 -0
- package/dist/presentation/types.d.ts +50 -0
- package/dist/presentation/types.js +1 -0
- package/dist/search/duckduckgo.d.ts +6 -1
- package/dist/search/duckduckgo.js +11 -1
- package/dist/tools/web-explore.d.ts +6 -16
- package/dist/tools/web-explore.js +12 -10
- package/dist/tools/web-fetch-headless.js +11 -2
- package/dist/tools/web-fetch.js +11 -2
- package/dist/tools/web-search.js +99 -12
- package/dist/types.d.ts +15 -0
- package/package.json +5 -1
package/dist/extension.js
CHANGED
|
@@ -1,14 +1,111 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { registerWebAgentConfigCommands } from './commands/web-agent-config.js';
|
|
3
|
+
import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presentation/config.js';
|
|
4
|
+
import { loadPresentationConfigLayers } from './presentation/config-store.js';
|
|
5
|
+
import { selectPresentationView } from './presentation/select-view.js';
|
|
2
6
|
import { createWebExploreTool } from './tools/web-explore.js';
|
|
3
7
|
import { createWebFetchTool } from './tools/web-fetch.js';
|
|
4
8
|
import { createWebFetchHeadlessTool } from './tools/web-fetch-headless.js';
|
|
5
9
|
import { createWebSearchTool } from './tools/web-search.js';
|
|
10
|
+
async function getEffectivePresentationConfig(pi) {
|
|
11
|
+
const store = pi.__presentationConfigStore;
|
|
12
|
+
try {
|
|
13
|
+
const loaded = await (store?.load?.() ?? loadPresentationConfigLayers());
|
|
14
|
+
return loaded.effectiveConfig;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return DEFAULT_PRESENTATION_CONFIG;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function renderToolText(pi, toolName, details) {
|
|
21
|
+
const config = await getEffectivePresentationConfig(pi);
|
|
22
|
+
const mode = resolvePresentationMode(toolName, config);
|
|
23
|
+
return selectPresentationView(details.presentation, mode) ?? JSON.stringify(details, null, 2);
|
|
24
|
+
}
|
|
6
25
|
export default function extension(pi) {
|
|
26
|
+
registerWebAgentConfigCommands(pi);
|
|
7
27
|
const webSearch = createWebSearchTool();
|
|
8
28
|
const webFetch = createWebFetchTool();
|
|
9
29
|
const webFetchHeadless = createWebFetchHeadlessTool();
|
|
10
30
|
const webExplore = createWebExploreTool();
|
|
31
|
+
let webExploreUsedInCurrentFlow = false;
|
|
32
|
+
const postWebExploreGuardError = {
|
|
33
|
+
code: 'POST_WEB_EXPLORE_GUARD',
|
|
34
|
+
message: 'web_explore already ran for this research task. Only use low-level web tools if there is a specific unresolved gap.'
|
|
35
|
+
};
|
|
36
|
+
async function guardSearchResponse() {
|
|
37
|
+
const result = {
|
|
38
|
+
status: 'error',
|
|
39
|
+
results: [],
|
|
40
|
+
metadata: {
|
|
41
|
+
backend: 'duckduckgo',
|
|
42
|
+
cacheHit: false
|
|
43
|
+
},
|
|
44
|
+
error: postWebExploreGuardError,
|
|
45
|
+
presentation: {
|
|
46
|
+
mode: 'compact',
|
|
47
|
+
views: {
|
|
48
|
+
compact: `Search failed: ${postWebExploreGuardError.message}`
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: await renderToolText(pi, 'web_search', result) }],
|
|
54
|
+
details: result,
|
|
55
|
+
isError: true
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function guardFetchResponse(url) {
|
|
59
|
+
const result = {
|
|
60
|
+
status: 'error',
|
|
61
|
+
url,
|
|
62
|
+
metadata: {
|
|
63
|
+
method: 'http',
|
|
64
|
+
cacheHit: false
|
|
65
|
+
},
|
|
66
|
+
error: postWebExploreGuardError,
|
|
67
|
+
presentation: {
|
|
68
|
+
mode: 'compact',
|
|
69
|
+
views: {
|
|
70
|
+
compact: `Fetch failed: ${postWebExploreGuardError.message}`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch', result) }],
|
|
76
|
+
details: result,
|
|
77
|
+
isError: true
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function guardHeadlessResponse(url) {
|
|
81
|
+
const result = {
|
|
82
|
+
status: 'error',
|
|
83
|
+
url,
|
|
84
|
+
metadata: {
|
|
85
|
+
method: 'headless',
|
|
86
|
+
cacheHit: false
|
|
87
|
+
},
|
|
88
|
+
error: postWebExploreGuardError,
|
|
89
|
+
presentation: {
|
|
90
|
+
mode: 'compact',
|
|
91
|
+
views: {
|
|
92
|
+
compact: `Fetch failed: ${postWebExploreGuardError.message}`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: await renderToolText(pi, 'web_fetch_headless', result)
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
details: result,
|
|
104
|
+
isError: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
11
107
|
pi.on('before_agent_start', async (event) => {
|
|
108
|
+
webExploreUsedInCurrentFlow = false;
|
|
12
109
|
return {
|
|
13
110
|
systemPrompt: `${event.systemPrompt}\n\n` +
|
|
14
111
|
'For web research questions that require finding and comparing multiple sources, prefer web_explore. ' +
|
|
@@ -25,9 +122,12 @@ export default function extension(pi) {
|
|
|
25
122
|
query: Type.String({ description: 'Search query.' })
|
|
26
123
|
}),
|
|
27
124
|
async execute(_toolCallId, params) {
|
|
125
|
+
if (webExploreUsedInCurrentFlow) {
|
|
126
|
+
return guardSearchResponse();
|
|
127
|
+
}
|
|
28
128
|
const result = await webSearch({ query: params.query });
|
|
29
129
|
return {
|
|
30
|
-
content: [{ type: 'text', text:
|
|
130
|
+
content: [{ type: 'text', text: await renderToolText(pi, 'web_search', result) }],
|
|
31
131
|
details: result,
|
|
32
132
|
isError: result.status === 'error'
|
|
33
133
|
};
|
|
@@ -41,9 +141,12 @@ export default function extension(pi) {
|
|
|
41
141
|
url: Type.String({ description: 'HTTP or HTTPS URL to fetch.' })
|
|
42
142
|
}),
|
|
43
143
|
async execute(_toolCallId, params) {
|
|
144
|
+
if (webExploreUsedInCurrentFlow) {
|
|
145
|
+
return guardFetchResponse(params.url);
|
|
146
|
+
}
|
|
44
147
|
const result = await webFetch({ url: params.url });
|
|
45
148
|
return {
|
|
46
|
-
content: [{ type: 'text', text:
|
|
149
|
+
content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch', result) }],
|
|
47
150
|
details: result,
|
|
48
151
|
isError: result.status === 'error'
|
|
49
152
|
};
|
|
@@ -57,9 +160,12 @@ export default function extension(pi) {
|
|
|
57
160
|
url: Type.String({ description: 'HTTP or HTTPS URL to fetch in headless mode.' })
|
|
58
161
|
}),
|
|
59
162
|
async execute(_toolCallId, params) {
|
|
163
|
+
if (webExploreUsedInCurrentFlow) {
|
|
164
|
+
return guardHeadlessResponse(params.url);
|
|
165
|
+
}
|
|
60
166
|
const result = await webFetchHeadless({ url: params.url });
|
|
61
167
|
return {
|
|
62
|
-
content: [{ type: 'text', text:
|
|
168
|
+
content: [{ type: 'text', text: await renderToolText(pi, 'web_fetch_headless', result) }],
|
|
63
169
|
details: result,
|
|
64
170
|
isError: result.status === 'error'
|
|
65
171
|
};
|
|
@@ -74,11 +180,14 @@ export default function extension(pi) {
|
|
|
74
180
|
}),
|
|
75
181
|
async execute(_toolCallId, params) {
|
|
76
182
|
const result = await webExplore({ query: params.query });
|
|
183
|
+
if (result.status === 'ok') {
|
|
184
|
+
webExploreUsedInCurrentFlow = true;
|
|
185
|
+
}
|
|
77
186
|
return {
|
|
78
187
|
content: [
|
|
79
188
|
{
|
|
80
189
|
type: 'text',
|
|
81
|
-
text:
|
|
190
|
+
text: await renderToolText(pi, 'web_explore', result)
|
|
82
191
|
}
|
|
83
192
|
],
|
|
84
193
|
details: result,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from './types.js';
|
|
2
|
+
export type PresentationConfigStoreOptions = {
|
|
3
|
+
homeDir?: string;
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
};
|
|
6
|
+
export type PresentationConfigLayer = {
|
|
7
|
+
path: string;
|
|
8
|
+
exists: boolean;
|
|
9
|
+
rawConfig?: PresentationConfigOverride;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
export type LoadedPresentationConfig = {
|
|
13
|
+
global: PresentationConfigLayer;
|
|
14
|
+
project: PresentationConfigLayer;
|
|
15
|
+
effectiveConfig: PresentationConfig;
|
|
16
|
+
};
|
|
17
|
+
export declare function getPresentationConfigPaths(options?: PresentationConfigStoreOptions): {
|
|
18
|
+
globalPath: string;
|
|
19
|
+
projectPath: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function loadPresentationConfigLayers(options?: PresentationConfigStoreOptions): Promise<LoadedPresentationConfig>;
|
|
22
|
+
export declare function savePresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope, config: PresentationConfigOverride): Promise<void>;
|
|
23
|
+
export declare function resetPresentationConfigScope(options: PresentationConfigStoreOptions, scope: PresentationScope): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_PRESENTATION_CONFIG, extractPresentationConfigOverride, mergePresentationConfigLayers } from './config.js';
|
|
4
|
+
export function getPresentationConfigPaths(options = {}) {
|
|
5
|
+
const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
6
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
7
|
+
return {
|
|
8
|
+
globalPath: path.join(homeDir, '.pi', 'agent', 'extensions', 'pi-web-agent', 'config.json'),
|
|
9
|
+
projectPath: path.join(projectDir, '.pi', 'extensions', 'pi-web-agent', 'config.json')
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
async function readPresentationConfigFile(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
const rawText = await readFile(filePath, 'utf8');
|
|
15
|
+
const parsed = JSON.parse(rawText);
|
|
16
|
+
return {
|
|
17
|
+
path: filePath,
|
|
18
|
+
exists: true,
|
|
19
|
+
rawConfig: extractPresentationConfigOverride(parsed)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error?.code === 'ENOENT') {
|
|
24
|
+
return { path: filePath, exists: false };
|
|
25
|
+
}
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
return {
|
|
28
|
+
path: filePath,
|
|
29
|
+
exists: true,
|
|
30
|
+
error: message
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function serializePresentationConfigOverride(config) {
|
|
35
|
+
const presentation = {};
|
|
36
|
+
if (config.defaultMode) {
|
|
37
|
+
presentation.defaultMode = config.defaultMode;
|
|
38
|
+
}
|
|
39
|
+
if (Object.keys(config.tools).length > 0) {
|
|
40
|
+
presentation.tools = config.tools;
|
|
41
|
+
}
|
|
42
|
+
return { presentation };
|
|
43
|
+
}
|
|
44
|
+
export async function loadPresentationConfigLayers(options = {}) {
|
|
45
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
46
|
+
const global = await readPresentationConfigFile(globalPath);
|
|
47
|
+
const project = await readPresentationConfigFile(projectPath);
|
|
48
|
+
return {
|
|
49
|
+
global,
|
|
50
|
+
project,
|
|
51
|
+
effectiveConfig: mergePresentationConfigLayers(DEFAULT_PRESENTATION_CONFIG, global.rawConfig, project.rawConfig)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function savePresentationConfigScope(options, scope, config) {
|
|
55
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
56
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
57
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
58
|
+
await writeFile(filePath, JSON.stringify(serializePresentationConfigOverride(config), null, 2) + '\n', 'utf8');
|
|
59
|
+
}
|
|
60
|
+
export async function resetPresentationConfigScope(options, scope) {
|
|
61
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
62
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
63
|
+
await rm(filePath, { force: true });
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type PresentationConfig, type PresentationConfigFile, type PresentationConfigOverride, type PresentationMode, type PresentationToolName } from './types.js';
|
|
2
|
+
export declare const DEFAULT_PRESENTATION_CONFIG: PresentationConfig;
|
|
3
|
+
export declare function isPresentationMode(value: unknown): value is PresentationMode;
|
|
4
|
+
export declare function extractPresentationConfigOverride(file: PresentationConfigFile | null | undefined): PresentationConfigOverride;
|
|
5
|
+
export declare function normalizePresentationConfigFile(file: PresentationConfigFile | null | undefined): PresentationConfig;
|
|
6
|
+
export declare function mergePresentationConfigLayers(defaults: PresentationConfig, globalConfig?: PresentationConfigOverride, projectConfig?: PresentationConfigOverride): PresentationConfig;
|
|
7
|
+
export declare function resolvePresentationMode(toolName: PresentationToolName, config?: PresentationConfig): PresentationMode;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PRESENTATION_MODES } from './types.js';
|
|
2
|
+
const PRESENTATION_MODE_SET = new Set(PRESENTATION_MODES);
|
|
3
|
+
export const DEFAULT_PRESENTATION_CONFIG = {
|
|
4
|
+
defaultMode: 'compact',
|
|
5
|
+
tools: {}
|
|
6
|
+
};
|
|
7
|
+
export function isPresentationMode(value) {
|
|
8
|
+
return typeof value === 'string' && PRESENTATION_MODE_SET.has(value);
|
|
9
|
+
}
|
|
10
|
+
export function extractPresentationConfigOverride(file) {
|
|
11
|
+
const presentation = file?.presentation;
|
|
12
|
+
const tools = Object.fromEntries(Object.entries(presentation?.tools ?? {}).flatMap(([toolName, value]) => {
|
|
13
|
+
if (!value || !isPresentationMode(value.mode)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return [[toolName, { mode: value.mode }]];
|
|
17
|
+
}));
|
|
18
|
+
return {
|
|
19
|
+
defaultMode: isPresentationMode(presentation?.defaultMode)
|
|
20
|
+
? presentation.defaultMode
|
|
21
|
+
: undefined,
|
|
22
|
+
tools
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function normalizePresentationConfigFile(file) {
|
|
26
|
+
const override = extractPresentationConfigOverride(file);
|
|
27
|
+
return {
|
|
28
|
+
defaultMode: override.defaultMode ?? DEFAULT_PRESENTATION_CONFIG.defaultMode,
|
|
29
|
+
tools: override.tools
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function mergePresentationConfigLayers(defaults, globalConfig, projectConfig) {
|
|
33
|
+
return {
|
|
34
|
+
defaultMode: projectConfig?.defaultMode ?? globalConfig?.defaultMode ?? defaults.defaultMode,
|
|
35
|
+
tools: {
|
|
36
|
+
...defaults.tools,
|
|
37
|
+
...globalConfig?.tools,
|
|
38
|
+
...projectConfig?.tools
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function resolvePresentationMode(toolName, config = DEFAULT_PRESENTATION_CONFIG) {
|
|
43
|
+
return config.tools[toolName]?.mode ?? config.defaultMode;
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function buildExplorePresentation(result) {
|
|
2
|
+
if (result.status === 'error') {
|
|
3
|
+
return {
|
|
4
|
+
mode: 'compact',
|
|
5
|
+
views: {
|
|
6
|
+
compact: `Research failed: ${result.error?.message ?? 'Unknown research failure.'}`
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const preview = result.findings.map((finding) => `- ${finding}`).join('\n');
|
|
11
|
+
const verbose = [
|
|
12
|
+
'Findings',
|
|
13
|
+
...result.findings.map((finding) => `- ${finding}`),
|
|
14
|
+
'',
|
|
15
|
+
'Sources',
|
|
16
|
+
...result.sources.map((source) => `- ${source.title}: ${source.url}`),
|
|
17
|
+
result.caveat ? `\nCaveat\n${result.caveat}` : undefined
|
|
18
|
+
]
|
|
19
|
+
.filter((line) => line !== undefined)
|
|
20
|
+
.join('\n');
|
|
21
|
+
return {
|
|
22
|
+
mode: 'compact',
|
|
23
|
+
views: {
|
|
24
|
+
compact: `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`,
|
|
25
|
+
preview,
|
|
26
|
+
verbose
|
|
27
|
+
},
|
|
28
|
+
metrics: {
|
|
29
|
+
sourceCount: result.sources.length,
|
|
30
|
+
resultCount: result.findings.length
|
|
31
|
+
},
|
|
32
|
+
sources: result.sources
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
2
|
+
import type { PresentationEnvelope } from './types.js';
|
|
3
|
+
type FetchLike = WebFetchResponse | WebFetchHeadlessResponse;
|
|
4
|
+
export declare function buildFetchPresentation(result: FetchLike): PresentationEnvelope;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function countWords(text) {
|
|
2
|
+
return text?.trim() ? text.trim().split(/\s+/).length : undefined;
|
|
3
|
+
}
|
|
4
|
+
function firstExcerpt(text, maxChars = 240) {
|
|
5
|
+
if (!text) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
return text.length <= maxChars ? text : `${text.slice(0, maxChars).trimEnd()}…`;
|
|
9
|
+
}
|
|
10
|
+
export function buildFetchPresentation(result) {
|
|
11
|
+
const wordCount = countWords(result.content?.text);
|
|
12
|
+
const compact = result.status === 'ok'
|
|
13
|
+
? `Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
|
|
14
|
+
: result.status === 'needs_headless'
|
|
15
|
+
? `Needs headless rendering: ${result.error?.message ?? 'Headless rendering recommended.'}`
|
|
16
|
+
: `Fetch failed: ${result.error?.message ?? 'Unknown fetch failure.'}`;
|
|
17
|
+
return {
|
|
18
|
+
mode: 'compact',
|
|
19
|
+
views: {
|
|
20
|
+
compact,
|
|
21
|
+
preview: result.content?.title
|
|
22
|
+
? `${result.content.title}\n${firstExcerpt(result.content.text) ?? ''}`.trim()
|
|
23
|
+
: firstExcerpt(result.content?.text),
|
|
24
|
+
verbose: result.status === 'ok'
|
|
25
|
+
? [
|
|
26
|
+
`URL: ${result.url}`,
|
|
27
|
+
result.content?.title ? `Title: ${result.content.title}` : undefined,
|
|
28
|
+
firstExcerpt(result.content?.text, 500)
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join('\n')
|
|
32
|
+
: undefined
|
|
33
|
+
},
|
|
34
|
+
metrics: {
|
|
35
|
+
wordCount,
|
|
36
|
+
cacheHit: result.metadata.cacheHit
|
|
37
|
+
},
|
|
38
|
+
sources: [{ title: result.content?.title ?? result.url, url: result.url }]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function formatCompact(result) {
|
|
2
|
+
if (result.status === 'error') {
|
|
3
|
+
return `Search failed: ${result.error?.message ?? 'Unknown search failure.'}`;
|
|
4
|
+
}
|
|
5
|
+
const suffix = result.results.length === 1 ? 'result' : 'results';
|
|
6
|
+
return `Found ${result.results.length} ${suffix}`;
|
|
7
|
+
}
|
|
8
|
+
export function buildSearchPresentation(result) {
|
|
9
|
+
const preview = result.results
|
|
10
|
+
.slice(0, 3)
|
|
11
|
+
.map((item, index) => `${index + 1}. ${item.title}`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
const verbose = result.results
|
|
14
|
+
.slice(0, 5)
|
|
15
|
+
.map((item, index) => `${index + 1}. ${item.title}\n ${item.url}\n ${item.snippet}`)
|
|
16
|
+
.join('\n');
|
|
17
|
+
return {
|
|
18
|
+
mode: 'compact',
|
|
19
|
+
views: {
|
|
20
|
+
compact: formatCompact(result),
|
|
21
|
+
preview: preview || undefined,
|
|
22
|
+
verbose: verbose || undefined
|
|
23
|
+
},
|
|
24
|
+
metrics: {
|
|
25
|
+
resultCount: result.results.length,
|
|
26
|
+
cacheHit: result.metadata.cacheHit
|
|
27
|
+
},
|
|
28
|
+
sources: result.results.map((item) => ({ title: item.title, url: item.url }))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function selectPresentationView(envelope, requestedMode) {
|
|
2
|
+
if (!envelope) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
if (requestedMode === 'preview' && envelope.views.preview) {
|
|
6
|
+
return envelope.views.preview;
|
|
7
|
+
}
|
|
8
|
+
if (requestedMode === 'verbose' && envelope.views.verbose) {
|
|
9
|
+
return envelope.views.verbose;
|
|
10
|
+
}
|
|
11
|
+
return envelope.views.compact;
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const PRESENTATION_MODES: readonly ["compact", "preview", "verbose"];
|
|
2
|
+
export type PresentationMode = (typeof PRESENTATION_MODES)[number];
|
|
3
|
+
export type PresentationViews = {
|
|
4
|
+
compact: string;
|
|
5
|
+
preview?: string;
|
|
6
|
+
verbose?: string;
|
|
7
|
+
};
|
|
8
|
+
export type PresentationMetrics = {
|
|
9
|
+
durationMs?: number;
|
|
10
|
+
resultCount?: number;
|
|
11
|
+
sourceCount?: number;
|
|
12
|
+
wordCount?: number;
|
|
13
|
+
statusCode?: number;
|
|
14
|
+
cacheHit?: boolean;
|
|
15
|
+
truncated?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type PresentationSource = {
|
|
18
|
+
title: string;
|
|
19
|
+
url: string;
|
|
20
|
+
domain?: string;
|
|
21
|
+
};
|
|
22
|
+
export type PresentationEnvelope = {
|
|
23
|
+
mode: PresentationMode;
|
|
24
|
+
views: PresentationViews;
|
|
25
|
+
metrics?: PresentationMetrics;
|
|
26
|
+
sources?: PresentationSource[];
|
|
27
|
+
debug?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
export type PresentationToolName = 'web_search' | 'web_fetch' | 'web_fetch_headless' | 'web_explore';
|
|
30
|
+
export type PresentationScope = 'global' | 'project';
|
|
31
|
+
export type PresentationToolOverrideMode = PresentationMode;
|
|
32
|
+
export type PresentationToolConfig = {
|
|
33
|
+
mode: PresentationToolOverrideMode;
|
|
34
|
+
};
|
|
35
|
+
export type PresentationConfig = {
|
|
36
|
+
defaultMode: PresentationMode;
|
|
37
|
+
tools: Partial<Record<PresentationToolName, PresentationToolConfig>>;
|
|
38
|
+
};
|
|
39
|
+
export type PresentationConfigOverride = {
|
|
40
|
+
defaultMode?: PresentationMode;
|
|
41
|
+
tools: Partial<Record<PresentationToolName, PresentationToolConfig>>;
|
|
42
|
+
};
|
|
43
|
+
export type PresentationConfigFile = {
|
|
44
|
+
presentation?: {
|
|
45
|
+
defaultMode?: unknown;
|
|
46
|
+
tools?: Partial<Record<PresentationToolName, {
|
|
47
|
+
mode?: unknown;
|
|
48
|
+
}>>;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PRESENTATION_MODES = ['compact', 'preview', 'verbose'];
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { SearchResult } from '../types.js';
|
|
2
|
+
export type ParsedDuckDuckGoResults = {
|
|
3
|
+
results: SearchResult[];
|
|
4
|
+
noResults: boolean;
|
|
5
|
+
hasResultContainers: boolean;
|
|
6
|
+
};
|
|
2
7
|
export declare function buildSearchUrl(query: string): string;
|
|
3
8
|
export declare function fetchDuckDuckGoHtml(query: string): Promise<string>;
|
|
4
|
-
export declare function parseDuckDuckGoResults(html: string):
|
|
9
|
+
export declare function parseDuckDuckGoResults(html: string): ParsedDuckDuckGoResults;
|
|
@@ -30,7 +30,8 @@ export async function fetchDuckDuckGoHtml(query) {
|
|
|
30
30
|
}
|
|
31
31
|
export function parseDuckDuckGoResults(html) {
|
|
32
32
|
const $ = cheerio.load(html);
|
|
33
|
-
|
|
33
|
+
const resultContainers = $('.result');
|
|
34
|
+
const results = resultContainers
|
|
34
35
|
.map((_, element) => {
|
|
35
36
|
const title = $(element).find('.result__a').first().text().trim();
|
|
36
37
|
const url = normalizeDuckDuckGoUrl($(element).find('.result__a').first().attr('href')?.trim() ?? '');
|
|
@@ -39,4 +40,13 @@ export function parseDuckDuckGoResults(html) {
|
|
|
39
40
|
})
|
|
40
41
|
.get()
|
|
41
42
|
.filter((value) => value !== null);
|
|
43
|
+
const text = $.text().toLowerCase();
|
|
44
|
+
const noResults = text.includes('no results found') ||
|
|
45
|
+
text.includes('no more results') ||
|
|
46
|
+
text.includes('did not match any documents');
|
|
47
|
+
return {
|
|
48
|
+
results,
|
|
49
|
+
noResults,
|
|
50
|
+
hasResultContainers: resultContainers.length > 0
|
|
51
|
+
};
|
|
42
52
|
}
|
|
@@ -22,23 +22,13 @@ export declare function createWebExploreTool({ explore }?: {
|
|
|
22
22
|
}): ({ query }: {
|
|
23
23
|
query: string;
|
|
24
24
|
}) => Promise<{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
sources: never[];
|
|
28
|
-
error: {
|
|
29
|
-
code: string;
|
|
30
|
-
message: string;
|
|
31
|
-
};
|
|
32
|
-
caveat?: undefined;
|
|
33
|
-
text?: undefined;
|
|
34
|
-
} | {
|
|
35
|
-
status: "ok";
|
|
25
|
+
presentation: import("../presentation/types.js").PresentationEnvelope;
|
|
26
|
+
status: "ok" | "error";
|
|
36
27
|
findings: string[];
|
|
37
|
-
sources: {
|
|
28
|
+
sources: Array<{
|
|
38
29
|
title: string;
|
|
39
30
|
url: string;
|
|
40
|
-
}
|
|
41
|
-
caveat
|
|
42
|
-
|
|
43
|
-
error?: undefined;
|
|
31
|
+
}>;
|
|
32
|
+
caveat?: string;
|
|
33
|
+
error?: import("../types.js").ToolError;
|
|
44
34
|
}>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createResearchWorkflow } from '../orchestration/index.js';
|
|
2
|
+
import { buildExplorePresentation } from '../presentation/explore-presentation.js';
|
|
2
3
|
function findingFromEvidence(evidence, index) {
|
|
3
4
|
if (evidence.summary.includes('Use channel')) {
|
|
4
5
|
return 'Use channel for branded Chrome or Edge when possible.';
|
|
@@ -12,23 +13,21 @@ function findingFromEvidence(evidence, index) {
|
|
|
12
13
|
}
|
|
13
14
|
return evidence.summary || `Finding ${index + 1}`;
|
|
14
15
|
}
|
|
15
|
-
function formatExploreText({ findings, sources, caveat }) {
|
|
16
|
-
const findingLines = findings.map((finding) => `- ${finding}`).join('\n');
|
|
17
|
-
const sourceLines = sources.map((source) => `- ${source.title}: ${source.url}`).join('\n');
|
|
18
|
-
const caveatBlock = caveat ? `\n\nCaveat\n${caveat}` : '';
|
|
19
|
-
return `Findings\n${findingLines}\n\nSources\n${sourceLines}${caveatBlock}`;
|
|
20
|
-
}
|
|
21
16
|
export function createWebExploreTool({ explore = createResearchWorkflow() } = {}) {
|
|
22
17
|
const runExplore = typeof explore === 'function' ? explore : explore.run.bind(explore);
|
|
23
18
|
return async function webExplore({ query }) {
|
|
24
19
|
const normalizedQuery = query.trim();
|
|
25
20
|
if (!normalizedQuery) {
|
|
26
|
-
|
|
21
|
+
const result = {
|
|
27
22
|
status: 'error',
|
|
28
23
|
findings: [],
|
|
29
24
|
sources: [],
|
|
30
25
|
error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
|
|
31
26
|
};
|
|
27
|
+
return {
|
|
28
|
+
...result,
|
|
29
|
+
presentation: buildExplorePresentation(result)
|
|
30
|
+
};
|
|
32
31
|
}
|
|
33
32
|
const result = await runExplore({ query: normalizedQuery });
|
|
34
33
|
const findings = result.evidence.slice(0, 5).map(findingFromEvidence);
|
|
@@ -39,12 +38,15 @@ export function createWebExploreTool({ explore = createResearchWorkflow() } = {}
|
|
|
39
38
|
const caveat = result.decision.action === 'answer'
|
|
40
39
|
? undefined
|
|
41
40
|
: 'Evidence is partial, so this answer is based on the strongest source found so far.';
|
|
42
|
-
|
|
41
|
+
const shaped = {
|
|
43
42
|
status: 'ok',
|
|
44
43
|
findings,
|
|
45
44
|
sources,
|
|
46
|
-
caveat
|
|
47
|
-
|
|
45
|
+
caveat
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
...shaped,
|
|
49
|
+
presentation: buildExplorePresentation(shaped)
|
|
48
50
|
};
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { headlessFetch } from '../fetch/headless-fetch.js';
|
|
2
|
+
import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
|
|
2
3
|
export function createWebFetchHeadlessTool({ fetchPage = headlessFetch } = {}) {
|
|
3
4
|
return async function webFetchHeadless({ url }) {
|
|
4
5
|
if (!/^https?:\/\//.test(url)) {
|
|
5
|
-
|
|
6
|
+
const result = {
|
|
6
7
|
status: 'unsupported',
|
|
7
8
|
url,
|
|
8
9
|
metadata: { method: 'headless', cacheHit: false },
|
|
9
10
|
error: { code: 'UNSUPPORTED_URL', message: 'Only http and https URLs are supported.' }
|
|
10
11
|
};
|
|
12
|
+
return {
|
|
13
|
+
...result,
|
|
14
|
+
presentation: buildFetchPresentation(result)
|
|
15
|
+
};
|
|
11
16
|
}
|
|
12
|
-
|
|
17
|
+
const result = await fetchPage(url);
|
|
18
|
+
return {
|
|
19
|
+
...result,
|
|
20
|
+
presentation: buildFetchPresentation(result)
|
|
21
|
+
};
|
|
13
22
|
};
|
|
14
23
|
}
|