Skip to content

Commit 9186cc8

Browse files
feat: add Seltz Search tool integration
1 parent 73b0b15 commit 9186cc8

8 files changed

Lines changed: 510 additions & 11 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { INodeParams, INodeCredential } from '../src/Interface'
2+
3+
class SeltzApi implements INodeCredential {
4+
label: string
5+
name: string
6+
version: number
7+
description: string
8+
inputs: INodeParams[]
9+
10+
constructor() {
11+
this.label = 'Seltz API'
12+
this.name = 'seltzApi'
13+
this.version = 1.0
14+
this.description =
15+
'You can get your API key from the <a target="_blank" href="https://console.seltz.ai">Seltz Console</a>. Refer to <a target="_blank" href="https://docs.seltz.ai">official docs</a> for more information.'
16+
this.inputs = [
17+
{
18+
label: 'Seltz API Key',
19+
name: 'seltzApiKey',
20+
type: 'password'
21+
}
22+
]
23+
}
24+
}
25+
26+
module.exports = { credClass: SeltzApi }
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Seltz Search Tool
2+
3+
A wrapper around the [Seltz](https://seltz.ai) Web Knowledge API. Returns web search results with source URLs, suitable for use in LLM tool-calling, agents, and RAG pipelines.
4+
5+
## Setup
6+
7+
1. Get your API key from the [Seltz Console](https://console.seltz.ai)
8+
2. In Flowise, add the **Seltz Search** tool node to your flow
9+
3. Create a new **Seltz API** credential with your API key
10+
4. Connect the credential to the tool node
11+
12+
## Configuration
13+
14+
| Parameter | Description | Required |
15+
| -------------------- | ------------------------------------------------------------------ | -------- |
16+
| **API Key** | Your Seltz API key (via credential) | Yes |
17+
| **Tool Description** | Custom description for the LLM to understand when to use this tool | No |
18+
| **Max Documents** | Maximum number of documents to return | No |
19+
| **Context** | Additional context to refine the search | No |
20+
| **Profile** | Search profile to use | No |
21+
22+
## Output Format
23+
24+
The tool returns a JSON array of documents:
25+
26+
```json
27+
[
28+
{
29+
"url": "https://example.com/article",
30+
"content": "The relevant content from the web page..."
31+
}
32+
]
33+
```
34+
35+
## Resources
36+
37+
- [Seltz Documentation](https://docs.seltz.ai)
38+
- [Seltz Console](https://console.seltz.ai)
39+
- [npm Package](https://www.npmjs.com/package/seltz)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { Tool } from '@langchain/core/tools'
2+
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
3+
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
4+
5+
const defaultDesc =
6+
'A context-engineered web search tool powered by Seltz. Useful for when you need to answer questions using fast, up-to-date web knowledge with sources for real-time AI reasoning. Input should be a search query. Output is a JSON array of documents with url and content fields.'
7+
8+
class SeltzSearch_Tools implements INode {
9+
label: string
10+
name: string
11+
version: number
12+
description: string
13+
type: string
14+
icon: string
15+
category: string
16+
baseClasses: string[]
17+
credential: INodeParams
18+
inputs: INodeParams[]
19+
20+
constructor() {
21+
this.label = 'Seltz Search'
22+
this.name = 'seltzSearch'
23+
this.version = 1.0
24+
this.type = 'SeltzSearch'
25+
this.icon = 'seltz.svg'
26+
this.category = 'Tools'
27+
this.description = 'Wrapper around Seltz Web Knowledge API - context-engineered web content with sources for real-time AI reasoning'
28+
this.inputs = [
29+
{
30+
label: 'Tool Description',
31+
name: 'toolDescription',
32+
type: 'string',
33+
rows: 4,
34+
default: defaultDesc,
35+
optional: true,
36+
additionalParams: true
37+
},
38+
{
39+
label: 'Max Documents',
40+
name: 'maxDocuments',
41+
type: 'number',
42+
description: 'Maximum number of documents to return',
43+
optional: true,
44+
step: 1,
45+
additionalParams: true
46+
},
47+
{
48+
label: 'Context',
49+
name: 'context',
50+
type: 'string',
51+
description: 'Additional context to refine the search',
52+
rows: 2,
53+
optional: true,
54+
additionalParams: true
55+
},
56+
{
57+
label: 'Profile',
58+
name: 'profile',
59+
type: 'string',
60+
description: 'Search profile to use',
61+
optional: true,
62+
additionalParams: true
63+
}
64+
]
65+
this.credential = {
66+
label: 'Connect Credential',
67+
name: 'credential',
68+
type: 'credential',
69+
credentialNames: ['seltzApi']
70+
}
71+
this.baseClasses = [this.type, ...getBaseClasses(SeltzSearchTool)]
72+
}
73+
74+
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
75+
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
76+
const seltzApiKey = getCredentialParam('seltzApiKey', credentialData, nodeData)
77+
78+
const toolDescription = nodeData.inputs?.toolDescription as string
79+
const maxDocuments = nodeData.inputs?.maxDocuments as string
80+
const context = nodeData.inputs?.context as string
81+
const profile = nodeData.inputs?.profile as string
82+
83+
const tool = new SeltzSearchTool({
84+
apiKey: seltzApiKey,
85+
maxDocuments: maxDocuments ? parseInt(maxDocuments, 10) : undefined,
86+
context: context || undefined,
87+
profile: profile || undefined
88+
})
89+
90+
if (toolDescription) tool.description = toolDescription
91+
92+
return tool
93+
}
94+
}
95+
96+
class SeltzSearchTool extends Tool {
97+
static lc_name() {
98+
return 'SeltzSearchTool'
99+
}
100+
101+
name = 'seltz-search'
102+
description = defaultDesc
103+
104+
private apiKey: string
105+
private maxDocuments?: number
106+
private context?: string
107+
private profile?: string
108+
109+
constructor({ apiKey, maxDocuments, context, profile }: { apiKey: string; maxDocuments?: number; context?: string; profile?: string }) {
110+
super()
111+
this.apiKey = apiKey
112+
this.maxDocuments = maxDocuments
113+
this.context = context
114+
this.profile = profile
115+
}
116+
117+
async _call(input: string): Promise<string> {
118+
const { Seltz } = await import('seltz')
119+
const client = new Seltz({ apiKey: this.apiKey })
120+
121+
const searchOptions: Record<string, any> = {}
122+
if (this.maxDocuments) {
123+
searchOptions.includes = { maxDocuments: this.maxDocuments }
124+
}
125+
if (this.context) {
126+
searchOptions.context = this.context
127+
}
128+
if (this.profile) {
129+
searchOptions.profile = this.profile
130+
}
131+
132+
const response = await client.search(input, Object.keys(searchOptions).length > 0 ? searchOptions : undefined)
133+
134+
if (!response.documents || response.documents.length === 0) {
135+
return 'No results found.'
136+
}
137+
138+
const results = response.documents.map((doc: any) => ({
139+
url: doc.url || '',
140+
content: doc.content || ''
141+
}))
142+
143+
return JSON.stringify(results)
144+
}
145+
}
146+
147+
module.exports = { nodeClass: SeltzSearch_Tools, SeltzSearchTool }
Lines changed: 1 addition & 0 deletions
Loading

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
"remove-markdown": "^0.6.2",
161161
"replicate": "^0.31.1",
162162
"sanitize-filename": "^1.6.3",
163+
"seltz": "^0.2.0",
163164
"srt-parser-2": "^1.2.3",
164165
"supergateway": "3.0.1",
165166
"typeorm": "^0.3.6",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Integration test for SeltzSearch tool.
3+
* Requires SELTZ_API_KEY environment variable to be set.
4+
* Run with: SELTZ_API_KEY=your-key npx jest --testPathPattern=SeltzSearch.integration
5+
*/
6+
7+
// Mock @langchain/core/tools to avoid ESM issues in Jest
8+
jest.mock('@langchain/core/tools', () => {
9+
class MockTool {
10+
name = ''
11+
description = ''
12+
}
13+
return { Tool: MockTool }
14+
})
15+
16+
jest.mock('../../../../src/utils', () => ({
17+
getBaseClasses: jest.fn().mockReturnValue(['Tool']),
18+
getCredentialData: jest.fn(),
19+
getCredentialParam: jest.fn()
20+
}))
21+
22+
const { SeltzSearchTool } = require('../../../../nodes/tools/SeltzSearch/SeltzSearch')
23+
24+
const SELTZ_API_KEY = process.env.SELTZ_API_KEY
25+
26+
const describeIfKey = SELTZ_API_KEY ? describe : describe.skip
27+
28+
describeIfKey('SeltzSearchTool Integration', () => {
29+
it('should return search results for a basic query', async () => {
30+
const tool = new SeltzSearchTool({ apiKey: SELTZ_API_KEY! })
31+
const result = await tool._call('What is Flowise AI?')
32+
33+
const parsed = JSON.parse(result)
34+
expect(Array.isArray(parsed)).toBe(true)
35+
expect(parsed.length).toBeGreaterThan(0)
36+
expect(parsed[0]).toHaveProperty('url')
37+
expect(parsed[0]).toHaveProperty('content')
38+
expect(parsed[0].url).toMatch(/^https?:\/\//)
39+
expect(parsed[0].content.length).toBeGreaterThan(0)
40+
}, 30000)
41+
42+
it('should respect maxDocuments option', async () => {
43+
const tool = new SeltzSearchTool({ apiKey: SELTZ_API_KEY!, maxDocuments: 3 })
44+
const result = await tool._call('TypeScript programming')
45+
46+
const parsed = JSON.parse(result)
47+
expect(parsed.length).toBeLessThanOrEqual(3)
48+
expect(parsed.length).toBeGreaterThan(0)
49+
}, 30000)
50+
51+
it('should work with context option', async () => {
52+
const tool = new SeltzSearchTool({
53+
apiKey: SELTZ_API_KEY!,
54+
context: 'open source AI workflow tools',
55+
maxDocuments: 5
56+
})
57+
const result = await tool._call('low-code AI automation')
58+
59+
const parsed = JSON.parse(result)
60+
expect(Array.isArray(parsed)).toBe(true)
61+
expect(parsed.length).toBeGreaterThan(0)
62+
}, 30000)
63+
})

0 commit comments

Comments
 (0)