@oevortex/ddg_search 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +56 -35
- package/bin/cli.js +4 -14
- package/package.json +1 -1
- package/src/index.js +3 -13
- package/src/index.ts +7 -17
- package/src/tools/iaskTool.js +101 -0
- package/src/utils/search_iask.js +381 -0
- package/src/tools/feloTool.js +0 -85
- package/src/tools/fetchUrlTool.js +0 -114
- package/src/tools/metadataTool.js +0 -54
- package/src/utils/search_felo.js +0 -237
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.1.3] - 2025-11-30
|
|
6
|
+
### Changed
|
|
7
|
+
- Replaced Felo AI tool with IAsk AI tool for advanced AI-powered search
|
|
8
|
+
- Added `src/utils/search_iask.js` implementing IAsk API client
|
|
9
|
+
- Added `src/tools/iaskTool.js` tool definition and handler
|
|
10
|
+
- Updated `src/index.ts` to use IAsk tool instead of Felo
|
|
11
|
+
- Updated `package.json` description, keywords, and dependencies (`turndown`, `ws`)
|
|
12
|
+
- Updated `README.md` to reference IAsk AI and document new tool parameters
|
|
13
|
+
- Removed old Felo tool files (`feloTool.js`, `search_felo.js`)
|
|
14
|
+
|
|
15
|
+
## [1.1.2] - 2025-11-29
|
|
16
|
+
### Added
|
|
17
|
+
- Initial release with DuckDuckGo and Felo AI search tools
|
|
18
|
+
- MCP server implementation
|
|
19
|
+
- Caching, rotating user agents, and web scraping features
|
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
<img src="https://img.shields.io/npm/v/@oevortex/ddg_search.svg" alt="npm version" />
|
|
3
3
|
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
|
|
4
4
|
<img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" />
|
|
5
|
-
<h1>DuckDuckGo &
|
|
6
|
-
<p>A blazing-fast, privacy-friendly Model Context Protocol (MCP) server for web search and AI-powered responses using DuckDuckGo and
|
|
5
|
+
<h1>DuckDuckGo & IAsk AI Search MCP 🔍🧠</h1>
|
|
6
|
+
<p>A blazing-fast, privacy-friendly Model Context Protocol (MCP) server for web search and AI-powered responses using DuckDuckGo and IAsk AI.</p>
|
|
7
7
|
<a href="https://glama.ai/mcp/servers/@OEvortex/ddg_search">
|
|
8
8
|
<img width="380" height="200" src="https://glama.ai/mcp/servers/@OEvortex/ddg_search/badge" alt="DuckDuckGo Search MCP server" />
|
|
9
9
|
</a>
|
|
@@ -20,9 +20,7 @@
|
|
|
20
20
|
## ✨ Features
|
|
21
21
|
|
|
22
22
|
<div style="display: flex; flex-wrap: wrap; gap: 1.5em; margin-bottom: 1.5em;"> <div><b>🌐 Web search</b> using DuckDuckGo HTML</div>
|
|
23
|
-
<div><b>🧠 AI search</b> using
|
|
24
|
-
<div><b>📄 URL content extraction</b> with smart filtering</div>
|
|
25
|
-
<div><b>📊 URL metadata extraction</b> (title, description, images)</div>
|
|
23
|
+
<div><b>🧠 AI search</b> using IAsk AI</div>
|
|
26
24
|
<div><b>⚡ Performance optimized</b> with caching</div>
|
|
27
25
|
<div><b>🛡️ Security features</b> including rate limiting and rotating user agents</div>
|
|
28
26
|
<div><b>🔌 MCP-compliant</b> server implementation</div>
|
|
@@ -52,7 +50,7 @@ npx -y @oevortex/ddg_search@latest
|
|
|
52
50
|
## 🛠️ Installation Options
|
|
53
51
|
|
|
54
52
|
<details>
|
|
55
|
-
<summary><b>Global Installation</b></summary>
|
|
53
|
+
<summary><b>Global Installation (npm)</b></summary>
|
|
56
54
|
|
|
57
55
|
```bash
|
|
58
56
|
npm install -g @oevortex/ddg_search
|
|
@@ -66,6 +64,36 @@ ddg-search-mcp
|
|
|
66
64
|
|
|
67
65
|
</details>
|
|
68
66
|
|
|
67
|
+
<details>
|
|
68
|
+
<summary><b>Global Installation (Yarn)</b></summary>
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
yarn global add @oevortex/ddg_search
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run globally:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ddg-search-mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
</details>
|
|
81
|
+
|
|
82
|
+
<details>
|
|
83
|
+
<summary><b>Global Installation (pnpm)</b></summary>
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm add -g @oevortex/ddg_search
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run globally:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
ddg-search-mcp
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
</details>
|
|
96
|
+
|
|
69
97
|
<details>
|
|
70
98
|
<summary><b>Local Installation (Development)</b></summary>
|
|
71
99
|
|
|
@@ -76,6 +104,20 @@ npm install
|
|
|
76
104
|
npm start
|
|
77
105
|
```
|
|
78
106
|
|
|
107
|
+
Or with Yarn:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
yarn install
|
|
111
|
+
yarn start
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Or with pnpm:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm install
|
|
118
|
+
pnpm start
|
|
119
|
+
```
|
|
120
|
+
|
|
79
121
|
</details>
|
|
80
122
|
|
|
81
123
|
---
|
|
@@ -140,34 +182,15 @@ Or if installed globally:
|
|
|
140
182
|
<i>Example: Search the web for "climate change solutions"</i>
|
|
141
183
|
</div>
|
|
142
184
|
<div style="margin-bottom: 1.5em;">
|
|
143
|
-
<b>🧠
|
|
144
|
-
<code>
|
|
185
|
+
<b>🧠 IAsk AI Search Tool</b><br/>
|
|
186
|
+
<code>iask-search</code><br/>
|
|
145
187
|
<ul>
|
|
146
|
-
<li><b>query</b> (string, required): The search query or
|
|
188
|
+
<li><b>query</b> (string, required): The search query or question</li>
|
|
189
|
+
<li><b>mode</b> (string, optional, default: "question"): Search mode - "question", "academic", "forums", "wiki", or "thinking"</li>
|
|
190
|
+
<li><b>detailLevel</b> (string, optional): Response detail level - "concise", "detailed", or "comprehensive"</li>
|
|
147
191
|
<li><b>stream</b> (boolean, optional, default: false): Whether to stream the response</li>
|
|
148
192
|
</ul>
|
|
149
|
-
<i>Example: Search
|
|
150
|
-
</div>
|
|
151
|
-
<div style="margin-bottom: 1.5em;">
|
|
152
|
-
<b>📄 Fetch URL Tool</b><br/>
|
|
153
|
-
<code>fetch-url</code><br/>
|
|
154
|
-
<ul>
|
|
155
|
-
<li><b>url</b> (string, required): The URL to fetch</li>
|
|
156
|
-
<li><b>maxLength</b> (integer, optional, default: 10000): Max content length</li>
|
|
157
|
-
<li><b>extractMainContent</b> (boolean, optional, default: true): Extract main content</li>
|
|
158
|
-
<li><b>includeLinks</b> (boolean, optional, default: true): Include link text</li>
|
|
159
|
-
<li><b>includeImages</b> (boolean, optional, default: true): Include image alt text</li>
|
|
160
|
-
<li><b>excludeTags</b> (array, optional): Tags to exclude</li>
|
|
161
|
-
</ul>
|
|
162
|
-
<i>Example: Fetch the content from "https://example.com"</i>
|
|
163
|
-
</div>
|
|
164
|
-
<div style="margin-bottom: 1.5em;">
|
|
165
|
-
<b>📊 URL Metadata Tool</b><br/>
|
|
166
|
-
<code>url-metadata</code><br/>
|
|
167
|
-
<ul>
|
|
168
|
-
<li><b>url</b> (string, required): The URL to extract metadata from</li>
|
|
169
|
-
</ul>
|
|
170
|
-
<i>Example: Get metadata for "https://example.com"</i>
|
|
193
|
+
<i>Example: Search IAsk AI for "Explain quantum computing in simple terms"</i>
|
|
171
194
|
</div>
|
|
172
195
|
</div>
|
|
173
196
|
|
|
@@ -182,12 +205,10 @@ src/
|
|
|
182
205
|
index.js # Main entry point
|
|
183
206
|
tools/ # Tool definitions and handlers
|
|
184
207
|
searchTool.js
|
|
185
|
-
|
|
186
|
-
metadataTool.js
|
|
187
|
-
feloTool.js
|
|
208
|
+
iaskTool.js
|
|
188
209
|
utils/
|
|
189
210
|
search.js # Search and URL utilities
|
|
190
|
-
|
|
211
|
+
search_iask.js # IAsk AI search utilities
|
|
191
212
|
package.json
|
|
192
213
|
README.md
|
|
193
214
|
```
|
package/bin/cli.js
CHANGED
|
@@ -12,9 +12,9 @@ async function startServer() {
|
|
|
12
12
|
try {
|
|
13
13
|
// Dynamically import the modules
|
|
14
14
|
const { searchToolDefinition, searchToolHandler } = await import(`${modulePath}/tools/searchTool.js`);
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const { feloToolDefinition, feloToolHandler } = await import(`${modulePath}/tools/feloTool.js`);
|
|
16
|
+
|
|
17
|
+
// Create the MCP server
|
|
18
18
|
const server = new Server({
|
|
19
19
|
id: 'ddg-search-mcp',
|
|
20
20
|
name: 'DuckDuckGo & Felo AI Search MCP',
|
|
@@ -31,8 +31,6 @@ async function startServer() {
|
|
|
31
31
|
// Global variable to track available tools
|
|
32
32
|
let availableTools = [
|
|
33
33
|
searchToolDefinition,
|
|
34
|
-
fetchUrlToolDefinition,
|
|
35
|
-
metadataToolDefinition,
|
|
36
34
|
feloToolDefinition
|
|
37
35
|
];
|
|
38
36
|
|
|
@@ -56,7 +54,7 @@ async function startServer() {
|
|
|
56
54
|
const { name, arguments: args } = request.params;
|
|
57
55
|
|
|
58
56
|
// Validate tool name
|
|
59
|
-
const validTools = ['web-search', '
|
|
57
|
+
const validTools = ['web-search', 'felo-search'];
|
|
60
58
|
if (!validTools.includes(name)) {
|
|
61
59
|
throw new Error(`Unknown tool: ${name}`);
|
|
62
60
|
}
|
|
@@ -66,12 +64,6 @@ async function startServer() {
|
|
|
66
64
|
case 'web-search':
|
|
67
65
|
return await searchToolHandler(args);
|
|
68
66
|
|
|
69
|
-
case 'fetch-url':
|
|
70
|
-
return await fetchUrlToolHandler(args);
|
|
71
|
-
|
|
72
|
-
case 'url-metadata':
|
|
73
|
-
return await metadataToolHandler(args);
|
|
74
|
-
|
|
75
67
|
case 'felo-search':
|
|
76
68
|
return await feloToolHandler(args);
|
|
77
69
|
|
|
@@ -129,8 +121,6 @@ Options:
|
|
|
129
121
|
|
|
130
122
|
This MCP server provides the following tools:
|
|
131
123
|
- web-search: Search the web using DuckDuckGo
|
|
132
|
-
- fetch-url: Fetch and extract content from a URL
|
|
133
|
-
- url-metadata: Extract metadata from a URL
|
|
134
124
|
- felo-search: Search using Felo AI for AI-generated responses
|
|
135
125
|
|
|
136
126
|
Created by @OEvortex
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@oevortex/ddg_search","version":"1.1.
|
|
1
|
+
{"name":"@oevortex/ddg_search","version":"1.1.4","description":"A Model Context Protocol server for web search using DuckDuckGo and IAsk AI","main":"src/index.js","module":"src/index.ts","exports":{".":{"import":"./src/index.js","default":"./src/index.js"}},"bin":{"ddg-search-mcp":"bin/cli.js","oevortex-ddg-search":"bin/cli.js"},"scripts":{"test":"echo \"Error: no test specified\" && exit 1","start":"node bin/cli.js","prepublishOnly":"npm run lint","lint":"echo \"No linting configured\"","build":"npx @smithery/cli build","dev":"npx @smithery/cli dev"},"publishConfig":{"access":"public"},"keywords":["mcp","model-context-protocol","duckduckgo","iask","search","web-search","ai-search","claude","ai","llm"],"author":"OEvortex","license":"Apache-2.0","type":"module","dependencies":{"@modelcontextprotocol/sdk":"^1.17.4","axios":"^1.8.4","cheerio":"^1.0.0","jsdom":"^26.1.0","smithery":"^0.5.2","turndown":"^7.2.2","uuid":"^9.0.1","ws":"^8.18.3"},"devDependencies":{"@types/node":"^24.3.0","tsx":"^4.20.4","typescript":"^5.9.2"}}
|
package/src/index.js
CHANGED
|
@@ -3,19 +3,15 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
3
3
|
|
|
4
4
|
// Import tool definitions and handlers
|
|
5
5
|
import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
|
|
6
|
-
import { fetchUrlToolDefinition, fetchUrlToolHandler } from './tools/fetchUrlTool.js';
|
|
7
|
-
import { metadataToolDefinition, metadataToolHandler } from './tools/metadataTool.js';
|
|
8
6
|
import { feloToolDefinition, feloToolHandler } from './tools/feloTool.js';
|
|
9
7
|
|
|
10
8
|
// Required: Export default createServer function for Smithery
|
|
11
9
|
export default function createServer({ config } = {}) {
|
|
12
10
|
console.log('Creating MCP server with latest SDK...');
|
|
13
|
-
|
|
11
|
+
|
|
14
12
|
// Global variable to track available tools
|
|
15
13
|
const availableTools = [
|
|
16
14
|
searchToolDefinition,
|
|
17
|
-
fetchUrlToolDefinition,
|
|
18
|
-
metadataToolDefinition,
|
|
19
15
|
feloToolDefinition
|
|
20
16
|
];
|
|
21
17
|
|
|
@@ -51,16 +47,10 @@ export default function createServer({ config } = {}) {
|
|
|
51
47
|
switch (name) {
|
|
52
48
|
case 'web-search':
|
|
53
49
|
return await searchToolHandler(args);
|
|
54
|
-
|
|
55
|
-
case 'fetch-url':
|
|
56
|
-
return await fetchUrlToolHandler(args);
|
|
57
|
-
|
|
58
|
-
case 'url-metadata':
|
|
59
|
-
return await metadataToolHandler(args);
|
|
60
|
-
|
|
50
|
+
|
|
61
51
|
case 'felo-search':
|
|
62
52
|
return await feloToolHandler(args);
|
|
63
|
-
|
|
53
|
+
|
|
64
54
|
default:
|
|
65
55
|
throw new Error(`Tool not found: ${name}`);
|
|
66
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,20 +3,16 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
3
3
|
|
|
4
4
|
// Import tool definitions and handlers
|
|
5
5
|
import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
|
|
6
|
-
import {
|
|
7
|
-
import { metadataToolDefinition, metadataToolHandler } from './tools/metadataTool.js';
|
|
8
|
-
import { feloToolDefinition, feloToolHandler } from './tools/feloTool.js';
|
|
6
|
+
import { iaskToolDefinition, iaskToolHandler } from './tools/iaskTool.js';
|
|
9
7
|
|
|
10
8
|
// Required: Export default createServer function for Smithery
|
|
11
9
|
export default function createServer({ config }: { config?: any } = {}) {
|
|
12
10
|
console.log('Creating MCP server with latest SDK...');
|
|
13
|
-
|
|
11
|
+
|
|
14
12
|
// Global variable to track available tools
|
|
15
13
|
const availableTools = [
|
|
16
14
|
searchToolDefinition,
|
|
17
|
-
|
|
18
|
-
metadataToolDefinition,
|
|
19
|
-
feloToolDefinition
|
|
15
|
+
iaskToolDefinition
|
|
20
16
|
];
|
|
21
17
|
|
|
22
18
|
console.log('Available tools:', availableTools.map(t => t.name));
|
|
@@ -51,16 +47,10 @@ export default function createServer({ config }: { config?: any } = {}) {
|
|
|
51
47
|
switch (name) {
|
|
52
48
|
case 'web-search':
|
|
53
49
|
return await searchToolHandler(args);
|
|
54
|
-
|
|
55
|
-
case '
|
|
56
|
-
return await
|
|
57
|
-
|
|
58
|
-
case 'url-metadata':
|
|
59
|
-
return await metadataToolHandler(args);
|
|
60
|
-
|
|
61
|
-
case 'felo-search':
|
|
62
|
-
return await feloToolHandler(args);
|
|
63
|
-
|
|
50
|
+
|
|
51
|
+
case 'iask-search':
|
|
52
|
+
return await iaskToolHandler(args);
|
|
53
|
+
|
|
64
54
|
default:
|
|
65
55
|
throw new Error(`Tool not found: ${name}`);
|
|
66
56
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS } from '../utils/search_iask.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IAsk AI search tool definition
|
|
5
|
+
*/
|
|
6
|
+
export const iaskToolDefinition = {
|
|
7
|
+
name: 'iask-search',
|
|
8
|
+
title: 'IAsk AI Search',
|
|
9
|
+
description: 'AI-powered search using IAsk.ai. Retrieves comprehensive, AI-generated responses based on web content. Supports different search modes (question, academic, forums, wiki, thinking) and detail levels (concise, detailed, comprehensive). Ideal for getting well-researched answers to complex questions.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
query: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'The search query or question to ask. Supports natural language questions for comprehensive AI-generated responses.'
|
|
16
|
+
},
|
|
17
|
+
mode: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Search mode to use. Options: "question" (general questions), "academic" (scholarly/research), "forums" (community discussions), "wiki" (encyclopedia-style), "thinking" (deep analysis). Default is "question".',
|
|
20
|
+
enum: VALID_MODES,
|
|
21
|
+
default: 'question'
|
|
22
|
+
},
|
|
23
|
+
detailLevel: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Level of detail in the response. Options: "concise" (brief), "detailed" (moderate), "comprehensive" (extensive). Default is null (standard response).',
|
|
26
|
+
enum: VALID_DETAIL_LEVELS
|
|
27
|
+
},
|
|
28
|
+
stream: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
description: 'Enable streaming mode to receive incremental results. Default is false.',
|
|
31
|
+
default: false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: ['query']
|
|
35
|
+
},
|
|
36
|
+
annotations: {
|
|
37
|
+
readOnlyHint: true,
|
|
38
|
+
openWorldHint: false
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* IAsk AI search tool handler
|
|
44
|
+
* @param {Object} params - The tool parameters
|
|
45
|
+
* @returns {Promise<Object>} - The tool result
|
|
46
|
+
*/
|
|
47
|
+
export async function iaskToolHandler(params) {
|
|
48
|
+
const {
|
|
49
|
+
query,
|
|
50
|
+
mode = 'question',
|
|
51
|
+
detailLevel = null,
|
|
52
|
+
stream = false
|
|
53
|
+
} = params;
|
|
54
|
+
|
|
55
|
+
console.log(`Searching IAsk AI for: "${query}" (mode: ${mode}, detailLevel: ${detailLevel || 'default'}, stream: ${stream})`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (stream) {
|
|
59
|
+
// For streaming responses, collect them and return
|
|
60
|
+
let fullResponse = '';
|
|
61
|
+
const chunks = [];
|
|
62
|
+
|
|
63
|
+
for await (const chunk of await searchIAsk(query, true, false, mode, detailLevel)) {
|
|
64
|
+
chunks.push(chunk);
|
|
65
|
+
fullResponse += chunk;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: fullResponse || 'No results found.'
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
} else {
|
|
77
|
+
// For non-streaming responses
|
|
78
|
+
const response = await searchIAsk(query, false, false, mode, detailLevel);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: response || 'No results found.'
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`Error in IAsk search: ${error.message}`);
|
|
91
|
+
return {
|
|
92
|
+
isError: true,
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: 'text',
|
|
96
|
+
text: `Error searching IAsk: ${error.message}`
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import * as cheerio from 'cheerio';
|
|
3
|
+
import TurndownService from 'turndown';
|
|
4
|
+
|
|
5
|
+
// Rotating User Agents
|
|
6
|
+
const USER_AGENTS = [
|
|
7
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
8
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/120.0.0.0',
|
|
9
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
|
|
10
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
|
|
11
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// Cache results to avoid repeated requests
|
|
15
|
+
const resultsCache = new Map();
|
|
16
|
+
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
17
|
+
|
|
18
|
+
// Valid modes and detail levels
|
|
19
|
+
const VALID_MODES = ['question', 'academic', 'forums', 'wiki', 'thinking'];
|
|
20
|
+
const VALID_DETAIL_LEVELS = ['concise', 'detailed', 'comprehensive'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Response class for IAsk API responses
|
|
24
|
+
*/
|
|
25
|
+
class Response {
|
|
26
|
+
/**
|
|
27
|
+
* Create a new Response
|
|
28
|
+
* @param {string} text - The text content of the response
|
|
29
|
+
*/
|
|
30
|
+
constructor(text) {
|
|
31
|
+
this.text = text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* String representation of the response
|
|
36
|
+
* @returns {string} The text content
|
|
37
|
+
*/
|
|
38
|
+
toString() {
|
|
39
|
+
return this.text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get a random user agent from the list
|
|
45
|
+
* @returns {string} A random user agent string
|
|
46
|
+
*/
|
|
47
|
+
function getRandomUserAgent() {
|
|
48
|
+
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate a cache key for a search query
|
|
53
|
+
* @param {string} query - The search query
|
|
54
|
+
* @param {string} mode - The search mode
|
|
55
|
+
* @param {string|null} detailLevel - The detail level
|
|
56
|
+
* @returns {string} The cache key
|
|
57
|
+
*/
|
|
58
|
+
function getCacheKey(query, mode, detailLevel) {
|
|
59
|
+
return `iask-${mode}-${detailLevel || 'default'}-${query}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Clear old entries from the cache
|
|
64
|
+
*/
|
|
65
|
+
function clearOldCache() {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
for (const [key, value] of resultsCache.entries()) {
|
|
68
|
+
if (now - value.timestamp > CACHE_DURATION) {
|
|
69
|
+
resultsCache.delete(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find HTML content in a nested structure
|
|
76
|
+
* @param {Object|Array} diff - The nested structure to search
|
|
77
|
+
* @returns {string|null} The found HTML content or null
|
|
78
|
+
*/
|
|
79
|
+
function cacheFind(diff) {
|
|
80
|
+
const values = Array.isArray(diff) ? diff : Object.values(diff);
|
|
81
|
+
const turndown = new TurndownService();
|
|
82
|
+
|
|
83
|
+
for (const value of values) {
|
|
84
|
+
if (typeof value === 'object' && value !== null) {
|
|
85
|
+
const cache = cacheFind(value);
|
|
86
|
+
if (cache) return cache;
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === 'string' && /<p>.+?<\/p>/.test(value)) {
|
|
89
|
+
return turndown.turndown(value).trim();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format HTML content into readable markdown text
|
|
97
|
+
* @param {string} htmlContent - The HTML content to format
|
|
98
|
+
* @returns {string} Formatted text
|
|
99
|
+
*/
|
|
100
|
+
function formatHtml(htmlContent) {
|
|
101
|
+
const $ = cheerio.load(htmlContent);
|
|
102
|
+
const outputLines = [];
|
|
103
|
+
|
|
104
|
+
$('h1, h2, h3, p, ol, ul, div').each((_, element) => {
|
|
105
|
+
const tagName = element.tagName.toLowerCase();
|
|
106
|
+
const $el = $(element);
|
|
107
|
+
|
|
108
|
+
if (['h1', 'h2', 'h3'].includes(tagName)) {
|
|
109
|
+
outputLines.push(`\n**${$el.text().trim()}**\n`);
|
|
110
|
+
} else if (tagName === 'p') {
|
|
111
|
+
let text = $el.text().trim();
|
|
112
|
+
// Remove IAsk attribution
|
|
113
|
+
text = text.replace(/^According to Ask AI & Question AI www\.iAsk\.ai:\s*/i, '').trim();
|
|
114
|
+
// Remove footnote markers
|
|
115
|
+
text = text.replace(/\[\d+\]\(#fn:\d+ 'see footnote'\)/g, '');
|
|
116
|
+
if (text) outputLines.push(text + '\n');
|
|
117
|
+
} else if (['ol', 'ul'].includes(tagName)) {
|
|
118
|
+
$el.find('li').each((_, li) => {
|
|
119
|
+
outputLines.push('- ' + $(li).text().trim() + '\n');
|
|
120
|
+
});
|
|
121
|
+
} else if (tagName === 'div' && $el.hasClass('footnotes')) {
|
|
122
|
+
outputLines.push('\n**Authoritative Sources**\n');
|
|
123
|
+
$el.find('li').each((_, li) => {
|
|
124
|
+
const link = $(li).find('a');
|
|
125
|
+
if (link.length) {
|
|
126
|
+
outputLines.push(`- ${link.text().trim()} (${link.attr('href')})\n`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return outputLines.join('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create an axios session with proper headers
|
|
137
|
+
* @returns {Object} Axios instance
|
|
138
|
+
*/
|
|
139
|
+
function createSession() {
|
|
140
|
+
return axios.create({
|
|
141
|
+
timeout: 30000,
|
|
142
|
+
headers: {
|
|
143
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
144
|
+
'accept-encoding': 'gzip, deflate, br',
|
|
145
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
146
|
+
'cache-control': 'no-cache',
|
|
147
|
+
'dnt': '1',
|
|
148
|
+
'pragma': 'no-cache',
|
|
149
|
+
'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
|
|
150
|
+
'sec-ch-ua-mobile': '?0',
|
|
151
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
152
|
+
'sec-fetch-dest': 'document',
|
|
153
|
+
'sec-fetch-mode': 'navigate',
|
|
154
|
+
'sec-fetch-site': 'none',
|
|
155
|
+
'sec-fetch-user': '?1',
|
|
156
|
+
'upgrade-insecure-requests': '1',
|
|
157
|
+
'user-agent': getRandomUserAgent()
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Search using the IAsk AI API
|
|
164
|
+
* @param {string} prompt - The search query or prompt
|
|
165
|
+
* @param {boolean} stream - If true, yields response chunks as they arrive
|
|
166
|
+
* @param {boolean} raw - If true, returns raw response dictionaries
|
|
167
|
+
* @param {string} mode - Search mode: 'question', 'academic', 'forums', 'wiki', 'thinking'
|
|
168
|
+
* @param {string|null} detailLevel - Detail level: 'concise', 'detailed', 'comprehensive'
|
|
169
|
+
* @returns {Promise<string|AsyncGenerator<string>>} The search results
|
|
170
|
+
*/
|
|
171
|
+
async function searchIAsk(prompt, stream = false, raw = false, mode = 'question', detailLevel = null) {
|
|
172
|
+
// Validate mode
|
|
173
|
+
if (!VALID_MODES.includes(mode)) {
|
|
174
|
+
throw new Error(`Invalid mode: ${mode}. Valid modes are: ${VALID_MODES.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate detail level
|
|
178
|
+
if (detailLevel && !VALID_DETAIL_LEVELS.includes(detailLevel)) {
|
|
179
|
+
throw new Error(`Invalid detail level: ${detailLevel}. Valid levels are: ${VALID_DETAIL_LEVELS.join(', ')}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Clear old cache entries
|
|
183
|
+
clearOldCache();
|
|
184
|
+
|
|
185
|
+
// Check cache first if not streaming
|
|
186
|
+
if (!stream) {
|
|
187
|
+
const cacheKey = getCacheKey(prompt, mode, detailLevel);
|
|
188
|
+
const cachedResults = resultsCache.get(cacheKey);
|
|
189
|
+
|
|
190
|
+
if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
|
|
191
|
+
return cachedResults.results;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const session = createSession();
|
|
196
|
+
const apiEndpoint = 'https://iask.ai/';
|
|
197
|
+
|
|
198
|
+
// Build URL with parameters
|
|
199
|
+
const params = new URLSearchParams({
|
|
200
|
+
mode: mode,
|
|
201
|
+
q: prompt
|
|
202
|
+
});
|
|
203
|
+
if (detailLevel) {
|
|
204
|
+
params.append('options[detail_level]', detailLevel);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Define the streaming function
|
|
208
|
+
async function* streamFunction() {
|
|
209
|
+
try {
|
|
210
|
+
// First, get the initial page to extract tokens
|
|
211
|
+
const initialUrl = `${apiEndpoint}?${params.toString()}`;
|
|
212
|
+
const initialResponse = await session.get(initialUrl);
|
|
213
|
+
|
|
214
|
+
if (initialResponse.status !== 200) {
|
|
215
|
+
throw new Error(`Failed to get initial page - (${initialResponse.status}, ${initialResponse.statusText})`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const $ = cheerio.load(initialResponse.data);
|
|
219
|
+
|
|
220
|
+
// Extract Phoenix LiveView tokens
|
|
221
|
+
const phxNode = $('[id^="phx-"]').last();
|
|
222
|
+
const phxId = phxNode.attr('id');
|
|
223
|
+
const phxSession = phxNode.attr('data-phx-session');
|
|
224
|
+
const csrfToken = $('meta[name="csrf-token"]').attr('content');
|
|
225
|
+
|
|
226
|
+
if (!phxId || !csrfToken) {
|
|
227
|
+
throw new Error('Failed to extract required tokens from IAsk page');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Connect to WebSocket
|
|
231
|
+
const WebSocket = (await import('ws')).default;
|
|
232
|
+
const wsUrl = `wss://iask.ai/live/websocket?_csrf_token=${encodeURIComponent(csrfToken)}&vsn=2.0.0`;
|
|
233
|
+
|
|
234
|
+
const ws = new WebSocket(wsUrl, {
|
|
235
|
+
headers: {
|
|
236
|
+
'Origin': 'https://iask.ai',
|
|
237
|
+
'User-Agent': getRandomUserAgent()
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
let streamingText = '';
|
|
242
|
+
let resolveWs;
|
|
243
|
+
let rejectWs;
|
|
244
|
+
const wsPromise = new Promise((resolve, reject) => {
|
|
245
|
+
resolveWs = resolve;
|
|
246
|
+
rejectWs = reject;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const chunks = [];
|
|
250
|
+
let isComplete = false;
|
|
251
|
+
|
|
252
|
+
ws.on('open', () => {
|
|
253
|
+
// Send join message
|
|
254
|
+
const joinMessage = [
|
|
255
|
+
null,
|
|
256
|
+
null,
|
|
257
|
+
`lv:${phxId}`,
|
|
258
|
+
'phx_join',
|
|
259
|
+
{
|
|
260
|
+
params: { _csrf_token: csrfToken },
|
|
261
|
+
url: initialUrl,
|
|
262
|
+
session: phxSession
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
ws.send(JSON.stringify(joinMessage));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
ws.on('message', (data) => {
|
|
269
|
+
try {
|
|
270
|
+
const jsonData = JSON.parse(data.toString());
|
|
271
|
+
if (!jsonData || !jsonData[4]) return;
|
|
272
|
+
|
|
273
|
+
const diff = jsonData[4];
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Try to extract streaming data
|
|
277
|
+
if (diff.e && diff.e[0] && diff.e[0][1] && diff.e[0][1].data) {
|
|
278
|
+
let chunk = diff.e[0][1].data;
|
|
279
|
+
// Check if chunk contains HTML
|
|
280
|
+
if (/<[^>]+>/.test(chunk)) {
|
|
281
|
+
chunk = formatHtml(chunk);
|
|
282
|
+
} else {
|
|
283
|
+
chunk = chunk.replace(/<br\/>/g, '\n');
|
|
284
|
+
}
|
|
285
|
+
chunks.push(chunk);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Try cache find for final response
|
|
289
|
+
const cache = cacheFind(diff);
|
|
290
|
+
if (cache) {
|
|
291
|
+
if (diff.response !== undefined) {
|
|
292
|
+
// Format if it contains HTML
|
|
293
|
+
let formattedCache = cache;
|
|
294
|
+
if (/<[^>]+>/.test(cache)) {
|
|
295
|
+
formattedCache = formatHtml(cache);
|
|
296
|
+
}
|
|
297
|
+
chunks.push(formattedCache);
|
|
298
|
+
isComplete = true;
|
|
299
|
+
ws.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.debug('WebSocket message parse error:', error.message);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
ws.on('close', () => {
|
|
309
|
+
resolveWs();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
ws.on('error', (error) => {
|
|
313
|
+
rejectWs(error);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Set timeout
|
|
317
|
+
const timeout = setTimeout(() => {
|
|
318
|
+
ws.close();
|
|
319
|
+
rejectWs(new Error('WebSocket connection timed out'));
|
|
320
|
+
}, 30000);
|
|
321
|
+
|
|
322
|
+
// Wait for WebSocket to complete
|
|
323
|
+
await wsPromise;
|
|
324
|
+
clearTimeout(timeout);
|
|
325
|
+
|
|
326
|
+
// Yield all collected chunks
|
|
327
|
+
for (const chunk of chunks) {
|
|
328
|
+
streamingText += chunk;
|
|
329
|
+
if (raw) {
|
|
330
|
+
yield { text: chunk };
|
|
331
|
+
} else {
|
|
332
|
+
yield new Response(chunk).toString();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Cache the complete response
|
|
337
|
+
if (streamingText) {
|
|
338
|
+
resultsCache.set(getCacheKey(prompt, mode, detailLevel), {
|
|
339
|
+
results: streamingText,
|
|
340
|
+
timestamp: Date.now()
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error('Error searching IAsk:', error.message);
|
|
346
|
+
|
|
347
|
+
if (error.response) {
|
|
348
|
+
const status = error.response.status;
|
|
349
|
+
const statusText = error.response.statusText;
|
|
350
|
+
throw new Error(`IAsk API error: ${status} ${statusText}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
throw new Error(`Failed to search IAsk: ${error.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// If streaming is requested, return the generator
|
|
358
|
+
if (stream) {
|
|
359
|
+
return streamFunction();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// For non-streaming, collect all chunks and return as a single string
|
|
363
|
+
let fullResponse = '';
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
for await (const chunk of streamFunction()) {
|
|
367
|
+
if (raw) {
|
|
368
|
+
fullResponse += chunk.text;
|
|
369
|
+
} else {
|
|
370
|
+
fullResponse += chunk;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return fullResponse;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error('Error in non-streaming IAsk search:', error.message);
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS };
|
package/src/tools/feloTool.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { searchFelo } from '../utils/search_felo.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Felo AI search tool definition
|
|
5
|
-
*/
|
|
6
|
-
export const feloToolDefinition = {
|
|
7
|
-
name: 'felo-search',
|
|
8
|
-
title: 'Felo AI Advanced Search',
|
|
9
|
-
description: 'Advanced AI-powered web search for technical intelligence. Retrieves up-to-date information including software releases, security advisories, migration guides, benchmarks, developer documentation, and community insights. Supports both standard and streaming responses.',
|
|
10
|
-
inputSchema: {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
query: {
|
|
14
|
-
type: 'string',
|
|
15
|
-
description: 'A detailed search query or prompt describing the technical information needed. Supports natural language and keyword-based queries for precise results.'
|
|
16
|
-
},
|
|
17
|
-
stream: {
|
|
18
|
-
type: 'boolean',
|
|
19
|
-
description: 'Enable streaming mode to receive incremental, real-time search results as they are discovered. Useful for monitoring live updates or large result sets. Default is false (returns full result at once).',
|
|
20
|
-
default: false
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
required: ['query']
|
|
24
|
-
},
|
|
25
|
-
annotations: {
|
|
26
|
-
readOnlyHint: true,
|
|
27
|
-
openWorldHint: false
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Felo AI search tool handler
|
|
33
|
-
* @param {Object} params - The tool parameters
|
|
34
|
-
* @returns {Promise<Object>} - The tool result
|
|
35
|
-
*/
|
|
36
|
-
export async function feloToolHandler(params) {
|
|
37
|
-
const { query, stream = false } = params;
|
|
38
|
-
console.log(`Searching Felo AI for: "${query}" (stream: ${stream})`);
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
if (stream) {
|
|
42
|
-
// For streaming responses, we need to collect them and then return
|
|
43
|
-
let fullResponse = '';
|
|
44
|
-
const chunks = [];
|
|
45
|
-
|
|
46
|
-
for await (const chunk of await searchFelo(query, true)) {
|
|
47
|
-
chunks.push(chunk);
|
|
48
|
-
fullResponse += chunk;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Format the response
|
|
52
|
-
return {
|
|
53
|
-
content: [
|
|
54
|
-
{
|
|
55
|
-
type: 'text',
|
|
56
|
-
text: fullResponse || 'No results found.'
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
};
|
|
60
|
-
} else {
|
|
61
|
-
// For non-streaming responses
|
|
62
|
-
const response = await searchFelo(query, false);
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
content: [
|
|
66
|
-
{
|
|
67
|
-
type: 'text',
|
|
68
|
-
text: response || 'No results found.'
|
|
69
|
-
}
|
|
70
|
-
]
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
} catch (error) {
|
|
74
|
-
console.error(`Error in Felo search: ${error.message}`);
|
|
75
|
-
return {
|
|
76
|
-
isError: true,
|
|
77
|
-
content: [
|
|
78
|
-
{
|
|
79
|
-
type: 'text',
|
|
80
|
-
text: `Error searching Felo: ${error.message}`
|
|
81
|
-
}
|
|
82
|
-
]
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { fetchUrlContent } from '../utils/search.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Fetch URL tool definition
|
|
5
|
-
*/
|
|
6
|
-
export const fetchUrlToolDefinition = {
|
|
7
|
-
name: 'fetch-url',
|
|
8
|
-
title: 'Fetch URL Content',
|
|
9
|
-
description: 'Fetch and extract the main content from any URL, with customizable extraction options for text, links, and images',
|
|
10
|
-
inputSchema: {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
url: {
|
|
14
|
-
type: 'string',
|
|
15
|
-
description: 'The URL to fetch content from (must be a valid HTTP/HTTPS URL)'
|
|
16
|
-
},
|
|
17
|
-
maxLength: {
|
|
18
|
-
type: 'integer',
|
|
19
|
-
description: 'Maximum length of content to return in characters (default: 10000)',
|
|
20
|
-
default: 10000,
|
|
21
|
-
minimum: 1000,
|
|
22
|
-
maximum: 50000
|
|
23
|
-
},
|
|
24
|
-
extractMainContent: {
|
|
25
|
-
type: 'boolean',
|
|
26
|
-
description: 'Whether to attempt to extract main content only, filtering out navigation and ads (default: true)',
|
|
27
|
-
default: true
|
|
28
|
-
},
|
|
29
|
-
includeLinks: {
|
|
30
|
-
type: 'boolean',
|
|
31
|
-
description: 'Whether to include link text in the extracted content (default: true)',
|
|
32
|
-
default: true
|
|
33
|
-
},
|
|
34
|
-
includeImages: {
|
|
35
|
-
type: 'boolean',
|
|
36
|
-
description: 'Whether to include image alt text in the extracted content (default: true)',
|
|
37
|
-
default: true
|
|
38
|
-
},
|
|
39
|
-
excludeTags: {
|
|
40
|
-
type: 'array',
|
|
41
|
-
description: 'HTML tags to exclude from extraction (default: script, style, etc.)',
|
|
42
|
-
items: {
|
|
43
|
-
type: 'string'
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
required: ['url']
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Fetch URL tool handler
|
|
53
|
-
* @param {Object} params - The tool parameters
|
|
54
|
-
* @returns {Promise<Object>} - The tool result
|
|
55
|
-
*/
|
|
56
|
-
export async function fetchUrlToolHandler(params) {
|
|
57
|
-
const {
|
|
58
|
-
url,
|
|
59
|
-
maxLength = 10000,
|
|
60
|
-
extractMainContent = true,
|
|
61
|
-
includeLinks = true,
|
|
62
|
-
includeImages = true,
|
|
63
|
-
excludeTags = ['script', 'style', 'noscript', 'iframe', 'svg', 'nav', 'footer', 'header', 'aside']
|
|
64
|
-
} = params;
|
|
65
|
-
|
|
66
|
-
console.log(`Fetching content from URL: ${url} (maxLength: ${maxLength})`);
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
// Fetch content with specified options
|
|
70
|
-
const content = await fetchUrlContent(url, {
|
|
71
|
-
extractMainContent,
|
|
72
|
-
includeLinks,
|
|
73
|
-
includeImages,
|
|
74
|
-
excludeTags
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Truncate content if it's too long
|
|
78
|
-
const truncatedContent = content.length > maxLength
|
|
79
|
-
? content.substring(0, maxLength) + '... [Content truncated due to length]'
|
|
80
|
-
: content;
|
|
81
|
-
|
|
82
|
-
// Add metadata about the extraction
|
|
83
|
-
const metadata = `
|
|
84
|
-
---
|
|
85
|
-
Extraction settings:
|
|
86
|
-
- URL: ${url}
|
|
87
|
-
- Main content extraction: ${extractMainContent ? 'Enabled' : 'Disabled'}
|
|
88
|
-
- Links included: ${includeLinks ? 'Yes' : 'No'}
|
|
89
|
-
- Images included: ${includeImages ? 'Yes (as alt text)' : 'No'}
|
|
90
|
-
- Content length: ${content.length} characters${content.length > maxLength ? ` (truncated to ${maxLength})` : ''}
|
|
91
|
-
---
|
|
92
|
-
`;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
content: [
|
|
96
|
-
{
|
|
97
|
-
type: 'text',
|
|
98
|
-
text: truncatedContent + metadata
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
};
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error(`Error fetching URL ${url}:`, error);
|
|
104
|
-
return {
|
|
105
|
-
isError: true,
|
|
106
|
-
content: [
|
|
107
|
-
{
|
|
108
|
-
type: 'text',
|
|
109
|
-
text: `Error fetching URL: ${error.message}`
|
|
110
|
-
}
|
|
111
|
-
]
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { extractUrlMetadata } from '../utils/search.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* URL metadata tool definition
|
|
5
|
-
*/
|
|
6
|
-
export const metadataToolDefinition = {
|
|
7
|
-
name: 'url-metadata',
|
|
8
|
-
title: 'URL Metadata Extractor',
|
|
9
|
-
description: 'Extract metadata from a URL including title, description, Open Graph data, and favicon information',
|
|
10
|
-
inputSchema: {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {
|
|
13
|
-
url: {
|
|
14
|
-
type: 'string',
|
|
15
|
-
description: 'The URL to extract metadata from (must be a valid HTTP/HTTPS URL)'
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
required: ['url']
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* URL metadata tool handler
|
|
24
|
-
* @param {Object} params - The tool parameters
|
|
25
|
-
* @returns {Promise<Object>} - The tool result
|
|
26
|
-
*/
|
|
27
|
-
export async function metadataToolHandler(params) {
|
|
28
|
-
const { url } = params;
|
|
29
|
-
console.log(`Extracting metadata from URL: ${url}`);
|
|
30
|
-
|
|
31
|
-
const metadata = await extractUrlMetadata(url);
|
|
32
|
-
|
|
33
|
-
// Format the metadata for display
|
|
34
|
-
const formattedMetadata = `
|
|
35
|
-
## URL Metadata for ${url}
|
|
36
|
-
|
|
37
|
-
**Title:** ${metadata.title}
|
|
38
|
-
|
|
39
|
-
**Description:** ${metadata.description}
|
|
40
|
-
|
|
41
|
-
**Image:** ${metadata.ogImage || 'None'}
|
|
42
|
-
|
|
43
|
-
**Favicon:** ${metadata.favicon || 'None'}
|
|
44
|
-
`.trim();
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
content: [
|
|
48
|
-
{
|
|
49
|
-
type: 'text',
|
|
50
|
-
text: formattedMetadata
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
};
|
|
54
|
-
}
|
package/src/utils/search_felo.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
-
import https from 'https';
|
|
4
|
-
|
|
5
|
-
// Rotating User Agents
|
|
6
|
-
const USER_AGENTS = [
|
|
7
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
8
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/120.0.0.0',
|
|
9
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
|
|
10
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
|
|
11
|
-
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
// Cache results to avoid repeated requests
|
|
15
|
-
const resultsCache = new Map();
|
|
16
|
-
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
17
|
-
|
|
18
|
-
// HTTPS agent configuration to handle certificate chain issues
|
|
19
|
-
const httpsAgent = new https.Agent({
|
|
20
|
-
rejectUnauthorized: true, // Keep security enabled
|
|
21
|
-
keepAlive: true,
|
|
22
|
-
timeout: 30000,
|
|
23
|
-
// Provide fallback for certificate issues while maintaining security
|
|
24
|
-
secureProtocol: 'TLSv1_2_method'
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Create a persistent axios instance to maintain session state
|
|
28
|
-
const feloSession = axios.create({
|
|
29
|
-
timeout: 30000,
|
|
30
|
-
httpsAgent: httpsAgent,
|
|
31
|
-
headers: {
|
|
32
|
-
'accept': '*/*',
|
|
33
|
-
'accept-encoding': 'gzip, deflate, br, zstd',
|
|
34
|
-
'accept-language': 'en-US,en;q=0.9,en-IN;q=0.8',
|
|
35
|
-
'content-type': 'application/json',
|
|
36
|
-
'dnt': '1',
|
|
37
|
-
'origin': 'https://felo.ai',
|
|
38
|
-
'referer': 'https://felo.ai/',
|
|
39
|
-
'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
|
|
40
|
-
'sec-ch-ua-mobile': '?0',
|
|
41
|
-
'sec-ch-ua-platform': '"Windows"',
|
|
42
|
-
'sec-fetch-dest': 'empty',
|
|
43
|
-
'sec-fetch-mode': 'cors',
|
|
44
|
-
'sec-fetch-site': 'same-site',
|
|
45
|
-
'user-agent': getRandomUserAgent()
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Response class for Felo API responses
|
|
51
|
-
*/
|
|
52
|
-
class Response {
|
|
53
|
-
/**
|
|
54
|
-
* Create a new Response
|
|
55
|
-
* @param {string} text - The text content of the response
|
|
56
|
-
*/
|
|
57
|
-
constructor(text) {
|
|
58
|
-
this.text = text;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* String representation of the response
|
|
63
|
-
* @returns {string} The text content
|
|
64
|
-
*/
|
|
65
|
-
toString() {
|
|
66
|
-
return this.text;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Get a random user agent from the list
|
|
72
|
-
* @returns {string} A random user agent string
|
|
73
|
-
*/
|
|
74
|
-
function getRandomUserAgent() {
|
|
75
|
-
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Generate a cache key for a search query
|
|
80
|
-
* @param {string} query - The search query
|
|
81
|
-
* @returns {string} The cache key
|
|
82
|
-
*/
|
|
83
|
-
function getCacheKey(query) {
|
|
84
|
-
return `felo-${query}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Clear old entries from the cache
|
|
89
|
-
*/
|
|
90
|
-
function clearOldCache() {
|
|
91
|
-
const now = Date.now();
|
|
92
|
-
for (const [key, value] of resultsCache.entries()) {
|
|
93
|
-
if (now - value.timestamp > CACHE_DURATION) {
|
|
94
|
-
resultsCache.delete(key);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Search using the Felo AI API
|
|
101
|
-
* @param {string} prompt - The search query or prompt
|
|
102
|
-
* @param {boolean} stream - If true, yields response chunks as they arrive
|
|
103
|
-
* @param {boolean} raw - If true, returns raw response dictionaries
|
|
104
|
-
* @returns {Promise<string|AsyncGenerator<string>>} The search results
|
|
105
|
-
*/
|
|
106
|
-
async function searchFelo(prompt, stream = false, raw = false) {
|
|
107
|
-
// Clear old cache entries
|
|
108
|
-
clearOldCache();
|
|
109
|
-
|
|
110
|
-
// Check cache first if not streaming
|
|
111
|
-
if (!stream) {
|
|
112
|
-
const cacheKey = getCacheKey(prompt);
|
|
113
|
-
const cachedResults = resultsCache.get(cacheKey);
|
|
114
|
-
|
|
115
|
-
if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
|
|
116
|
-
return cachedResults.results;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Create payload for Felo API with proper structure from reference
|
|
121
|
-
const payload = {
|
|
122
|
-
query: prompt,
|
|
123
|
-
search_uuid: uuidv4().replace(/-/g, ''), // Remove dashes like in reference
|
|
124
|
-
lang: "",
|
|
125
|
-
agent_lang: "en",
|
|
126
|
-
search_options: {
|
|
127
|
-
langcode: "en-US",
|
|
128
|
-
search_image: true,
|
|
129
|
-
search_video: true
|
|
130
|
-
},
|
|
131
|
-
search_video: true,
|
|
132
|
-
model: "",
|
|
133
|
-
contexts_from: "google",
|
|
134
|
-
auto_routing: true
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// Update user agent for this request
|
|
138
|
-
feloSession.defaults.headers['user-agent'] = getRandomUserAgent();
|
|
139
|
-
|
|
140
|
-
// Define the streaming function
|
|
141
|
-
async function* streamFunction() {
|
|
142
|
-
try {
|
|
143
|
-
const response = await feloSession.post('https://api.felo.ai/search/threads', payload, {
|
|
144
|
-
responseType: 'stream'
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Check for HTTP errors
|
|
148
|
-
if (response.status !== 200) {
|
|
149
|
-
throw new Error(`Failed to generate response - (${response.status}, ${response.statusText}) - ${response.data}`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let streamingText = '';
|
|
153
|
-
let buffer = '';
|
|
154
|
-
|
|
155
|
-
// Process the stream as it comes in
|
|
156
|
-
for await (const chunk of response.data) {
|
|
157
|
-
buffer += chunk.toString();
|
|
158
|
-
|
|
159
|
-
const lines = buffer.split('\n');
|
|
160
|
-
buffer = lines.pop() || ''; // Keep the last (potentially incomplete) line in the buffer
|
|
161
|
-
|
|
162
|
-
for (const line of lines) {
|
|
163
|
-
if (line.startsWith('data:')) {
|
|
164
|
-
try {
|
|
165
|
-
const dataStr = line.substring(5).trim();
|
|
166
|
-
if (dataStr) {
|
|
167
|
-
const data = JSON.parse(dataStr);
|
|
168
|
-
if (data.type === 'answer' && 'text' in data.data) {
|
|
169
|
-
const newText = data.data.text;
|
|
170
|
-
if (newText.length > streamingText.length) {
|
|
171
|
-
const delta = newText.substring(streamingText.length);
|
|
172
|
-
streamingText = newText;
|
|
173
|
-
|
|
174
|
-
if (raw) {
|
|
175
|
-
yield { text: delta };
|
|
176
|
-
} else {
|
|
177
|
-
yield new Response(delta).toString();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
} catch (error) {
|
|
183
|
-
// Ignore JSON parse errors and continue
|
|
184
|
-
console.debug('JSON parse error:', error.message);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Cache the complete response
|
|
191
|
-
if (streamingText) {
|
|
192
|
-
resultsCache.set(getCacheKey(prompt), {
|
|
193
|
-
results: streamingText,
|
|
194
|
-
timestamp: Date.now()
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
} catch (error) {
|
|
199
|
-
console.error('Error searching Felo:', error.message);
|
|
200
|
-
|
|
201
|
-
// Handle specific API errors
|
|
202
|
-
if (error.response) {
|
|
203
|
-
const status = error.response.status;
|
|
204
|
-
const statusText = error.response.statusText;
|
|
205
|
-
const data = error.response.data;
|
|
206
|
-
throw new Error(`Felo API error: ${status} ${statusText} - ${data}`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
throw new Error(`Failed to search Felo: ${error.message}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// If streaming is requested, return the generator
|
|
214
|
-
if (stream) {
|
|
215
|
-
return streamFunction();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// For non-streaming, collect all chunks and return as a single string
|
|
219
|
-
let fullResponse = '';
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
for await (const chunk of streamFunction()) {
|
|
223
|
-
if (raw) {
|
|
224
|
-
fullResponse += chunk.text;
|
|
225
|
-
} else {
|
|
226
|
-
fullResponse += chunk;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return fullResponse;
|
|
231
|
-
} catch (error) {
|
|
232
|
-
console.error('Error in non-streaming Felo search:', error.message);
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export { searchFelo };
|