Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ OLLAMA_BASE_URL=http://127.0.0.1:11434
# Stock Market API Key
FINANCIAL_DATASETS_API_KEY=your-financial-datasets-api-key

# Web Search API Keys (Exa → Perplexity → Tavily)
# Web Search API Keys (Exa → Perplexity → Tavily → LangSearch)
EXASEARCH_API_KEY=your-exa-api-key
PERPLEXITY_API_KEY=your-perplexity-api-key
TAVILY_API_KEY=your-tavily-api-key
LANGSEARCH_API_KEY=your-langsearch-api-key

# X/Twitter API (enables x_search tool for public sentiment research)
X_BEARER_TOKEN=your-X-bearer-token
Expand Down
7 changes: 4 additions & 3 deletions src/components/select-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,21 @@ export function createProviderSelector(

export function createSearchProviderSelector(
currentProvider: string,
onSelect: (providerId: 'exa' | 'perplexity' | 'tavily') => void,
onSelect: (providerId: 'exa' | 'perplexity' | 'tavily' | 'langsearch') => void,
onCancel: () => void,
) {
const providers: { id: 'exa' | 'perplexity' | 'tavily'; displayName: string }[] = [
const providers: { id: 'exa' | 'perplexity' | 'tavily' | 'langsearch'; displayName: string }[] = [
{ id: 'exa', displayName: 'Exa' },
{ id: 'perplexity', displayName: 'Perplexity' },
{ id: 'tavily', displayName: 'Tavily' },
{ id: 'langsearch', displayName: 'LangSearch' },
];
const items: SelectItem[] = providers.map((provider, index) => ({
value: provider.id,
label: `${index + 1}. ${provider.displayName}${currentProvider === provider.id ? ' ✓' : ''}`,
}));
const list = new VimSelectList(items, 5, selectListTheme);
list.onSelect = (item) => onSelect(item.value as 'exa' | 'perplexity' | 'tavily');
list.onSelect = (item) => onSelect(item.value as 'exa' | 'perplexity' | 'tavily' | 'langsearch');
list.onCancel = () => onCancel();
return list;
}
Expand Down
8 changes: 1 addition & 7 deletions src/controllers/search-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,7 @@ export class SearchSelectionController {
return;
}

const providerId = this.pendingProviderValue;
const displayName = getSearchProviderDisplayName(providerId);
this.commitPreference(providerId);

if (providerId !== 'perplexity') {
this.onError(`API key saved. Restart Dexter to use ${displayName}.`);
}
this.commitPreference(this.pendingProviderValue);
}

private commitPreference(providerId: SearchProviderId) {
Expand Down
5 changes: 4 additions & 1 deletion src/tools/registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { StructuredToolInterface } from '@langchain/core/tools';
import { createGetFinancials, createGetMarketData, createReadFilings, createScreenStocks } from './finance/index.js';
import { exaSearch, perplexitySearch, tavilySearch, WEB_SEARCH_DESCRIPTION, xSearchTool, X_SEARCH_DESCRIPTION } from './search/index.js';
import { exaSearch, perplexitySearch, tavilySearch, langSearch, WEB_SEARCH_DESCRIPTION, xSearchTool, X_SEARCH_DESCRIPTION } from './search/index.js';
import { createWebSearchTool, type WebSearchProvider } from './search/web-search.js';
import { getSetting } from '../utils/config.js';
import type { SearchProviderId } from '../utils/env.js';
Expand Down Expand Up @@ -156,6 +156,9 @@ export function getToolRegistry(model: string): RegisteredTool[] {
if (process.env.TAVILY_API_KEY) {
allWebSearchProviders.push({ id: 'tavily', name: 'Tavily', tool: tavilySearch });
}
if (process.env.LANGSEARCH_API_KEY) {
allWebSearchProviders.push({ id: 'langsearch', name: 'LangSearch', tool: langSearch });
}

if (allWebSearchProviders.length > 0) {
const preferred = getSetting<SearchProviderId | undefined>('webSearchPreferredProvider', undefined);
Expand Down
1 change: 1 addition & 0 deletions src/tools/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ Search the web for current information on any topic. Returns relevant search res
export { tavilySearch } from './tavily.js';
export { exaSearch } from './exa.js';
export { perplexitySearch } from './perplexity.js';
export { langSearch } from './langsearch.js';
export { xSearchTool, X_SEARCH_DESCRIPTION } from './x-search.js';
91 changes: 91 additions & 0 deletions src/tools/search/langsearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { formatToolResult } from '../types.js';
import { logger } from '@/utils';

const LANGSEARCH_API_URL = 'https://api.langsearch.com/v1/web-search';

interface LangSearchWebPage {
name: string;
url: string;
snippet?: string;
summary?: string;
}

interface LangSearchResponse {
code: number;
msg?: string | null;
data?: {
_type?: string;
queryContext?: { originalQuery?: string };
webPages?: {
totalEstimatedMatches?: number | null;
value?: LangSearchWebPage[];
};
};
}

async function callLangSearch(query: string): Promise<LangSearchResponse> {
const apiKey = process.env.LANGSEARCH_API_KEY;
if (!apiKey) {
throw new Error('[LangSearch API] LANGSEARCH_API_KEY is not set');
}

const response = await fetch(LANGSEARCH_API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
summary: true,
count: 10,
}),
});

if (!response.ok) {
const text = await response.text();
throw new Error(`[LangSearch API] ${response.status}: ${text}`);
}

return response.json() as Promise<LangSearchResponse>;
}

export const langSearch = new DynamicStructuredTool({
name: 'web_search',
description:
'Search the web for current information on any topic. Returns relevant search results with URLs and content snippets.',
schema: z.object({
query: z.string().describe('The search query to look up on the web'),
}),
func: async (input) => {
try {
const res = await callLangSearch(input.query);

if (res.code !== 200) {
throw new Error(`[LangSearch API] Error code ${res.code}: ${res.msg ?? 'Unknown error'}`);
}

const results = res.data?.webPages?.value ?? [];
const urls: string[] = [];
const formattedResults = results.map((r) => {
if (r.url && !urls.includes(r.url)) {
urls.push(r.url);
}
return {
title: r.name,
url: r.url,
snippet: r.summary ?? r.snippet ?? undefined,
};
});

const data = { results: formattedResults };
return formatToolResult(data, urls.length ? urls : undefined);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`[LangSearch API] error: ${message}`);
throw new Error(`[LangSearch API] ${message}`);
}
},
});
3 changes: 2 additions & 1 deletion src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,13 @@ export function saveApiKeyForProvider(providerId: string, apiKey: string): boole
return saveApiKeyToEnv(apiKeyName, apiKey);
}

export type SearchProviderId = 'exa' | 'perplexity' | 'tavily';
export type SearchProviderId = 'exa' | 'perplexity' | 'tavily' | 'langsearch';

export const SEARCH_PROVIDERS: Record<SearchProviderId, { displayName: string; apiKeyEnvVar: string }> = {
exa: { displayName: 'Exa', apiKeyEnvVar: 'EXASEARCH_API_KEY' },
perplexity: { displayName: 'Perplexity', apiKeyEnvVar: 'PERPLEXITY_API_KEY' },
tavily: { displayName: 'Tavily', apiKeyEnvVar: 'TAVILY_API_KEY' },
langsearch: { displayName: 'LangSearch', apiKeyEnvVar: 'LANGSEARCH_API_KEY' },
};

export function getSearchProviderDisplayName(providerId: SearchProviderId): string {
Expand Down
Loading