@pi-unipi/web-api 0.1.0
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 +179 -0
- package/package.json +50 -0
- package/skills/web/SKILL.md +108 -0
- package/src/cache.ts +240 -0
- package/src/commands.ts +45 -0
- package/src/index.ts +100 -0
- package/src/providers/base.ts +108 -0
- package/src/providers/duckduckgo.ts +115 -0
- package/src/providers/firecrawl.ts +105 -0
- package/src/providers/jina-reader.ts +89 -0
- package/src/providers/jina-search.ts +88 -0
- package/src/providers/llm-summarize.ts +71 -0
- package/src/providers/perplexity.ts +191 -0
- package/src/providers/registry.ts +128 -0
- package/src/providers/serpapi.ts +86 -0
- package/src/providers/tavily.ts +95 -0
- package/src/settings.ts +263 -0
- package/src/tools.ts +329 -0
- package/src/tui/provider-selector.ts +71 -0
- package/src/tui/settings-dialog.ts +177 -0
package/src/settings.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/web-api — Settings storage
|
|
3
|
+
*
|
|
4
|
+
* Manages API keys and provider configuration.
|
|
5
|
+
* Persists to ~/.unipi/config/web-api/auth.json and config.json
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
|
|
12
|
+
/** Auth storage structure (API keys) */
|
|
13
|
+
export interface WebApiAuth {
|
|
14
|
+
[providerId: string]: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Provider configuration */
|
|
18
|
+
export interface ProviderSettings {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Cache configuration */
|
|
25
|
+
export interface CacheSettings {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
ttlMs: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Config storage structure */
|
|
31
|
+
export interface WebApiConfig {
|
|
32
|
+
providers: Record<string, ProviderSettings>;
|
|
33
|
+
cache: CacheSettings;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default configuration */
|
|
37
|
+
const DEFAULT_CONFIG: WebApiConfig = {
|
|
38
|
+
providers: {
|
|
39
|
+
duckduckgo: { enabled: true },
|
|
40
|
+
"jina-search": { enabled: true },
|
|
41
|
+
"jina-reader": { enabled: true },
|
|
42
|
+
serpapi: { enabled: false },
|
|
43
|
+
tavily: { enabled: false },
|
|
44
|
+
firecrawl: { enabled: false },
|
|
45
|
+
perplexity: { enabled: false },
|
|
46
|
+
"llm-summarize": { enabled: true },
|
|
47
|
+
},
|
|
48
|
+
cache: {
|
|
49
|
+
enabled: true,
|
|
50
|
+
ttlMs: 3600000, // 1 hour
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the config directory path.
|
|
56
|
+
*/
|
|
57
|
+
function getConfigDir(): string {
|
|
58
|
+
const homeDir = os.homedir();
|
|
59
|
+
return path.join(homeDir, ".unipi", "config", "web-api");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the auth file path.
|
|
64
|
+
*/
|
|
65
|
+
function getAuthPath(): string {
|
|
66
|
+
return path.join(getConfigDir(), "auth.json");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the config file path.
|
|
71
|
+
*/
|
|
72
|
+
function getConfigPath(): string {
|
|
73
|
+
return path.join(getConfigDir(), "config.json");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ensure config directory exists.
|
|
78
|
+
*/
|
|
79
|
+
function ensureConfigDir(): void {
|
|
80
|
+
const dir = getConfigDir();
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Load API keys from auth.json.
|
|
88
|
+
* @returns API keys object
|
|
89
|
+
*/
|
|
90
|
+
export function loadAuth(): WebApiAuth {
|
|
91
|
+
try {
|
|
92
|
+
const authPath = getAuthPath();
|
|
93
|
+
if (fs.existsSync(authPath)) {
|
|
94
|
+
const content = fs.readFileSync(authPath, "utf-8");
|
|
95
|
+
return JSON.parse(content);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("[web-api] Failed to load auth:", error);
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Save API keys to auth.json.
|
|
105
|
+
* @param auth - API keys object
|
|
106
|
+
*/
|
|
107
|
+
export function saveAuth(auth: WebApiAuth): void {
|
|
108
|
+
ensureConfigDir();
|
|
109
|
+
const authPath = getAuthPath();
|
|
110
|
+
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), "utf-8");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Load configuration from config.json.
|
|
115
|
+
* @returns Configuration object
|
|
116
|
+
*/
|
|
117
|
+
export function loadConfig(): WebApiConfig {
|
|
118
|
+
try {
|
|
119
|
+
const configPath = getConfigPath();
|
|
120
|
+
if (fs.existsSync(configPath)) {
|
|
121
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
122
|
+
const config = JSON.parse(content) as Partial<WebApiConfig>;
|
|
123
|
+
return {
|
|
124
|
+
...DEFAULT_CONFIG,
|
|
125
|
+
...config,
|
|
126
|
+
providers: {
|
|
127
|
+
...DEFAULT_CONFIG.providers,
|
|
128
|
+
...config.providers,
|
|
129
|
+
},
|
|
130
|
+
cache: {
|
|
131
|
+
...DEFAULT_CONFIG.cache,
|
|
132
|
+
...config.cache,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("[web-api] Failed to load config:", error);
|
|
138
|
+
}
|
|
139
|
+
return DEFAULT_CONFIG;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Save configuration to config.json.
|
|
144
|
+
* @param config - Configuration object
|
|
145
|
+
*/
|
|
146
|
+
export function saveConfig(config: WebApiConfig): void {
|
|
147
|
+
ensureConfigDir();
|
|
148
|
+
const configPath = getConfigPath();
|
|
149
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get API key for a provider.
|
|
154
|
+
* @param providerId - Provider ID
|
|
155
|
+
* @returns API key or undefined
|
|
156
|
+
*/
|
|
157
|
+
export function getApiKey(providerId: string): string | undefined {
|
|
158
|
+
const auth = loadAuth();
|
|
159
|
+
return auth[providerId];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Set API key for a provider.
|
|
164
|
+
* @param providerId - Provider ID
|
|
165
|
+
* @param apiKey - API key
|
|
166
|
+
*/
|
|
167
|
+
export function setApiKey(providerId: string, apiKey: string): void {
|
|
168
|
+
const auth = loadAuth();
|
|
169
|
+
auth[providerId] = apiKey;
|
|
170
|
+
saveAuth(auth);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Remove API key for a provider.
|
|
175
|
+
* @param providerId - Provider ID
|
|
176
|
+
*/
|
|
177
|
+
export function removeApiKey(providerId: string): void {
|
|
178
|
+
const auth = loadAuth();
|
|
179
|
+
delete auth[providerId];
|
|
180
|
+
saveAuth(auth);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if a provider is enabled.
|
|
185
|
+
* @param providerId - Provider ID
|
|
186
|
+
* @returns true if enabled
|
|
187
|
+
*/
|
|
188
|
+
export function isProviderEnabled(providerId: string): boolean {
|
|
189
|
+
const config = loadConfig();
|
|
190
|
+
return config.providers[providerId]?.enabled !== false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Enable or disable a provider.
|
|
195
|
+
* @param providerId - Provider ID
|
|
196
|
+
* @param enabled - Whether to enable
|
|
197
|
+
*/
|
|
198
|
+
export function setProviderEnabled(providerId: string, enabled: boolean): void {
|
|
199
|
+
const config = loadConfig();
|
|
200
|
+
if (!config.providers[providerId]) {
|
|
201
|
+
config.providers[providerId] = { enabled };
|
|
202
|
+
} else {
|
|
203
|
+
config.providers[providerId].enabled = enabled;
|
|
204
|
+
}
|
|
205
|
+
saveConfig(config);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get cache settings.
|
|
210
|
+
* @returns Cache configuration
|
|
211
|
+
*/
|
|
212
|
+
export function getCacheSettings(): CacheSettings {
|
|
213
|
+
const config = loadConfig();
|
|
214
|
+
return config.cache;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Update cache settings.
|
|
219
|
+
* @param cache - New cache settings
|
|
220
|
+
*/
|
|
221
|
+
export function updateCacheSettings(cache: Partial<CacheSettings>): void {
|
|
222
|
+
const config = loadConfig();
|
|
223
|
+
config.cache = {
|
|
224
|
+
...config.cache,
|
|
225
|
+
...cache,
|
|
226
|
+
};
|
|
227
|
+
saveConfig(config);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validate API key format (basic validation).
|
|
232
|
+
* @param providerId - Provider ID
|
|
233
|
+
* @param apiKey - API key to validate
|
|
234
|
+
* @returns true if format looks valid
|
|
235
|
+
*/
|
|
236
|
+
export function validateApiKeyFormat(providerId: string, apiKey: string): boolean {
|
|
237
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Provider-specific format checks
|
|
242
|
+
switch (providerId) {
|
|
243
|
+
case "serpapi":
|
|
244
|
+
// SerpAPI keys are typically 64 characters
|
|
245
|
+
return apiKey.length >= 32;
|
|
246
|
+
case "tavily":
|
|
247
|
+
// Tavily keys start with "tvly-"
|
|
248
|
+
return apiKey.startsWith("tvly-") && apiKey.length >= 10;
|
|
249
|
+
case "firecrawl":
|
|
250
|
+
// Firecrawl keys are typically longer
|
|
251
|
+
return apiKey.length >= 20;
|
|
252
|
+
case "perplexity":
|
|
253
|
+
// Perplexity keys are typically longer
|
|
254
|
+
return apiKey.length >= 20;
|
|
255
|
+
case "jina-search":
|
|
256
|
+
case "jina-reader":
|
|
257
|
+
// Jina keys are typically longer
|
|
258
|
+
return apiKey.length >= 10;
|
|
259
|
+
default:
|
|
260
|
+
// Generic validation
|
|
261
|
+
return apiKey.length >= 8;
|
|
262
|
+
}
|
|
263
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/web-api — Agent tools registration
|
|
3
|
+
*
|
|
4
|
+
* Registers web-search, web-read, and web-llm-summarize tools.
|
|
5
|
+
* Implements smart provider selection based on ranking.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { registry } from "./providers/registry.js";
|
|
11
|
+
import type {
|
|
12
|
+
WebProvider,
|
|
13
|
+
WebCapability,
|
|
14
|
+
SearchResult,
|
|
15
|
+
ReadResult,
|
|
16
|
+
SummarizeResult,
|
|
17
|
+
} from "./providers/base.js";
|
|
18
|
+
import {
|
|
19
|
+
getApiKey,
|
|
20
|
+
isProviderEnabled,
|
|
21
|
+
loadConfig,
|
|
22
|
+
} from "./settings.js";
|
|
23
|
+
|
|
24
|
+
/** Tool names */
|
|
25
|
+
export const WEB_TOOLS = {
|
|
26
|
+
SEARCH: "web_search",
|
|
27
|
+
READ: "web_read",
|
|
28
|
+
SUMMARIZE: "web_llm_summarize",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get available providers for a capability.
|
|
33
|
+
* Filters by enabled status and API key availability.
|
|
34
|
+
*/
|
|
35
|
+
function getAvailableProviders(capability: WebCapability): WebProvider[] {
|
|
36
|
+
const config = loadConfig();
|
|
37
|
+
const ranked = registry.getRankedProviders(capability);
|
|
38
|
+
|
|
39
|
+
return ranked.filter((provider) => {
|
|
40
|
+
// Check if provider is enabled
|
|
41
|
+
if (!isProviderEnabled(provider.id)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if provider requires API key
|
|
46
|
+
if (provider.requiresApiKey) {
|
|
47
|
+
const apiKey = getApiKey(provider.id);
|
|
48
|
+
if (!apiKey) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Select provider for a capability.
|
|
59
|
+
* If sourceRank is specified, use that rank.
|
|
60
|
+
* Otherwise, use the lowest-ranked available provider.
|
|
61
|
+
*/
|
|
62
|
+
function selectProvider(
|
|
63
|
+
capability: WebCapability,
|
|
64
|
+
sourceRank?: number
|
|
65
|
+
): WebProvider {
|
|
66
|
+
const available = getAvailableProviders(capability);
|
|
67
|
+
|
|
68
|
+
if (available.length === 0) {
|
|
69
|
+
const allProviders = registry.getProvidersForCapability(capability);
|
|
70
|
+
const providerNames = allProviders.map((p) => p.name).join(", ");
|
|
71
|
+
throw new Error(
|
|
72
|
+
`No ${capability} provider available.\n` +
|
|
73
|
+
`Configure providers via /unipi:web-settings\n` +
|
|
74
|
+
`Available providers: ${providerNames}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sourceRank !== undefined) {
|
|
79
|
+
// Find provider with matching rank
|
|
80
|
+
const provider = available.find((p) => p.ranking[capability] === sourceRank);
|
|
81
|
+
if (!provider) {
|
|
82
|
+
const availableRanks = available.map((p) => p.ranking[capability]).join(", ");
|
|
83
|
+
throw new Error(
|
|
84
|
+
`No provider at rank ${sourceRank} for ${capability}.\n` +
|
|
85
|
+
`Available ranks: ${availableRanks}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return provider;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Return lowest-ranked (cheapest/simplest) available provider
|
|
92
|
+
return available[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Execute web search.
|
|
97
|
+
*/
|
|
98
|
+
async function executeSearch(
|
|
99
|
+
query: string,
|
|
100
|
+
sourceRank?: number
|
|
101
|
+
): Promise<SearchResult[]> {
|
|
102
|
+
const provider = selectProvider("search", sourceRank);
|
|
103
|
+
|
|
104
|
+
if (!provider.search) {
|
|
105
|
+
throw new Error(`Provider "${provider.name}" does not support search`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const apiKey = provider.requiresApiKey ? getApiKey(provider.id) : undefined;
|
|
109
|
+
const config = { enabled: true, apiKey };
|
|
110
|
+
|
|
111
|
+
return provider.search(query, config);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Execute web read.
|
|
116
|
+
*/
|
|
117
|
+
async function executeRead(
|
|
118
|
+
url: string,
|
|
119
|
+
sourceRank?: number
|
|
120
|
+
): Promise<ReadResult> {
|
|
121
|
+
const provider = selectProvider("read", sourceRank);
|
|
122
|
+
|
|
123
|
+
if (!provider.read) {
|
|
124
|
+
throw new Error(`Provider "${provider.name}" does not support read`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const apiKey = provider.requiresApiKey ? getApiKey(provider.id) : undefined;
|
|
128
|
+
const config = { enabled: true, apiKey };
|
|
129
|
+
|
|
130
|
+
return provider.read(url, config);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Execute web summarize.
|
|
135
|
+
*/
|
|
136
|
+
async function executeSummarize(
|
|
137
|
+
url: string,
|
|
138
|
+
prompt?: string,
|
|
139
|
+
sourceRank?: number
|
|
140
|
+
): Promise<SummarizeResult> {
|
|
141
|
+
const provider = selectProvider("summarize", sourceRank);
|
|
142
|
+
|
|
143
|
+
if (!provider.summarize) {
|
|
144
|
+
throw new Error(`Provider "${provider.name}" does not support summarize`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const apiKey = provider.requiresApiKey ? getApiKey(provider.id) : undefined;
|
|
148
|
+
const config = { enabled: true, apiKey };
|
|
149
|
+
|
|
150
|
+
return provider.summarize(url, prompt, config);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Register web tools with pi.
|
|
155
|
+
*/
|
|
156
|
+
export function registerWebTools(pi: ExtensionAPI): void {
|
|
157
|
+
// --- web_search tool ---
|
|
158
|
+
pi.registerTool({
|
|
159
|
+
name: WEB_TOOLS.SEARCH,
|
|
160
|
+
label: "Web Search",
|
|
161
|
+
description:
|
|
162
|
+
"Search the web for information using various providers. " +
|
|
163
|
+
"Lower source = simpler/cheaper providers (DuckDuckGo, Jina Search). " +
|
|
164
|
+
"Higher source = more capable providers (SerpAPI, Tavily, Perplexity).",
|
|
165
|
+
promptSnippet: "Search the web for information.",
|
|
166
|
+
promptGuidelines: [
|
|
167
|
+
"Use web_search to find information on the web.",
|
|
168
|
+
"Omit source for auto-selection (cheapest available).",
|
|
169
|
+
"Specify source number for specific provider (1=DuckDuckGo, 2=Jina, 3=SerpAPI, 4=Tavily, 5=Perplexity).",
|
|
170
|
+
"Quick facts: source 1-2. Research: source 3-5.",
|
|
171
|
+
],
|
|
172
|
+
parameters: Type.Object({
|
|
173
|
+
query: Type.String({ description: "Search query string" }),
|
|
174
|
+
source: Type.Optional(
|
|
175
|
+
Type.Number({
|
|
176
|
+
description:
|
|
177
|
+
"Provider selection (1=DuckDuckGo, 2=Jina Search, 3=SerpAPI, 4=Tavily, 5=Perplexity). " +
|
|
178
|
+
"Omit for auto-selection.",
|
|
179
|
+
minimum: 1,
|
|
180
|
+
maximum: 5,
|
|
181
|
+
})
|
|
182
|
+
),
|
|
183
|
+
}),
|
|
184
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
185
|
+
try {
|
|
186
|
+
const results = await executeSearch(params.query, params.source);
|
|
187
|
+
|
|
188
|
+
if (results.length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: "No results found." }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const formatted = results
|
|
195
|
+
.map(
|
|
196
|
+
(r, i) =>
|
|
197
|
+
`${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`
|
|
198
|
+
)
|
|
199
|
+
.join("\n\n");
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: `Found ${results.length} results:\n\n${formatted}`,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const message =
|
|
211
|
+
error instanceof Error ? error.message : String(error);
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: `Search failed: ${message}` }],
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// --- web_read tool ---
|
|
221
|
+
pi.registerTool({
|
|
222
|
+
name: WEB_TOOLS.READ,
|
|
223
|
+
label: "Web Read",
|
|
224
|
+
description:
|
|
225
|
+
"Read and extract content from a URL. " +
|
|
226
|
+
"Extracts main content, strips navigation/ads. Returns markdown.",
|
|
227
|
+
promptSnippet: "Read content from a URL.",
|
|
228
|
+
promptGuidelines: [
|
|
229
|
+
"Use web_read to extract content from a web page.",
|
|
230
|
+
"Returns main content as markdown.",
|
|
231
|
+
"Lower source = simpler providers (Jina Reader).",
|
|
232
|
+
"Higher source = more capable providers (Firecrawl, Perplexity).",
|
|
233
|
+
],
|
|
234
|
+
parameters: Type.Object({
|
|
235
|
+
url: Type.String({ description: "URL to read" }),
|
|
236
|
+
source: Type.Optional(
|
|
237
|
+
Type.Number({
|
|
238
|
+
description:
|
|
239
|
+
"Provider selection (1=Jina Reader, 2=Firecrawl, 3=Perplexity). " +
|
|
240
|
+
"Omit for auto-selection.",
|
|
241
|
+
minimum: 1,
|
|
242
|
+
maximum: 3,
|
|
243
|
+
})
|
|
244
|
+
),
|
|
245
|
+
}),
|
|
246
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
247
|
+
try {
|
|
248
|
+
const result = await executeRead(params.url, params.source);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: "text",
|
|
254
|
+
text: `Content from ${result.url}:\n\n${result.content}`,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const message =
|
|
260
|
+
error instanceof Error ? error.message : String(error);
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: "text", text: `Read failed: ${message}` }],
|
|
263
|
+
isError: true,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// --- web_llm_summarize tool ---
|
|
270
|
+
pi.registerTool({
|
|
271
|
+
name: WEB_TOOLS.SUMMARIZE,
|
|
272
|
+
label: "Web LLM Summarize",
|
|
273
|
+
description:
|
|
274
|
+
"Summarize web content using LLM. " +
|
|
275
|
+
"Fetches content from URL, then uses LLM to summarize. " +
|
|
276
|
+
"Higher cost (LLM tokens + provider cost).",
|
|
277
|
+
promptSnippet: "Summarize web content using LLM.",
|
|
278
|
+
promptGuidelines: [
|
|
279
|
+
"Use web_llm_summarize to get a summary of web content.",
|
|
280
|
+
"Specify custom prompt for targeted summaries.",
|
|
281
|
+
"Omit source for auto-selection of content provider.",
|
|
282
|
+
"Higher cost due to LLM token usage.",
|
|
283
|
+
],
|
|
284
|
+
parameters: Type.Object({
|
|
285
|
+
url: Type.String({ description: "URL to summarize" }),
|
|
286
|
+
prompt: Type.Optional(
|
|
287
|
+
Type.String({
|
|
288
|
+
description:
|
|
289
|
+
"Custom summarization prompt. " +
|
|
290
|
+
"Omit for default comprehensive summary.",
|
|
291
|
+
})
|
|
292
|
+
),
|
|
293
|
+
source: Type.Optional(
|
|
294
|
+
Type.Number({
|
|
295
|
+
description:
|
|
296
|
+
"Provider selection for content fetch (1=Jina Reader, 2=Firecrawl, 3=Perplexity). " +
|
|
297
|
+
"Omit for auto-selection.",
|
|
298
|
+
minimum: 1,
|
|
299
|
+
maximum: 3,
|
|
300
|
+
})
|
|
301
|
+
),
|
|
302
|
+
}),
|
|
303
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
304
|
+
try {
|
|
305
|
+
const result = await executeSummarize(
|
|
306
|
+
params.url,
|
|
307
|
+
params.prompt,
|
|
308
|
+
params.source
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: "text",
|
|
315
|
+
text: `Summary of ${result.url}:\n\n${result.summary}`,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const message =
|
|
321
|
+
error instanceof Error ? error.message : String(error);
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text: `Summarize failed: ${message}` }],
|
|
324
|
+
isError: true,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/web-api — Provider selector TUI component
|
|
3
|
+
*
|
|
4
|
+
* Displays provider list with status indicators for API key management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WebProvider } from "../providers/base.js";
|
|
8
|
+
import { registry } from "../providers/registry.js";
|
|
9
|
+
import { getApiKey, isProviderEnabled } from "../settings.js";
|
|
10
|
+
|
|
11
|
+
/** Provider status */
|
|
12
|
+
export interface ProviderStatus {
|
|
13
|
+
provider: WebProvider;
|
|
14
|
+
configured: boolean;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
hasApiKey: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get status of all providers.
|
|
21
|
+
*/
|
|
22
|
+
export function getProviderStatuses(): ProviderStatus[] {
|
|
23
|
+
const providers = registry.getAllProviders();
|
|
24
|
+
|
|
25
|
+
return providers.map((provider) => {
|
|
26
|
+
const hasApiKey = provider.requiresApiKey
|
|
27
|
+
? !!getApiKey(provider.id)
|
|
28
|
+
: true;
|
|
29
|
+
const enabled = isProviderEnabled(provider.id);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
provider,
|
|
33
|
+
configured: hasApiKey && enabled,
|
|
34
|
+
enabled,
|
|
35
|
+
hasApiKey,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format provider status for display.
|
|
42
|
+
*/
|
|
43
|
+
export function formatProviderStatus(status: ProviderStatus): string {
|
|
44
|
+
const icon = status.configured ? "✓" : "✗";
|
|
45
|
+
const name = status.provider.name.padEnd(20);
|
|
46
|
+
const capabilities = status.provider.capabilities.join(", ");
|
|
47
|
+
const apiKeyStatus = status.provider.requiresApiKey
|
|
48
|
+
? status.hasApiKey
|
|
49
|
+
? "API key configured"
|
|
50
|
+
: "API key required"
|
|
51
|
+
: "No API key needed";
|
|
52
|
+
|
|
53
|
+
return `${icon} ${name} ${capabilities.padEnd(30)} ${apiKeyStatus}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get provider selection options for TUI.
|
|
58
|
+
*/
|
|
59
|
+
export function getProviderOptions(): Array<{
|
|
60
|
+
label: string;
|
|
61
|
+
value: string;
|
|
62
|
+
description: string;
|
|
63
|
+
}> {
|
|
64
|
+
const statuses = getProviderStatuses();
|
|
65
|
+
|
|
66
|
+
return statuses.map((status) => ({
|
|
67
|
+
label: formatProviderStatus(status),
|
|
68
|
+
value: status.provider.id,
|
|
69
|
+
description: `${status.provider.name} - ${status.provider.capabilities.join(", ")}`,
|
|
70
|
+
}));
|
|
71
|
+
}
|