From 32b80b45fe8ebd48c92933a2ba31e3bcba497d00 Mon Sep 17 00:00:00 2001 From: aditya mer Date: Tue, 12 May 2026 21:36:59 +0530 Subject: [PATCH 1/3] feat: add LangSearch API integration and update search provider options --- env.example | 3 +- src/components/select-list.ts | 7 ++- src/controllers/search-selection.ts | 2 +- src/tools/registry.ts | 5 +- src/tools/search/index.ts | 1 + src/tools/search/langsearch.ts | 91 +++++++++++++++++++++++++++++ src/utils/env.ts | 3 +- 7 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 src/tools/search/langsearch.ts diff --git a/env.example b/env.example index 14ffa33a8..5ea4b4061 100644 --- a/env.example +++ b/env.example @@ -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 diff --git a/src/components/select-list.ts b/src/components/select-list.ts index da8eb3178..780948742 100644 --- a/src/components/select-list.ts +++ b/src/components/select-list.ts @@ -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; } diff --git a/src/controllers/search-selection.ts b/src/controllers/search-selection.ts index 7cb9e4d82..13a52f88f 100644 --- a/src/controllers/search-selection.ts +++ b/src/controllers/search-selection.ts @@ -115,7 +115,7 @@ export class SearchSelectionController { const displayName = getSearchProviderDisplayName(providerId); this.commitPreference(providerId); - if (providerId !== 'perplexity') { + if (providerId !== 'perplexity' && providerId !== 'langsearch') { this.onError(`API key saved. Restart Dexter to use ${displayName}.`); } } diff --git a/src/tools/registry.ts b/src/tools/registry.ts index 52460c7be..cae96e6f3 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -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, langSearchTool, 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'; @@ -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: langSearchTool }); + } if (allWebSearchProviders.length > 0) { const preferred = getSetting('webSearchPreferredProvider', undefined); diff --git a/src/tools/search/index.ts b/src/tools/search/index.ts index e8974b116..fab6df33a 100644 --- a/src/tools/search/index.ts +++ b/src/tools/search/index.ts @@ -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 { langSearchTool } from './langsearch.js'; export { xSearchTool, X_SEARCH_DESCRIPTION } from './x-search.js'; diff --git a/src/tools/search/langsearch.ts b/src/tools/search/langsearch.ts new file mode 100644 index 000000000..17c4b9a17 --- /dev/null +++ b/src/tools/search/langsearch.ts @@ -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 { + 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; +} + +export const langSearchTool = new DynamicStructuredTool({ + name: 'web_search', + description: + 'Search the web for current information on any topic. Returns relevant search results with URLs and content summaries.', + 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}`); + } + }, +}); \ No newline at end of file diff --git a/src/utils/env.ts b/src/utils/env.ts index 0c1c23d01..54aceed76 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -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 = { 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 { From 409bcca724576a54ebdebb0526370b8dfb36af11 Mon Sep 17 00:00:00 2001 From: virattt Date: Tue, 12 May 2026 15:21:25 -0400 Subject: [PATCH 2/3] refactor: drop misleading restart message, fix naming and description nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-query Agent.create call invokes getToolRegistry, which reads process.env fresh. saveApiKeyToEnv reloads .env into process.env via dotenv override. So no provider needs a restart — replace the misleading "Restart Dexter to use X" message (carved out only for perplexity, now also langsearch) with a single accurate "X is now active" confirmation. Also: - Rename langSearchTool -> langSearch to match exaSearch/tavilySearch/ perplexitySearch - Sync description "content summaries" -> "content snippets" for consistency with other web_search providers - Add trailing newline to langsearch.ts Co-Authored-By: Claude Opus 4.7 (1M context) --- src/controllers/search-selection.ts | 5 +---- src/tools/registry.ts | 4 ++-- src/tools/search/index.ts | 2 +- src/tools/search/langsearch.ts | 6 +++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/controllers/search-selection.ts b/src/controllers/search-selection.ts index 13a52f88f..c03544c68 100644 --- a/src/controllers/search-selection.ts +++ b/src/controllers/search-selection.ts @@ -114,10 +114,7 @@ export class SearchSelectionController { const providerId = this.pendingProviderValue; const displayName = getSearchProviderDisplayName(providerId); this.commitPreference(providerId); - - if (providerId !== 'perplexity' && providerId !== 'langsearch') { - this.onError(`API key saved. Restart Dexter to use ${displayName}.`); - } + this.onError(`API key saved. ${displayName} is now active.`); } private commitPreference(providerId: SearchProviderId) { diff --git a/src/tools/registry.ts b/src/tools/registry.ts index cae96e6f3..76e51acc9 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -1,6 +1,6 @@ import { StructuredToolInterface } from '@langchain/core/tools'; import { createGetFinancials, createGetMarketData, createReadFilings, createScreenStocks } from './finance/index.js'; -import { exaSearch, perplexitySearch, tavilySearch, langSearchTool, 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'; @@ -157,7 +157,7 @@ export function getToolRegistry(model: string): RegisteredTool[] { allWebSearchProviders.push({ id: 'tavily', name: 'Tavily', tool: tavilySearch }); } if (process.env.LANGSEARCH_API_KEY) { - allWebSearchProviders.push({ id: 'langsearch', name: 'LangSearch', tool: langSearchTool }); + allWebSearchProviders.push({ id: 'langsearch', name: 'LangSearch', tool: langSearch }); } if (allWebSearchProviders.length > 0) { diff --git a/src/tools/search/index.ts b/src/tools/search/index.ts index fab6df33a..220b4541c 100644 --- a/src/tools/search/index.ts +++ b/src/tools/search/index.ts @@ -29,5 +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 { langSearchTool } from './langsearch.js'; +export { langSearch } from './langsearch.js'; export { xSearchTool, X_SEARCH_DESCRIPTION } from './x-search.js'; diff --git a/src/tools/search/langsearch.ts b/src/tools/search/langsearch.ts index 17c4b9a17..ee5fa0a72 100644 --- a/src/tools/search/langsearch.ts +++ b/src/tools/search/langsearch.ts @@ -52,10 +52,10 @@ async function callLangSearch(query: string): Promise { return response.json() as Promise; } -export const langSearchTool = new DynamicStructuredTool({ +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 summaries.', + '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'), }), @@ -88,4 +88,4 @@ export const langSearchTool = new DynamicStructuredTool({ throw new Error(`[LangSearch API] ${message}`); } }, -}); \ No newline at end of file +}); From 296310ed3263f1ba92d6d63202fbdce8df366edb Mon Sep 17 00:00:00 2001 From: virattt Date: Tue, 12 May 2026 15:31:12 -0400 Subject: [PATCH 3/3] fix: drop API-key-saved confirmation that misused onError The previous commit replaced "Restart Dexter to use X" with "X is now active" but kept routing through onError, which logs at ERROR level and renders as "Error: ..." in red. Removing the confirmation entirely matches perplexity's longstanding silent-success path; the selector overlay closes on commit and /search shows the active provider with a checkmark, so implicit feedback is sufficient. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/controllers/search-selection.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/controllers/search-selection.ts b/src/controllers/search-selection.ts index c03544c68..692a7fd75 100644 --- a/src/controllers/search-selection.ts +++ b/src/controllers/search-selection.ts @@ -111,10 +111,7 @@ export class SearchSelectionController { return; } - const providerId = this.pendingProviderValue; - const displayName = getSearchProviderDisplayName(providerId); - this.commitPreference(providerId); - this.onError(`API key saved. ${displayName} is now active.`); + this.commitPreference(this.pendingProviderValue); } private commitPreference(providerId: SearchProviderId) {