ubersearch 0.0.0-development
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/LICENSE +21 -0
- package/README.md +374 -0
- package/package.json +76 -0
- package/src/app/index.ts +30 -0
- package/src/bootstrap/container.ts +157 -0
- package/src/cli.ts +380 -0
- package/src/config/defineConfig.ts +176 -0
- package/src/config/load.ts +368 -0
- package/src/config/types.ts +86 -0
- package/src/config/validation.ts +148 -0
- package/src/core/cache.ts +74 -0
- package/src/core/container.ts +268 -0
- package/src/core/credits/CreditManager.ts +158 -0
- package/src/core/credits/CreditStateProvider.ts +151 -0
- package/src/core/credits/FileCreditStateProvider.ts +137 -0
- package/src/core/credits/index.ts +3 -0
- package/src/core/docker/dockerComposeHelper.ts +177 -0
- package/src/core/docker/dockerLifecycleManager.ts +361 -0
- package/src/core/docker/index.ts +8 -0
- package/src/core/logger.ts +146 -0
- package/src/core/orchestrator.ts +103 -0
- package/src/core/paths.ts +157 -0
- package/src/core/provider/ILifecycleProvider.ts +120 -0
- package/src/core/provider/ProviderFactory.ts +120 -0
- package/src/core/provider.ts +61 -0
- package/src/core/serviceKeys.ts +45 -0
- package/src/core/strategy/AllProvidersStrategy.ts +245 -0
- package/src/core/strategy/FirstSuccessStrategy.ts +98 -0
- package/src/core/strategy/ISearchStrategy.ts +94 -0
- package/src/core/strategy/StrategyFactory.ts +204 -0
- package/src/core/strategy/index.ts +9 -0
- package/src/core/strategy/types.ts +56 -0
- package/src/core/types.ts +58 -0
- package/src/index.ts +1 -0
- package/src/plugin/PluginRegistry.ts +336 -0
- package/src/plugin/builtin.ts +130 -0
- package/src/plugin/index.ts +33 -0
- package/src/plugin/types.ts +212 -0
- package/src/providers/BaseProvider.ts +49 -0
- package/src/providers/brave.ts +66 -0
- package/src/providers/constants.ts +13 -0
- package/src/providers/helpers/index.ts +24 -0
- package/src/providers/helpers/lifecycleHelpers.ts +110 -0
- package/src/providers/helpers/resultMappers.ts +168 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/linkup.ts +114 -0
- package/src/providers/retry.ts +95 -0
- package/src/providers/searchxng.ts +163 -0
- package/src/providers/tavily.ts +73 -0
- package/src/providers/types/index.ts +185 -0
- package/src/providers/utils.ts +182 -0
- package/src/tool/allSearchTool.ts +110 -0
- package/src/tool/interface.ts +71 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linkup Search Provider Implementation
|
|
3
|
+
* https://www.linkup.ai/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LinkupConfig } from "../config/types";
|
|
7
|
+
import { DockerLifecycleManager } from "../core/docker/dockerLifecycleManager";
|
|
8
|
+
import type { ILifecycleProvider } from "../core/provider";
|
|
9
|
+
import type { SearchQuery, SearchResponse } from "../core/types";
|
|
10
|
+
import { BaseProvider } from "./BaseProvider";
|
|
11
|
+
import { PROVIDER_DEFAULTS } from "./constants";
|
|
12
|
+
import type { LinkupApiResponse, LinkupSearchResult } from "./types";
|
|
13
|
+
import { fetchWithErrorHandling } from "./utils";
|
|
14
|
+
|
|
15
|
+
export class LinkupProvider extends BaseProvider<LinkupConfig> implements ILifecycleProvider {
|
|
16
|
+
private lifecycleManager: DockerLifecycleManager;
|
|
17
|
+
|
|
18
|
+
constructor(config: LinkupConfig) {
|
|
19
|
+
super(config);
|
|
20
|
+
|
|
21
|
+
const autoStart = config.autoStart ?? false;
|
|
22
|
+
const autoStop = config.autoStop ?? false;
|
|
23
|
+
const initTimeoutMs = config.initTimeoutMs ?? 30000;
|
|
24
|
+
|
|
25
|
+
this.lifecycleManager = new DockerLifecycleManager({
|
|
26
|
+
containerName: config.containerName,
|
|
27
|
+
composeFile: config.composeFile,
|
|
28
|
+
healthEndpoint: config.healthEndpoint,
|
|
29
|
+
autoStart,
|
|
30
|
+
autoStop,
|
|
31
|
+
initTimeoutMs,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected getDocsUrl(): string {
|
|
36
|
+
return "https://docs.linkup.ai/";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected getApiKeyEnv(): string {
|
|
40
|
+
return this.config.apiKeyEnv;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async search(query: SearchQuery): Promise<SearchResponse> {
|
|
44
|
+
const apiKey = this.getApiKey();
|
|
45
|
+
|
|
46
|
+
const requestBody = {
|
|
47
|
+
q: query.query,
|
|
48
|
+
depth: "standard",
|
|
49
|
+
outputType: "searchResults",
|
|
50
|
+
maxResults: query.limit ?? 5,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const { data: json, tookMs } = await fetchWithErrorHandling<LinkupApiResponse>(
|
|
54
|
+
this.id,
|
|
55
|
+
this.config.endpoint,
|
|
56
|
+
{
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${apiKey}`,
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(requestBody),
|
|
63
|
+
timeoutMs: PROVIDER_DEFAULTS.DEFAULT_TIMEOUT_MS,
|
|
64
|
+
},
|
|
65
|
+
"Linkup",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const results: LinkupSearchResult[] = json.results ?? [];
|
|
69
|
+
|
|
70
|
+
this.validateResults(results, "Linkup");
|
|
71
|
+
|
|
72
|
+
// Map to normalized format
|
|
73
|
+
// Linkup results have url, name/title, content/snippet
|
|
74
|
+
const items = results.map((r: LinkupSearchResult) => ({
|
|
75
|
+
title: r.name ?? r.title ?? r.url,
|
|
76
|
+
url: r.url,
|
|
77
|
+
snippet: r.content ?? r.snippet ?? r.description ?? "",
|
|
78
|
+
score: r.score ?? r.relevance,
|
|
79
|
+
sourceEngine: this.id,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
engineId: this.id,
|
|
84
|
+
items,
|
|
85
|
+
raw: query.includeRaw ? json : undefined,
|
|
86
|
+
tookMs,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ILifecycleProvider implementation
|
|
91
|
+
async init(): Promise<void> {
|
|
92
|
+
await this.lifecycleManager.init();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async healthcheck(): Promise<boolean> {
|
|
96
|
+
return await this.lifecycleManager.healthcheck();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async shutdown(): Promise<void> {
|
|
100
|
+
await this.lifecycleManager.shutdown();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async validateConfig(): Promise<{
|
|
104
|
+
valid: boolean;
|
|
105
|
+
errors: string[];
|
|
106
|
+
warnings: string[];
|
|
107
|
+
}> {
|
|
108
|
+
return await this.lifecycleManager.validateDockerConfig();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
isLifecycleManaged(): boolean {
|
|
112
|
+
return true; // This provider manages lifecycle
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Logic for Provider Operations
|
|
3
|
+
*
|
|
4
|
+
* Provides exponential backoff retry functionality for resilient
|
|
5
|
+
* provider operations
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EngineId } from "../core/types";
|
|
9
|
+
import { SearchError } from "../core/types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Retry configuration
|
|
13
|
+
*/
|
|
14
|
+
export interface RetryConfig {
|
|
15
|
+
/** Maximum number of retry attempts */
|
|
16
|
+
maxAttempts?: number;
|
|
17
|
+
|
|
18
|
+
/** Initial delay between retries in milliseconds */
|
|
19
|
+
initialDelayMs?: number;
|
|
20
|
+
|
|
21
|
+
/** Multiplier for exponential backoff */
|
|
22
|
+
backoffMultiplier?: number;
|
|
23
|
+
|
|
24
|
+
/** Maximum delay between retries in milliseconds */
|
|
25
|
+
maxDelayMs?: number;
|
|
26
|
+
|
|
27
|
+
/** Whether to retry on specific error types */
|
|
28
|
+
retryableErrors?: Array<"network_error" | "api_error" | "timeout">;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default retry configuration
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
|
|
35
|
+
maxAttempts: 3,
|
|
36
|
+
initialDelayMs: 1000,
|
|
37
|
+
backoffMultiplier: 2,
|
|
38
|
+
maxDelayMs: 10000,
|
|
39
|
+
retryableErrors: ["network_error", "api_error"],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute a function with retry logic and exponential backoff
|
|
44
|
+
*
|
|
45
|
+
* @param engineId - Engine ID for error messages
|
|
46
|
+
* @param fn - Function to execute with retry logic
|
|
47
|
+
* @param config - Retry configuration
|
|
48
|
+
* @returns Result of the function
|
|
49
|
+
* @throws SearchError if all retry attempts fail
|
|
50
|
+
*/
|
|
51
|
+
export async function withRetry<T>(
|
|
52
|
+
engineId: EngineId,
|
|
53
|
+
fn: () => Promise<T>,
|
|
54
|
+
config: RetryConfig = {},
|
|
55
|
+
): Promise<T> {
|
|
56
|
+
const {
|
|
57
|
+
maxAttempts = DEFAULT_RETRY_CONFIG.maxAttempts,
|
|
58
|
+
initialDelayMs = DEFAULT_RETRY_CONFIG.initialDelayMs,
|
|
59
|
+
backoffMultiplier = DEFAULT_RETRY_CONFIG.backoffMultiplier,
|
|
60
|
+
maxDelayMs = DEFAULT_RETRY_CONFIG.maxDelayMs,
|
|
61
|
+
retryableErrors = DEFAULT_RETRY_CONFIG.retryableErrors,
|
|
62
|
+
} = config;
|
|
63
|
+
|
|
64
|
+
let lastError: unknown;
|
|
65
|
+
let delay = initialDelayMs;
|
|
66
|
+
|
|
67
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
return await fn();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
lastError = error;
|
|
72
|
+
|
|
73
|
+
if (!(error instanceof SearchError)) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const shouldRetry = retryableErrors.includes(error.reason as "network_error" | "api_error");
|
|
78
|
+
|
|
79
|
+
if (!shouldRetry || attempt >= maxAttempts) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const nextDelay = Math.min(delay * backoffMultiplier, maxDelayMs);
|
|
84
|
+
|
|
85
|
+
console.warn(
|
|
86
|
+
`[${engineId}] Attempt ${attempt}/${maxAttempts} failed: ${error.message}. Retrying in ${nextDelay}ms...`,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, nextDelay));
|
|
90
|
+
delay = nextDelay;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw lastError;
|
|
95
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearXNG Search Provider Implementation
|
|
3
|
+
*
|
|
4
|
+
* Integrates SearXNG with auto-start Docker container management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import type { SearchxngConfig as BaseSearchxngConfig } from "../config/types";
|
|
9
|
+
import { DockerLifecycleManager } from "../core/docker/dockerLifecycleManager";
|
|
10
|
+
import type { ILifecycleProvider, ProviderMetadata } from "../core/provider";
|
|
11
|
+
import type { SearchQuery, SearchResponse, SearchResultItem } from "../core/types";
|
|
12
|
+
import { SearchError } from "../core/types";
|
|
13
|
+
import { BaseProvider } from "./BaseProvider";
|
|
14
|
+
import { PROVIDER_DEFAULTS } from "./constants";
|
|
15
|
+
import type { SearxngApiResponse, SearxngSearchResult } from "./types";
|
|
16
|
+
import { buildUrl, fetchWithErrorHandling } from "./utils";
|
|
17
|
+
|
|
18
|
+
export class SearchxngProvider
|
|
19
|
+
extends BaseProvider<BaseSearchxngConfig>
|
|
20
|
+
implements ILifecycleProvider
|
|
21
|
+
{
|
|
22
|
+
private lifecycleManager: DockerLifecycleManager;
|
|
23
|
+
private defaultLimit: number;
|
|
24
|
+
|
|
25
|
+
constructor(config: BaseSearchxngConfig) {
|
|
26
|
+
super(config);
|
|
27
|
+
this.defaultLimit = config.defaultLimit;
|
|
28
|
+
|
|
29
|
+
const autoStart = config.autoStart ?? true;
|
|
30
|
+
const autoStop = config.autoStop ?? true;
|
|
31
|
+
const initTimeoutMs = config.initTimeoutMs ?? PROVIDER_DEFAULTS.SEARXNG_INIT_TIMEOUT_MS;
|
|
32
|
+
|
|
33
|
+
const projectRoot = config.composeFile ? dirname(config.composeFile) : process.cwd();
|
|
34
|
+
|
|
35
|
+
this.lifecycleManager = new DockerLifecycleManager({
|
|
36
|
+
containerName: config.containerName,
|
|
37
|
+
composeFile: config.composeFile,
|
|
38
|
+
healthEndpoint: config.healthEndpoint,
|
|
39
|
+
autoStart,
|
|
40
|
+
autoStop,
|
|
41
|
+
initTimeoutMs,
|
|
42
|
+
projectRoot,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected getDocsUrl(): string {
|
|
47
|
+
return "https://docs.searxng.org/";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected getApiKeyEnv(): string {
|
|
51
|
+
return this.config.apiKeyEnv ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override getMetadata(): ProviderMetadata {
|
|
55
|
+
return {
|
|
56
|
+
id: this.id,
|
|
57
|
+
displayName: "SearXNG (Local)",
|
|
58
|
+
docsUrl: this.getDocsUrl(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected override getApiKey(): string {
|
|
63
|
+
const apiKeyEnv = this.getApiKeyEnv();
|
|
64
|
+
if (apiKeyEnv) {
|
|
65
|
+
return process.env[apiKeyEnv] ?? "";
|
|
66
|
+
}
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async search(query: SearchQuery): Promise<SearchResponse> {
|
|
71
|
+
const isHealthy = await this.healthcheck();
|
|
72
|
+
|
|
73
|
+
if (!isHealthy) {
|
|
74
|
+
const isInitializing = !!(await (this.lifecycleManager as any).initPromise);
|
|
75
|
+
if (!isInitializing) {
|
|
76
|
+
try {
|
|
77
|
+
console.log(`[SearXNG] Container not healthy, attempting auto-start...`);
|
|
78
|
+
await this.init();
|
|
79
|
+
} catch (initError) {
|
|
80
|
+
console.error(`[SearXNG] Failed to auto-start container:`, initError);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!isHealthy) {
|
|
86
|
+
throw new SearchError(
|
|
87
|
+
this.id,
|
|
88
|
+
"provider_unavailable",
|
|
89
|
+
"SearXNG container is not healthy. Check logs with: docker compose logs -f searxng",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const limit = query.limit ?? this.defaultLimit;
|
|
94
|
+
const url = buildUrl(this.config.endpoint, {
|
|
95
|
+
q: query.query,
|
|
96
|
+
format: "json",
|
|
97
|
+
language: "all",
|
|
98
|
+
pageno: 1,
|
|
99
|
+
safesearch: 0,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const { data: json, tookMs } = await fetchWithErrorHandling<SearxngApiResponse>(
|
|
103
|
+
this.id,
|
|
104
|
+
url,
|
|
105
|
+
{
|
|
106
|
+
method: "GET",
|
|
107
|
+
headers: {
|
|
108
|
+
Accept: "application/json",
|
|
109
|
+
"X-Forwarded-For": "127.0.0.1",
|
|
110
|
+
"X-Real-IP": "127.0.0.1",
|
|
111
|
+
},
|
|
112
|
+
timeoutMs: PROVIDER_DEFAULTS.DEFAULT_TIMEOUT_MS,
|
|
113
|
+
},
|
|
114
|
+
"SearXNG",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const results: SearxngSearchResult[] = json.results ?? [];
|
|
118
|
+
|
|
119
|
+
this.validateResults(results, "SearXNG");
|
|
120
|
+
|
|
121
|
+
const items: SearchResultItem[] = results.map((r: SearxngSearchResult) => ({
|
|
122
|
+
title: r.title ?? r.url ?? "#",
|
|
123
|
+
url: r.url ?? "#",
|
|
124
|
+
snippet: r.content ?? r.description ?? "",
|
|
125
|
+
score: r.score ?? r.rank,
|
|
126
|
+
sourceEngine: r.engine ?? this.id,
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
const limitedItems = items.slice(0, limit);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
engineId: this.id,
|
|
133
|
+
items: limitedItems,
|
|
134
|
+
raw: query.includeRaw ? json : undefined,
|
|
135
|
+
tookMs,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ILifecycleProvider implementation
|
|
140
|
+
async init(): Promise<void> {
|
|
141
|
+
await this.lifecycleManager.init();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async healthcheck(): Promise<boolean> {
|
|
145
|
+
return await this.lifecycleManager.healthcheck();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async shutdown(): Promise<void> {
|
|
149
|
+
await this.lifecycleManager.shutdown();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async validateConfig(): Promise<{
|
|
153
|
+
valid: boolean;
|
|
154
|
+
errors: string[];
|
|
155
|
+
warnings: string[];
|
|
156
|
+
}> {
|
|
157
|
+
return await this.lifecycleManager.validateDockerConfig();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
isLifecycleManaged(): boolean {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tavily Search Provider Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TavilyConfig } from "../config/types";
|
|
6
|
+
import type { SearchQuery, SearchResponse } from "../core/types";
|
|
7
|
+
import { BaseProvider } from "./BaseProvider";
|
|
8
|
+
import { PROVIDER_DEFAULTS } from "./constants";
|
|
9
|
+
import type { TavilyApiResponse, TavilySearchResult } from "./types";
|
|
10
|
+
import { fetchWithErrorHandling } from "./utils";
|
|
11
|
+
|
|
12
|
+
export class TavilyProvider extends BaseProvider<TavilyConfig> {
|
|
13
|
+
protected getDocsUrl(): string {
|
|
14
|
+
return "https://docs.tavily.com/";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected getApiKeyEnv(): string {
|
|
18
|
+
return this.config.apiKeyEnv;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async search(query: SearchQuery): Promise<SearchResponse> {
|
|
22
|
+
const apiKey = this.getApiKey();
|
|
23
|
+
|
|
24
|
+
const requestBody = {
|
|
25
|
+
api_key: apiKey,
|
|
26
|
+
query: query.query,
|
|
27
|
+
search_depth: this.config.searchDepth,
|
|
28
|
+
max_results: query.limit ?? 5,
|
|
29
|
+
include_answer: false,
|
|
30
|
+
include_raw_content: false,
|
|
31
|
+
include_images: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Make request with error handling
|
|
35
|
+
const { data: json, tookMs } = await fetchWithErrorHandling<TavilyApiResponse>(
|
|
36
|
+
this.id,
|
|
37
|
+
this.config.endpoint,
|
|
38
|
+
{
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify(requestBody),
|
|
42
|
+
timeoutMs: PROVIDER_DEFAULTS.DEFAULT_TIMEOUT_MS,
|
|
43
|
+
},
|
|
44
|
+
"Tavily",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
this.validateResults(json.results, "Tavily");
|
|
48
|
+
|
|
49
|
+
// Map to normalized format and filter out invalid results
|
|
50
|
+
const items = json.results
|
|
51
|
+
.filter(
|
|
52
|
+
(r: TavilySearchResult): boolean =>
|
|
53
|
+
r != null &&
|
|
54
|
+
typeof r === "object" &&
|
|
55
|
+
(r.title != null || r.url != null) &&
|
|
56
|
+
(r.title != null || r.content != null || r.snippet != null),
|
|
57
|
+
)
|
|
58
|
+
.map((r: TavilySearchResult) => ({
|
|
59
|
+
title: r.title ?? r.url ?? "Untitled",
|
|
60
|
+
url: r.url ?? "",
|
|
61
|
+
snippet: r.content ?? r.snippet ?? "",
|
|
62
|
+
score: r.score,
|
|
63
|
+
sourceEngine: this.id,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
engineId: this.id,
|
|
68
|
+
items,
|
|
69
|
+
raw: query.includeRaw ? json : undefined,
|
|
70
|
+
tookMs,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider API Response Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for external search provider API responses.
|
|
5
|
+
* These types ensure type safety when parsing provider responses.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============ Tavily API Types ============
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Single result from Tavily search API
|
|
12
|
+
*/
|
|
13
|
+
export interface TavilySearchResult {
|
|
14
|
+
title?: string;
|
|
15
|
+
url: string;
|
|
16
|
+
content?: string;
|
|
17
|
+
snippet?: string;
|
|
18
|
+
score?: number;
|
|
19
|
+
published_date?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tavily search API response
|
|
24
|
+
*/
|
|
25
|
+
export interface TavilyApiResponse {
|
|
26
|
+
query?: string;
|
|
27
|
+
results: TavilySearchResult[];
|
|
28
|
+
answer?: string;
|
|
29
|
+
response_time?: number;
|
|
30
|
+
images?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============ Brave API Types ============
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Single web result from Brave search API
|
|
37
|
+
*/
|
|
38
|
+
export interface BraveWebResult {
|
|
39
|
+
title: string;
|
|
40
|
+
url: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
snippet?: string;
|
|
43
|
+
abstract?: string;
|
|
44
|
+
rank?: number;
|
|
45
|
+
score?: number;
|
|
46
|
+
age?: string;
|
|
47
|
+
language?: string;
|
|
48
|
+
family_friendly?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Brave search API response
|
|
53
|
+
*/
|
|
54
|
+
export interface BraveApiResponse {
|
|
55
|
+
type?: string;
|
|
56
|
+
query?: {
|
|
57
|
+
original: string;
|
|
58
|
+
altered?: string;
|
|
59
|
+
};
|
|
60
|
+
web?: {
|
|
61
|
+
type?: string;
|
|
62
|
+
results: BraveWebResult[];
|
|
63
|
+
family_friendly?: boolean;
|
|
64
|
+
};
|
|
65
|
+
/** Fallback for alternative response format */
|
|
66
|
+
results?: BraveWebResult[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============ Linkup API Types ============
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Single result from Linkup search API
|
|
73
|
+
*/
|
|
74
|
+
export interface LinkupSearchResult {
|
|
75
|
+
url: string;
|
|
76
|
+
name?: string;
|
|
77
|
+
title?: string;
|
|
78
|
+
content?: string;
|
|
79
|
+
snippet?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
score?: number;
|
|
82
|
+
relevance?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Linkup search API response
|
|
87
|
+
*/
|
|
88
|
+
export interface LinkupApiResponse {
|
|
89
|
+
results: LinkupSearchResult[];
|
|
90
|
+
answer?: string;
|
|
91
|
+
sources?: LinkupSearchResult[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============ SearXNG API Types ============
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Single result from SearXNG search API
|
|
98
|
+
*/
|
|
99
|
+
export interface SearxngSearchResult {
|
|
100
|
+
title?: string;
|
|
101
|
+
url: string;
|
|
102
|
+
content?: string;
|
|
103
|
+
description?: string;
|
|
104
|
+
score?: number;
|
|
105
|
+
rank?: number;
|
|
106
|
+
engine?: string;
|
|
107
|
+
parsed_url?: string[];
|
|
108
|
+
engines?: string[];
|
|
109
|
+
positions?: number[];
|
|
110
|
+
category?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* SearXNG search API response
|
|
115
|
+
*/
|
|
116
|
+
export interface SearxngApiResponse {
|
|
117
|
+
query?: string;
|
|
118
|
+
results: SearxngSearchResult[];
|
|
119
|
+
number_of_results?: number;
|
|
120
|
+
infoboxes?: unknown[];
|
|
121
|
+
suggestions?: string[];
|
|
122
|
+
answers?: string[];
|
|
123
|
+
corrections?: string[];
|
|
124
|
+
unresponsive_engines?: string[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============ Type Guards ============
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Type guard to check if a value is a valid Tavily result
|
|
131
|
+
*/
|
|
132
|
+
export function isTavilyResult(value: unknown): value is TavilySearchResult {
|
|
133
|
+
if (typeof value !== "object" || value === null) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const obj = value as Record<string, unknown>;
|
|
137
|
+
return typeof obj.url === "string" || typeof obj.title === "string";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Type guard to check if a value is a valid Tavily response
|
|
142
|
+
*/
|
|
143
|
+
export function isTavilyResponse(value: unknown): value is TavilyApiResponse {
|
|
144
|
+
if (typeof value !== "object" || value === null) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const obj = value as Record<string, unknown>;
|
|
148
|
+
return "results" in obj && Array.isArray(obj.results);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Type guard to check if a value is a valid Brave response
|
|
153
|
+
*/
|
|
154
|
+
export function isBraveResponse(value: unknown): value is BraveApiResponse {
|
|
155
|
+
if (typeof value !== "object" || value === null) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const obj = value as Record<string, unknown>;
|
|
159
|
+
return (
|
|
160
|
+
("web" in obj && typeof obj.web === "object") ||
|
|
161
|
+
("results" in obj && Array.isArray(obj.results))
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Type guard to check if a value is a valid Linkup response
|
|
167
|
+
*/
|
|
168
|
+
export function isLinkupResponse(value: unknown): value is LinkupApiResponse {
|
|
169
|
+
if (typeof value !== "object" || value === null) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const obj = value as Record<string, unknown>;
|
|
173
|
+
return "results" in obj && Array.isArray(obj.results);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Type guard to check if a value is a valid SearXNG response
|
|
178
|
+
*/
|
|
179
|
+
export function isSearxngResponse(value: unknown): value is SearxngApiResponse {
|
|
180
|
+
if (typeof value !== "object" || value === null) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const obj = value as Record<string, unknown>;
|
|
184
|
+
return "results" in obj && Array.isArray(obj.results);
|
|
185
|
+
}
|