Skip to content

Commit a264b12

Browse files
HenryHengZJ0xi4o
authored andcommitted
Bugfix/Searxng tool not working (#3263)
fix searxng tool not working
1 parent 7f82587 commit a264b12

File tree

1 file changed

+197
-16
lines changed

1 file changed

+197
-16
lines changed

packages/components/nodes/tools/Searxng/Searxng.ts

Lines changed: 197 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,56 @@
1-
import { SearxngSearch } from '@langchain/community/tools/searxng_search'
1+
import { Tool } from '@langchain/core/tools'
22
import { INode, INodeData, INodeParams } from '../../../src/Interface'
33
import { getBaseClasses } from '../../../src/utils'
44

5+
interface SearxngResults {
6+
query: string
7+
number_of_results: number
8+
results: Array<{
9+
url: string
10+
title: string
11+
content: string
12+
img_src: string
13+
engine: string
14+
parsed_url: Array<string>
15+
template: string
16+
engines: Array<string>
17+
positions: Array<number>
18+
score: number
19+
category: string
20+
pretty_url: string
21+
open_group?: boolean
22+
close_group?: boolean
23+
}>
24+
answers: Array<string>
25+
corrections: Array<string>
26+
infoboxes: Array<{
27+
infobox: string
28+
content: string
29+
engine: string
30+
engines: Array<string>
31+
}>
32+
suggestions: Array<string>
33+
unresponsive_engines: Array<string>
34+
}
35+
36+
interface SearxngCustomHeaders {
37+
[key: string]: string
38+
}
39+
40+
interface SearxngSearchParams {
41+
numResults?: number
42+
categories?: string
43+
engines?: string
44+
language?: string
45+
pageNumber?: number
46+
timeRange?: number
47+
format?: string
48+
resultsOnNewTab?: 0 | 1
49+
imageProxy?: boolean
50+
autocomplete?: string
51+
safesearch?: 0 | 1 | 2
52+
}
53+
554
class Searxng_Tools implements INode {
655
label: string
756
name: string
@@ -16,7 +65,7 @@ class Searxng_Tools implements INode {
1665
constructor() {
1766
this.label = 'SearXNG'
1867
this.name = 'searXNG'
19-
this.version = 1.0
68+
this.version = 2.0
2069
this.type = 'SearXNG'
2170
this.icon = 'SearXNG.svg'
2271
this.category = 'Tools'
@@ -28,6 +77,33 @@ class Searxng_Tools implements INode {
2877
type: 'string',
2978
default: 'http://searxng:8080'
3079
},
80+
{
81+
label: 'Headers',
82+
name: 'headers',
83+
type: 'json',
84+
description: 'Custom headers for the request',
85+
optional: true,
86+
additionalParams: true
87+
},
88+
{
89+
label: 'Format',
90+
name: 'format',
91+
type: 'options',
92+
options: [
93+
{
94+
label: 'JSON',
95+
name: 'json'
96+
},
97+
{
98+
label: 'HTML',
99+
name: 'html'
100+
}
101+
],
102+
default: 'json',
103+
description:
104+
'Format of the response. You need to enable search formats in settings.yml. Refer to <a target="_blank" href="https://docs.flowiseai.com/integrations/langchain/tools/searchapi">SearXNG Setup Guide</a> for more details.',
105+
additionalParams: true
106+
},
31107
{
32108
label: 'Categories',
33109
name: 'categories',
@@ -86,34 +162,139 @@ class Searxng_Tools implements INode {
86162

87163
async init(nodeData: INodeData, _: string): Promise<any> {
88164
const apiBase = nodeData.inputs?.apiBase as string
165+
const headers = nodeData.inputs?.headers as string
89166
const categories = nodeData.inputs?.categories as string
90167
const engines = nodeData.inputs?.engines as string
91168
const language = nodeData.inputs?.language as string
92-
const pageno = nodeData.inputs?.pageno as number
169+
const pageno = nodeData.inputs?.pageno as string
93170
const time_range = nodeData.inputs?.time_range as string
94171
const safesearch = nodeData.inputs?.safesearch as 0 | 1 | 2 | undefined
95-
const format = 'json' as 'json'
96-
97-
const params = {
98-
format,
99-
categories,
100-
engines,
101-
language,
102-
pageno,
103-
time_range,
104-
safesearch
105-
}
172+
const format = nodeData.inputs?.format as string
106173

107-
const headers = {}
174+
const params: SearxngSearchParams = {}
175+
176+
if (categories) params.categories = categories
177+
if (engines) params.engines = engines
178+
if (language) params.language = language
179+
if (pageno) params.pageNumber = parseFloat(pageno)
180+
if (time_range) params.timeRange = parseFloat(time_range)
181+
if (safesearch) params.safesearch = safesearch
182+
if (format) params.format = format
183+
184+
let customHeaders = undefined
185+
if (headers) {
186+
customHeaders = typeof headers === 'string' ? JSON.parse(headers) : headers
187+
}
108188

109189
const tool = new SearxngSearch({
110190
apiBase,
111191
params,
112-
headers
192+
headers: customHeaders
113193
})
114194

115195
return tool
116196
}
117197
}
118198

199+
class SearxngSearch extends Tool {
200+
static lc_name() {
201+
return 'SearxngSearch'
202+
}
203+
204+
name = 'searxng-search'
205+
206+
description =
207+
'A meta search engine. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results'
208+
209+
protected apiBase?: string
210+
211+
protected params?: SearxngSearchParams = {
212+
numResults: 10,
213+
pageNumber: 1,
214+
imageProxy: true,
215+
safesearch: 0
216+
}
217+
218+
protected headers?: SearxngCustomHeaders
219+
220+
get lc_secrets(): { [key: string]: string } | undefined {
221+
return {
222+
apiBase: 'SEARXNG_API_BASE'
223+
}
224+
}
225+
226+
constructor({ apiBase, params, headers }: { apiBase?: string; params?: SearxngSearchParams; headers?: SearxngCustomHeaders }) {
227+
super(...arguments)
228+
229+
this.apiBase = apiBase
230+
this.headers = { 'content-type': 'application/json', ...headers }
231+
232+
if (!this.apiBase) {
233+
throw new Error(`SEARXNG_API_BASE not set. You can set it as "SEARXNG_API_BASE" in your environment variables.`)
234+
}
235+
236+
if (params) {
237+
this.params = { ...this.params, ...params }
238+
}
239+
}
240+
241+
protected buildUrl<P extends SearxngSearchParams>(path: string, parameters: P, baseUrl: string): string {
242+
const nonUndefinedParams: [string, string][] = Object.entries(parameters)
243+
.filter(([_, value]) => value !== undefined)
244+
.map(([key, value]) => [key, value.toString()]) // Avoid string conversion
245+
const searchParams = new URLSearchParams(nonUndefinedParams)
246+
return `${baseUrl}/${path}?${searchParams}`
247+
}
248+
249+
async _call(input: string): Promise<string> {
250+
const queryParams = {
251+
q: input,
252+
...this.params
253+
}
254+
const url = this.buildUrl('search', queryParams, this.apiBase as string)
255+
256+
const resp = await fetch(url, {
257+
method: 'POST',
258+
headers: this.headers,
259+
signal: AbortSignal.timeout(5 * 1000) // 5 seconds
260+
})
261+
262+
if (!resp.ok) {
263+
throw new Error(resp.statusText)
264+
}
265+
266+
const res: SearxngResults = await resp.json()
267+
268+
if (!res.results.length && !res.answers.length && !res.infoboxes.length && !res.suggestions.length) {
269+
return 'No good results found.'
270+
} else if (res.results.length) {
271+
const response: string[] = []
272+
273+
res.results.forEach((r) => {
274+
response.push(
275+
JSON.stringify({
276+
title: r.title || '',
277+
link: r.url || '',
278+
snippet: r.content || ''
279+
})
280+
)
281+
})
282+
283+
return response.slice(0, this.params?.numResults).toString()
284+
} else if (res.answers.length) {
285+
return res.answers[0]
286+
} else if (res.infoboxes.length) {
287+
return res.infoboxes[0]?.content.replaceAll(/<[^>]+>/gi, '')
288+
} else if (res.suggestions.length) {
289+
let suggestions = 'Suggestions: '
290+
res.suggestions.forEach((s) => {
291+
suggestions += `${s}, `
292+
})
293+
return suggestions
294+
} else {
295+
return 'No good results found.'
296+
}
297+
}
298+
}
299+
119300
module.exports = { nodeClass: Searxng_Tools }

0 commit comments

Comments
 (0)