1
- import { SearxngSearch } from '@langchain/community /tools/searxng_search '
1
+ import { Tool } from '@langchain/core /tools'
2
2
import { INode , INodeData , INodeParams } from '../../../src/Interface'
3
3
import { getBaseClasses } from '../../../src/utils'
4
4
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
+
5
54
class Searxng_Tools implements INode {
6
55
label : string
7
56
name : string
@@ -16,7 +65,7 @@ class Searxng_Tools implements INode {
16
65
constructor ( ) {
17
66
this . label = 'SearXNG'
18
67
this . name = 'searXNG'
19
- this . version = 1 .0
68
+ this . version = 2 .0
20
69
this . type = 'SearXNG'
21
70
this . icon = 'SearXNG.svg'
22
71
this . category = 'Tools'
@@ -28,6 +77,33 @@ class Searxng_Tools implements INode {
28
77
type : 'string' ,
29
78
default : 'http://searxng:8080'
30
79
} ,
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
+ } ,
31
107
{
32
108
label : 'Categories' ,
33
109
name : 'categories' ,
@@ -86,34 +162,139 @@ class Searxng_Tools implements INode {
86
162
87
163
async init ( nodeData : INodeData , _ : string ) : Promise < any > {
88
164
const apiBase = nodeData . inputs ?. apiBase as string
165
+ const headers = nodeData . inputs ?. headers as string
89
166
const categories = nodeData . inputs ?. categories as string
90
167
const engines = nodeData . inputs ?. engines as string
91
168
const language = nodeData . inputs ?. language as string
92
- const pageno = nodeData . inputs ?. pageno as number
169
+ const pageno = nodeData . inputs ?. pageno as string
93
170
const time_range = nodeData . inputs ?. time_range as string
94
171
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
106
173
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
+ }
108
188
109
189
const tool = new SearxngSearch ( {
110
190
apiBase,
111
191
params,
112
- headers
192
+ headers : customHeaders
113
193
} )
114
194
115
195
return tool
116
196
}
117
197
}
118
198
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
+
119
300
module . exports = { nodeClass : Searxng_Tools }
0 commit comments