@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.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @unipi/web-api — Perplexity provider
3
+ *
4
+ * Paid search and summarization provider using Perplexity API.
5
+ * Supports search, read, and summarize capabilities.
6
+ * Requires API key (PERPLEXITY_API_KEY environment variable).
7
+ */
8
+
9
+ import type {
10
+ WebProvider,
11
+ SearchResult,
12
+ ReadResult,
13
+ SummarizeResult,
14
+ ProviderConfig,
15
+ } from "./base.js";
16
+ import { registry } from "./registry.js";
17
+
18
+ /** Perplexity API response format */
19
+ interface PerplexityResponse {
20
+ choices: Array<{
21
+ message: {
22
+ content: string;
23
+ };
24
+ citations?: Array<{
25
+ url: string;
26
+ }>;
27
+ }>;
28
+ }
29
+
30
+ /**
31
+ * Search via Perplexity.
32
+ * @param query - Search query
33
+ * @param apiKey - Perplexity API key
34
+ * @returns Array of search results
35
+ */
36
+ async function searchPerplexity(query: string, apiKey: string): Promise<SearchResult[]> {
37
+ const response = await fetch("https://api.perplexity.ai/chat/completions", {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ Authorization: `Bearer ${apiKey}`,
42
+ },
43
+ body: JSON.stringify({
44
+ model: "llama-3.1-sonar-small-128k-online",
45
+ messages: [
46
+ {
47
+ role: "system",
48
+ content: "You are a search assistant. Return search results as a JSON array with objects containing 'title', 'url', and 'snippet' fields.",
49
+ },
50
+ {
51
+ role: "user",
52
+ content: query,
53
+ },
54
+ ],
55
+ }),
56
+ });
57
+
58
+ if (!response.ok) {
59
+ throw new Error(`Perplexity search failed: ${response.status} ${response.statusText}`);
60
+ }
61
+
62
+ const data = (await response.json()) as PerplexityResponse;
63
+ const content = data.choices[0]?.message?.content || "[]";
64
+
65
+ try {
66
+ // Parse JSON from response
67
+ const jsonMatch = content.match(/\[[\s\S]*\]/);
68
+ if (jsonMatch) {
69
+ return JSON.parse(jsonMatch[0]);
70
+ }
71
+ } catch {
72
+ // Fall back to extracting from text
73
+ }
74
+
75
+ // Fallback: create single result from response
76
+ const citations = data.choices[0]?.citations || [];
77
+ return citations.map((citation, i) => ({
78
+ title: `Result ${i + 1}`,
79
+ url: citation.url,
80
+ snippet: content.substring(0, 200),
81
+ }));
82
+ }
83
+
84
+ /**
85
+ * Summarize via Perplexity.
86
+ * @param url - URL to summarize
87
+ * @param prompt - Custom prompt
88
+ * @param apiKey - Perplexity API key
89
+ * @returns Summarized content
90
+ */
91
+ async function summarizePerplexity(
92
+ url: string,
93
+ prompt: string | undefined,
94
+ apiKey: string
95
+ ): Promise<SummarizeResult> {
96
+ const systemPrompt = prompt || "Summarize the content of this URL concisely, highlighting key points.";
97
+
98
+ const response = await fetch("https://api.perplexity.ai/chat/completions", {
99
+ method: "POST",
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ Authorization: `Bearer ${apiKey}`,
103
+ },
104
+ body: JSON.stringify({
105
+ model: "llama-3.1-sonar-small-128k-online",
106
+ messages: [
107
+ {
108
+ role: "system",
109
+ content: systemPrompt,
110
+ },
111
+ {
112
+ role: "user",
113
+ content: `Summarize this URL: ${url}`,
114
+ },
115
+ ],
116
+ }),
117
+ });
118
+
119
+ if (!response.ok) {
120
+ throw new Error(`Perplexity summarize failed: ${response.status} ${response.statusText}`);
121
+ }
122
+
123
+ const data = (await response.json()) as PerplexityResponse;
124
+ const summary = data.choices[0]?.message?.content || "";
125
+
126
+ return {
127
+ url: url,
128
+ summary: summary,
129
+ prompt: prompt,
130
+ };
131
+ }
132
+
133
+ /** Perplexity provider implementation */
134
+ const perplexityProvider: WebProvider = {
135
+ id: "perplexity",
136
+ name: "Perplexity",
137
+ capabilities: ["search", "read", "summarize"],
138
+ requiresApiKey: true,
139
+ apiKeyEnv: "PERPLEXITY_API_KEY",
140
+ ranking: {
141
+ search: 5,
142
+ read: 3,
143
+ summarize: 1,
144
+ },
145
+ config: {},
146
+
147
+ async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
148
+ const apiKey = config?.apiKey || process.env.PERPLEXITY_API_KEY;
149
+ if (!apiKey) {
150
+ throw new Error("Perplexity requires an API key. Set PERPLEXITY_API_KEY environment variable or configure via /unipi:web-settings");
151
+ }
152
+ return searchPerplexity(query, apiKey);
153
+ },
154
+
155
+ async read(url: string, config?: ProviderConfig): Promise<ReadResult> {
156
+ const apiKey = config?.apiKey || process.env.PERPLEXITY_API_KEY;
157
+ if (!apiKey) {
158
+ throw new Error("Perplexity requires an API key. Set PERPLEXITY_API_KEY environment variable or configure via /unipi:web-settings");
159
+ }
160
+
161
+ const result = await summarizePerplexity(url, "Extract and return the full content of this URL as markdown.", apiKey);
162
+
163
+ return {
164
+ url: url,
165
+ content: result.summary,
166
+ contentType: "markdown",
167
+ };
168
+ },
169
+
170
+ async summarize(url: string, prompt?: string, config?: ProviderConfig): Promise<SummarizeResult> {
171
+ const apiKey = config?.apiKey || process.env.PERPLEXITY_API_KEY;
172
+ if (!apiKey) {
173
+ throw new Error("Perplexity requires an API key. Set PERPLEXITY_API_KEY environment variable or configure via /unipi:web-settings");
174
+ }
175
+ return summarizePerplexity(url, prompt, apiKey);
176
+ },
177
+
178
+ async validateApiKey(apiKey: string): Promise<boolean> {
179
+ try {
180
+ await searchPerplexity("test", apiKey);
181
+ return true;
182
+ } catch {
183
+ return false;
184
+ }
185
+ },
186
+ };
187
+
188
+ // Register provider
189
+ registry.register(perplexityProvider);
190
+
191
+ export { perplexityProvider };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @unipi/web-api — Provider registry
3
+ *
4
+ * Registry for managing web providers.
5
+ * Handles registration, retrieval, and ranked selection.
6
+ */
7
+
8
+ import type {
9
+ WebProvider,
10
+ WebCapability,
11
+ ProviderConfig,
12
+ } from "./base.js";
13
+
14
+ /**
15
+ * ProviderRegistry manages all registered web providers.
16
+ *
17
+ * Provides methods to:
18
+ * - Register providers
19
+ * - Retrieve providers by ID
20
+ * - Get providers for a specific capability
21
+ * - Get ranked providers for smart selection
22
+ */
23
+ export class ProviderRegistry {
24
+ private providers: Map<string, WebProvider> = new Map();
25
+
26
+ /**
27
+ * Register a provider.
28
+ * @param provider - Provider to register
29
+ */
30
+ register(provider: WebProvider): void {
31
+ if (this.providers.has(provider.id)) {
32
+ throw new Error(`Provider "${provider.id}" is already registered`);
33
+ }
34
+ this.providers.set(provider.id, provider);
35
+ }
36
+
37
+ /**
38
+ * Unregister a provider.
39
+ * @param providerId - Provider ID to unregister
40
+ */
41
+ unregister(providerId: string): void {
42
+ this.providers.delete(providerId);
43
+ }
44
+
45
+ /**
46
+ * Get a provider by ID.
47
+ * @param providerId - Provider ID
48
+ * @returns Provider or undefined
49
+ */
50
+ getProvider(providerId: string): WebProvider | undefined {
51
+ return this.providers.get(providerId);
52
+ }
53
+
54
+ /**
55
+ * Get all registered providers.
56
+ * @returns Array of all providers
57
+ */
58
+ getAllProviders(): WebProvider[] {
59
+ return Array.from(this.providers.values());
60
+ }
61
+
62
+ /**
63
+ * Get providers that support a specific capability.
64
+ * @param capability - Capability to filter by
65
+ * @returns Array of providers with the capability
66
+ */
67
+ getProvidersForCapability(capability: WebCapability): WebProvider[] {
68
+ return this.getAllProviders().filter((p) =>
69
+ p.capabilities.includes(capability)
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Get ranked providers for a specific capability.
75
+ * Sorted by ranking (lower = better/simpler/cheaper).
76
+ * @param capability - Capability to rank by
77
+ * @returns Array of providers sorted by ranking
78
+ */
79
+ getRankedProviders(capability: WebCapability): WebProvider[] {
80
+ return this.getProvidersForCapability(capability)
81
+ .filter((p) => p.ranking[capability] > 0)
82
+ .sort((a, b) => a.ranking[capability] - b.ranking[capability]);
83
+ }
84
+
85
+ /**
86
+ * Get the best provider for a capability (lowest rank).
87
+ * @param capability - Capability to find best provider for
88
+ * @returns Best provider or undefined
89
+ */
90
+ getBestProvider(capability: WebCapability): WebProvider | undefined {
91
+ const ranked = this.getRankedProviders(capability);
92
+ return ranked[0];
93
+ }
94
+
95
+ /**
96
+ * Get a provider by rank for a capability.
97
+ * @param capability - Capability to search
98
+ * @param rank - Desired rank (1-based)
99
+ * @returns Provider at that rank or undefined
100
+ */
101
+ getProviderByRank(capability: WebCapability, rank: number): WebProvider | undefined {
102
+ const ranked = this.getRankedProviders(capability);
103
+ return ranked.find((p) => p.ranking[capability] === rank);
104
+ }
105
+
106
+ /**
107
+ * Get enabled providers based on configuration.
108
+ * @param configMap - Map of provider ID to config
109
+ * @returns Array of enabled providers
110
+ */
111
+ getEnabledProviders(configMap: Map<string, ProviderConfig>): WebProvider[] {
112
+ return this.getAllProviders().filter((p) => {
113
+ const config = configMap.get(p.id);
114
+ return config?.enabled !== false;
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Get provider count.
120
+ * @returns Number of registered providers
121
+ */
122
+ get count(): number {
123
+ return this.providers.size;
124
+ }
125
+ }
126
+
127
+ /** Singleton registry instance */
128
+ export const registry = new ProviderRegistry();
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @unipi/web-api — SerpAPI provider
3
+ *
4
+ * Paid search provider using SerpAPI for Google search results.
5
+ * Requires API key (SERPAPI_KEY environment variable).
6
+ */
7
+
8
+ import type {
9
+ WebProvider,
10
+ SearchResult,
11
+ ProviderConfig,
12
+ } from "./base.js";
13
+ import { registry } from "./registry.js";
14
+
15
+ /** SerpAPI response format */
16
+ interface SerpAPIResponse {
17
+ organic_results: Array<{
18
+ title: string;
19
+ link: string;
20
+ snippet: string;
21
+ }>;
22
+ }
23
+
24
+ /**
25
+ * Search via SerpAPI.
26
+ * @param query - Search query
27
+ * @param apiKey - SerpAPI key
28
+ * @returns Array of search results
29
+ */
30
+ async function searchSerpAPI(query: string, apiKey: string): Promise<SearchResult[]> {
31
+ const url = new URL("https://serpapi.com/search");
32
+ url.searchParams.set("q", query);
33
+ url.searchParams.set("api_key", apiKey);
34
+ url.searchParams.set("engine", "google");
35
+
36
+ const response = await fetch(url.toString());
37
+
38
+ if (!response.ok) {
39
+ throw new Error(`SerpAPI search failed: ${response.status} ${response.statusText}`);
40
+ }
41
+
42
+ const data = (await response.json()) as SerpAPIResponse;
43
+
44
+ return (data.organic_results || []).map((item) => ({
45
+ title: item.title,
46
+ url: item.link,
47
+ snippet: item.snippet,
48
+ }));
49
+ }
50
+
51
+ /** SerpAPI provider implementation */
52
+ const serpapiProvider: WebProvider = {
53
+ id: "serpapi",
54
+ name: "SerpAPI",
55
+ capabilities: ["search"],
56
+ requiresApiKey: true,
57
+ apiKeyEnv: "SERPAPI_KEY",
58
+ ranking: {
59
+ search: 3,
60
+ read: 0,
61
+ summarize: 0,
62
+ },
63
+ config: {},
64
+
65
+ async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
66
+ const apiKey = config?.apiKey || process.env.SERPAPI_KEY;
67
+ if (!apiKey) {
68
+ throw new Error("SerpAPI requires an API key. Set SERPAPI_KEY environment variable or configure via /unipi:web-settings");
69
+ }
70
+ return searchSerpAPI(query, apiKey);
71
+ },
72
+
73
+ async validateApiKey(apiKey: string): Promise<boolean> {
74
+ try {
75
+ const results = await searchSerpAPI("test", apiKey);
76
+ return Array.isArray(results);
77
+ } catch {
78
+ return false;
79
+ }
80
+ },
81
+ };
82
+
83
+ // Register provider
84
+ registry.register(serpapiProvider);
85
+
86
+ export { serpapiProvider };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @unipi/web-api — Tavily provider
3
+ *
4
+ * Paid search provider using Tavily API for AI-optimized search results.
5
+ * Requires API key (TAVILY_API_KEY environment variable).
6
+ */
7
+
8
+ import type {
9
+ WebProvider,
10
+ SearchResult,
11
+ ProviderConfig,
12
+ } from "./base.js";
13
+ import { registry } from "./registry.js";
14
+
15
+ /** Tavily API response format */
16
+ interface TavilyResponse {
17
+ results: Array<{
18
+ title: string;
19
+ url: string;
20
+ content: string;
21
+ }>;
22
+ }
23
+
24
+ /**
25
+ * Search via Tavily.
26
+ * @param query - Search query
27
+ * @param apiKey - Tavily API key
28
+ * @returns Array of search results
29
+ */
30
+ async function searchTavily(query: string, apiKey: string): Promise<SearchResult[]> {
31
+ const url = "https://api.tavily.com/search";
32
+
33
+ const response = await fetch(url, {
34
+ method: "POST",
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ },
38
+ body: JSON.stringify({
39
+ api_key: apiKey,
40
+ query: query,
41
+ search_depth: "basic",
42
+ include_answer: false,
43
+ include_raw_content: false,
44
+ }),
45
+ });
46
+
47
+ if (!response.ok) {
48
+ throw new Error(`Tavily search failed: ${response.status} ${response.statusText}`);
49
+ }
50
+
51
+ const data = (await response.json()) as TavilyResponse;
52
+
53
+ return (data.results || []).map((item) => ({
54
+ title: item.title,
55
+ url: item.url,
56
+ snippet: item.content,
57
+ }));
58
+ }
59
+
60
+ /** Tavily provider implementation */
61
+ const tavilyProvider: WebProvider = {
62
+ id: "tavily",
63
+ name: "Tavily",
64
+ capabilities: ["search"],
65
+ requiresApiKey: true,
66
+ apiKeyEnv: "TAVILY_API_KEY",
67
+ ranking: {
68
+ search: 4,
69
+ read: 0,
70
+ summarize: 0,
71
+ },
72
+ config: {},
73
+
74
+ async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
75
+ const apiKey = config?.apiKey || process.env.TAVILY_API_KEY;
76
+ if (!apiKey) {
77
+ throw new Error("Tavily requires an API key. Set TAVILY_API_KEY environment variable or configure via /unipi:web-settings");
78
+ }
79
+ return searchTavily(query, apiKey);
80
+ },
81
+
82
+ async validateApiKey(apiKey: string): Promise<boolean> {
83
+ try {
84
+ const results = await searchTavily("test", apiKey);
85
+ return Array.isArray(results);
86
+ } catch {
87
+ return false;
88
+ }
89
+ },
90
+ };
91
+
92
+ // Register provider
93
+ registry.register(tavilyProvider);
94
+
95
+ export { tavilyProvider };