@moorchehai/mcp 1.2.2 → 1.3.0
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/package.json +4 -3
- package/src/server/config/api.js +57 -3
- package/src/server/index.js +8 -1
- package/src/server/tools/data-tools.js +36 -1
- package/src/server/tools/search-tools.js +273 -229
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moorchehai/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Moorcheh MCP Server with completable functionality for AI-powered search and answer operations",
|
|
5
5
|
"main": "src/server/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
"license": "Apache-2.0",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"axios": "^1.11.0",
|
|
41
|
+
"form-data": "^4.0.5",
|
|
42
|
+
"zod": "^3.22.4"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"node": ">=18.0.0"
|
package/src/server/config/api.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
2
|
+
import { readFileSync, createReadStream, statSync } from 'fs';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname, join } from 'path';
|
|
4
|
+
import { dirname, join, basename } from 'path';
|
|
5
|
+
import FormData from 'form-data';
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = dirname(__filename);
|
|
@@ -87,4 +88,57 @@ async function makeApiRequest(method, url, data = null) {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
// Helper function to upload files (multipart/form-data)
|
|
92
|
+
async function uploadFile(namespace_name, filePath) {
|
|
93
|
+
try {
|
|
94
|
+
// Check if file exists
|
|
95
|
+
const stats = statSync(filePath);
|
|
96
|
+
const fileSizeInMB = stats.size / (1024 * 1024);
|
|
97
|
+
|
|
98
|
+
// Check file size (max 10MB)
|
|
99
|
+
if (fileSizeInMB > 10) {
|
|
100
|
+
throw new Error(`File size (${fileSizeInMB.toFixed(2)}MB) exceeds maximum allowed size of 10MB`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check file extension
|
|
104
|
+
const allowedExtensions = ['.pdf', '.docx', '.xlsx', '.json', '.txt', '.csv', '.md'];
|
|
105
|
+
const fileExtension = filePath.toLowerCase().substring(filePath.lastIndexOf('.'));
|
|
106
|
+
if (!allowedExtensions.includes(fileExtension)) {
|
|
107
|
+
throw new Error(`File type '${fileExtension}' is not supported. Allowed types: ${allowedExtensions.join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create FormData
|
|
111
|
+
const formData = new FormData();
|
|
112
|
+
const fileName = basename(filePath);
|
|
113
|
+
formData.append('file', createReadStream(filePath), fileName);
|
|
114
|
+
|
|
115
|
+
// Make the request
|
|
116
|
+
const url = `${API_ENDPOINTS.namespaces}/${namespace_name}/upload-file`;
|
|
117
|
+
const response = await axios.post(url, formData, {
|
|
118
|
+
headers: {
|
|
119
|
+
'x-api-key': MOORCHEH_API_KEY,
|
|
120
|
+
...formData.getHeaders(),
|
|
121
|
+
},
|
|
122
|
+
maxContentLength: Infinity,
|
|
123
|
+
maxBodyLength: Infinity,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return response.data;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.response) {
|
|
129
|
+
const status = error.response.status;
|
|
130
|
+
const data = error.response.data;
|
|
131
|
+
|
|
132
|
+
if (status === 403) {
|
|
133
|
+
throw new Error(`Forbidden: Check your API key. Status: ${status}, Response: ${JSON.stringify(data)}`);
|
|
134
|
+
} else if (status === 401) {
|
|
135
|
+
throw new Error(`Unauthorized: Invalid API key. Status: ${status}, Response: ${JSON.stringify(data)}`);
|
|
136
|
+
} else {
|
|
137
|
+
throw new Error(`API Error (${status}): ${JSON.stringify(data)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`File upload error: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { API_ENDPOINTS, makeApiRequest, uploadFile, MOORCHEH_API_KEY };
|
package/src/server/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
5
5
|
|
|
6
6
|
// Import tools
|
|
7
7
|
import { listNamespacesTool, createNamespaceTool, deleteNamespaceTool } from './tools/namespace-tools.js';
|
|
8
|
-
import { uploadTextTool, uploadVectorsTool, deleteDataTool, getDataTool } from './tools/data-tools.js';
|
|
8
|
+
import { uploadTextTool, uploadVectorsTool, deleteDataTool, getDataTool, uploadFileTool } from './tools/data-tools.js';
|
|
9
9
|
import { searchTool, answerTool } from './tools/search-tools.js';
|
|
10
10
|
|
|
11
11
|
// Import resources
|
|
@@ -156,6 +156,13 @@ server.tool(
|
|
|
156
156
|
getDataTool.handler,
|
|
157
157
|
);
|
|
158
158
|
|
|
159
|
+
server.tool(
|
|
160
|
+
uploadFileTool.name,
|
|
161
|
+
uploadFileTool.description,
|
|
162
|
+
uploadFileTool.parameters,
|
|
163
|
+
uploadFileTool.handler,
|
|
164
|
+
);
|
|
165
|
+
|
|
159
166
|
// Register search tools
|
|
160
167
|
server.tool(
|
|
161
168
|
searchTool.name,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { makeApiRequest, API_ENDPOINTS } from '../config/api.js';
|
|
2
|
+
import { makeApiRequest, API_ENDPOINTS, uploadFile } from '../config/api.js';
|
|
3
3
|
|
|
4
4
|
// Upload text documents tool
|
|
5
5
|
export const uploadTextTool = {
|
|
@@ -155,4 +155,39 @@ export const getDataTool = {
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Upload file tool
|
|
161
|
+
export const uploadFileTool = {
|
|
162
|
+
name: "upload-file",
|
|
163
|
+
description: "Upload a file directly to a text-type namespace for processing and indexing. Files are queued for ingestion and will be available for search once processed. Supported file types: .pdf, .docx, .xlsx, .json, .txt, .csv, .md (max 10MB)",
|
|
164
|
+
parameters: {
|
|
165
|
+
namespace_name: z.string().describe("Name of the text namespace to upload the file to"),
|
|
166
|
+
file_path: z.string().describe("Path to the file to upload (max 10MB). Must be one of: .pdf, .docx, .xlsx, .json, .txt, .csv, .md"),
|
|
167
|
+
},
|
|
168
|
+
handler: async ({ namespace_name, file_path }) => {
|
|
169
|
+
try {
|
|
170
|
+
const data = await uploadFile(namespace_name, file_path);
|
|
171
|
+
|
|
172
|
+
const resultText = `Successfully uploaded file "${data.fileName}" to namespace "${namespace_name}":\n${JSON.stringify(data, null, 2)}`;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: resultText,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: `Error uploading file: ${error.message}`,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
},
|
|
158
193
|
};
|
|
@@ -1,230 +1,274 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { makeApiRequest, API_ENDPOINTS } from '../config/api.js';
|
|
3
|
-
|
|
4
|
-
// Search tool
|
|
5
|
-
export const searchTool = {
|
|
6
|
-
name: "search",
|
|
7
|
-
description: "Search for data in a namespace using semantic search or vector similarity. This tool provides powerful search capabilities across your namespaces, supporting both text-based semantic search and vector-based similarity search. For text search, you can use natural language queries to find relevant documents based on meaning rather than just keywords. For vector search, you can find similar content by comparing vector embeddings. The tool supports advanced features like result filtering, similarity thresholds, and kiosk mode for production environments. This is ideal for building intelligent search interfaces, recommendation systems, or content discovery features.",
|
|
8
|
-
parameters: {
|
|
9
|
-
namespaces: z.array(z.string().min(1)).min(1).describe("Namespaces to search in. Provide an array of namespace names where you want to search for content. You can search across multiple namespaces simultaneously. All namespaces must be accessible with your API key."),
|
|
10
|
-
query: z.union([z.string().min(1), z.array(z.number())]).describe("Search query. For text search: provide a natural language query string (e.g., 'tell me about the company?', 'how to configure authentication?'). For vector search: provide an array of numbers representing a vector embedding (e.g., [0.1, 0.2, 0.3, ..., 0.768]). The query type will be automatically detected based on the input format. DO NOT USE QUOTES IN THE QUERY FOR VECTOR SEARCH
|
|
11
|
-
query_type: z.enum(['text', 'vector']).optional().describe("Type of query to perform. 'text' for semantic search using natural language queries. 'vector' for similarity search using vector embeddings. If not specified, the type will be automatically detected based on the query format (string for text, array for vector)."),
|
|
12
|
-
top_k: z.number().int().positive().optional().describe("Number of top results to return. Controls how many search results are returned, with higher values providing more comprehensive results. Default is 10. Use lower values (3-5) for focused results, higher values (10-20) for broader exploration."),
|
|
13
|
-
threshold: z.number().min(0).max(1).optional().describe("Similarity threshold for results. A value between 0 and 1 that filters results based on similarity score. Higher values (0.7-0.9) return only highly similar results, lower values (0.3-0.5) return more comprehensive results. Required when kiosk_mode is true."),
|
|
14
|
-
kiosk_mode: z.boolean().optional().describe("Kiosk mode for restricted search. When true, search is restricted to specific namespaces with threshold filtering, providing more controlled results suitable for production environments. When false, search across all specified namespaces without strict filtering."),
|
|
15
|
-
},
|
|
16
|
-
handler: async ({ namespaces, query, query_type, top_k = 10, threshold, kiosk_mode = false }) => {
|
|
17
|
-
try {
|
|
18
|
-
// Determine query type if not explicitly provided
|
|
19
|
-
let finalQueryType = query_type;
|
|
20
|
-
let finalQuery = query;
|
|
21
|
-
|
|
22
|
-
if (!finalQueryType) {
|
|
23
|
-
if (typeof query === 'string') {
|
|
24
|
-
// Check if it's a string representation of a vector array
|
|
25
|
-
if (query.startsWith('[') && query.endsWith(']')) {
|
|
26
|
-
try {
|
|
27
|
-
const parsedArray = JSON.parse(query);
|
|
28
|
-
if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
|
|
29
|
-
finalQuery = parsedArray;
|
|
30
|
-
finalQueryType = 'vector';
|
|
31
|
-
} else {
|
|
32
|
-
finalQueryType = 'text';
|
|
33
|
-
}
|
|
34
|
-
} catch (e) {
|
|
35
|
-
finalQueryType = 'text';
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
finalQueryType = 'text';
|
|
39
|
-
}
|
|
40
|
-
} else if (Array.isArray(query) && query.every(item => typeof item === 'number')) {
|
|
41
|
-
finalQueryType = 'vector';
|
|
42
|
-
} else {
|
|
43
|
-
return {
|
|
44
|
-
content: [
|
|
45
|
-
{
|
|
46
|
-
type: "text",
|
|
47
|
-
text: 'Error: Unable to determine query type. Please specify query_type parameter or provide a valid string (for text) or number array (for vector).',
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Handle vector query type with string input
|
|
55
|
-
if (finalQueryType === 'vector' && typeof query === 'string') {
|
|
56
|
-
try {
|
|
57
|
-
const parsedArray = JSON.parse(query);
|
|
58
|
-
if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
|
|
59
|
-
finalQuery = parsedArray;
|
|
60
|
-
} else {
|
|
61
|
-
return {
|
|
62
|
-
content: [
|
|
63
|
-
{
|
|
64
|
-
type: "text",
|
|
65
|
-
text: 'Error: Vector query type requires an array of numbers',
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
} catch (e) {
|
|
71
|
-
return {
|
|
72
|
-
content: [
|
|
73
|
-
{
|
|
74
|
-
type: "text",
|
|
75
|
-
text: 'Error: Vector query type requires an array of numbers',
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Validate query format matches query type
|
|
83
|
-
if (finalQueryType === 'text' && typeof finalQuery !== 'string') {
|
|
84
|
-
return {
|
|
85
|
-
content: [
|
|
86
|
-
{
|
|
87
|
-
type: "text",
|
|
88
|
-
text: 'Error: Text query type requires a string query. Example: "your search text here"',
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (finalQueryType === 'vector' && (!Array.isArray(finalQuery) || !finalQuery.every(item => typeof item === 'number'))) {
|
|
94
|
-
return {
|
|
95
|
-
content: [
|
|
96
|
-
{
|
|
97
|
-
type: "text",
|
|
98
|
-
text: 'Error: Vector query type requires an array of numbers. Example: [0.1, 0.2, 0.3, 0.4, 0.5] for 5-dimensional namespace',
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const requestBody = {
|
|
105
|
-
namespaces,
|
|
106
|
-
query: finalQuery,
|
|
107
|
-
top_k,
|
|
108
|
-
kiosk_mode,
|
|
109
|
-
};
|
|
110
|
-
if (threshold !== undefined) {
|
|
111
|
-
requestBody.threshold = threshold;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const data = await makeApiRequest('POST', API_ENDPOINTS.search, requestBody);
|
|
115
|
-
|
|
116
|
-
if (!data.results || data.results.length === 0) {
|
|
117
|
-
return {
|
|
118
|
-
content: [
|
|
119
|
-
{
|
|
120
|
-
type: "text",
|
|
121
|
-
text: `No results found for query: "${query}"`,
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const formattedResults = data.results.map((result, index) =>
|
|
128
|
-
[
|
|
129
|
-
`Result ${index + 1}:`,
|
|
130
|
-
`
|
|
131
|
-
`
|
|
132
|
-
`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { makeApiRequest, API_ENDPOINTS } from '../config/api.js';
|
|
3
|
+
|
|
4
|
+
// Search tool
|
|
5
|
+
export const searchTool = {
|
|
6
|
+
name: "search",
|
|
7
|
+
description: "Search for data in a namespace using semantic search or vector similarity. This tool provides powerful search capabilities across your namespaces, supporting both text-based semantic search and vector-based similarity search. For text search, you can use natural language queries to find relevant documents based on meaning rather than just keywords. For vector search, you can find similar content by comparing vector embeddings. The tool supports advanced features like result filtering, similarity thresholds, metadata filters, keyword filters, and kiosk mode for production environments. This is ideal for building intelligent search interfaces, recommendation systems, or content discovery features.\n\nFiltering Capabilities:\n- Metadata Filters: Use #key:value format (e.g., #category:tech, #priority:high)\n- Keyword Filters: Use #keyword format (e.g., #important, #urgent)\n- Filters only apply to text search and metadata must be manually uploaded with documents",
|
|
8
|
+
parameters: {
|
|
9
|
+
namespaces: z.array(z.string().min(1)).min(1).describe("Namespaces to search in. Provide an array of namespace names where you want to search for content. You can search across multiple namespaces simultaneously. All namespaces must be accessible with your API key."),
|
|
10
|
+
query: z.union([z.string().min(1), z.array(z.number())]).describe("Search query. For text search: provide a natural language query string (e.g., 'tell me about the company?', 'how to configure authentication?'). For vector search: provide an array of numbers representing a vector embedding (e.g., [0.1, 0.2, 0.3, ..., 0.768]). The query type will be automatically detected based on the input format. DO NOT USE QUOTES IN THE QUERY FOR VECTOR SEARCH.\n\nFiltering: For text search, you can include filters in your query:\n- Metadata filters: #category:tech #priority:high\n- Keyword filters: #important #urgent\n- Combine both: 'serverless benefits #category:tech #important'"),
|
|
11
|
+
query_type: z.enum(['text', 'vector']).optional().describe("Type of query to perform. 'text' for semantic search using natural language queries. 'vector' for similarity search using vector embeddings. If not specified, the type will be automatically detected based on the query format (string for text, array for vector)."),
|
|
12
|
+
top_k: z.number().int().positive().optional().describe("Number of top results to return. Controls how many search results are returned, with higher values providing more comprehensive results. Default is 10. Use lower values (3-5) for focused results, higher values (10-20) for broader exploration."),
|
|
13
|
+
threshold: z.number().min(0).max(1).optional().describe("Similarity threshold for results. A value between 0 and 1 that filters results based on similarity score. Higher values (0.7-0.9) return only highly similar results, lower values (0.3-0.5) return more comprehensive results. Required when kiosk_mode is true."),
|
|
14
|
+
kiosk_mode: z.boolean().optional().describe("Kiosk mode for restricted search. When true, search is restricted to specific namespaces with threshold filtering, providing more controlled results suitable for production environments. When false, search across all specified namespaces without strict filtering."),
|
|
15
|
+
},
|
|
16
|
+
handler: async ({ namespaces, query, query_type, top_k = 10, threshold, kiosk_mode = false }) => {
|
|
17
|
+
try {
|
|
18
|
+
// Determine query type if not explicitly provided
|
|
19
|
+
let finalQueryType = query_type;
|
|
20
|
+
let finalQuery = query;
|
|
21
|
+
|
|
22
|
+
if (!finalQueryType) {
|
|
23
|
+
if (typeof query === 'string') {
|
|
24
|
+
// Check if it's a string representation of a vector array
|
|
25
|
+
if (query.startsWith('[') && query.endsWith(']')) {
|
|
26
|
+
try {
|
|
27
|
+
const parsedArray = JSON.parse(query);
|
|
28
|
+
if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
|
|
29
|
+
finalQuery = parsedArray;
|
|
30
|
+
finalQueryType = 'vector';
|
|
31
|
+
} else {
|
|
32
|
+
finalQueryType = 'text';
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
finalQueryType = 'text';
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
finalQueryType = 'text';
|
|
39
|
+
}
|
|
40
|
+
} else if (Array.isArray(query) && query.every(item => typeof item === 'number')) {
|
|
41
|
+
finalQueryType = 'vector';
|
|
42
|
+
} else {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: 'Error: Unable to determine query type. Please specify query_type parameter or provide a valid string (for text) or number array (for vector).',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle vector query type with string input
|
|
55
|
+
if (finalQueryType === 'vector' && typeof query === 'string') {
|
|
56
|
+
try {
|
|
57
|
+
const parsedArray = JSON.parse(query);
|
|
58
|
+
if (Array.isArray(parsedArray) && parsedArray.every(item => typeof item === 'number')) {
|
|
59
|
+
finalQuery = parsedArray;
|
|
60
|
+
} else {
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: 'Error: Vector query type requires an array of numbers',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: 'Error: Vector query type requires an array of numbers',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate query format matches query type
|
|
83
|
+
if (finalQueryType === 'text' && typeof finalQuery !== 'string') {
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: "text",
|
|
88
|
+
text: 'Error: Text query type requires a string query. Example: "your search text here"',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (finalQueryType === 'vector' && (!Array.isArray(finalQuery) || !finalQuery.every(item => typeof item === 'number'))) {
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: 'Error: Vector query type requires an array of numbers. Example: [0.1, 0.2, 0.3, 0.4, 0.5] for 5-dimensional namespace',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const requestBody = {
|
|
105
|
+
namespaces,
|
|
106
|
+
query: finalQuery,
|
|
107
|
+
top_k,
|
|
108
|
+
kiosk_mode,
|
|
109
|
+
};
|
|
110
|
+
if (threshold !== undefined) {
|
|
111
|
+
requestBody.threshold = threshold;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await makeApiRequest('POST', API_ENDPOINTS.search, requestBody);
|
|
115
|
+
|
|
116
|
+
if (!data.results || data.results.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `No results found for query: "${query}"`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const formattedResults = data.results.map((result, index) =>
|
|
128
|
+
[
|
|
129
|
+
`Result ${index + 1}:`,
|
|
130
|
+
`ID: ${result.id || 'N/A'}`,
|
|
131
|
+
`Text: ${result.text || result.content || 'N/A'}`,
|
|
132
|
+
`Score: ${result.score || 'N/A'}`,
|
|
133
|
+
`Label: ${result.label || 'N/A'}`,
|
|
134
|
+
`Metadata: ${JSON.stringify(result.metadata || {}, null, 2)}`,
|
|
135
|
+
"---",
|
|
136
|
+
].join("\n")
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
let searchText = `Search results for "${query}" in namespaces [${namespaces.join(', ')}]:\n\n${formattedResults.join("\n")}\n\nTotal results: ${data.total || data.results.length}`;
|
|
140
|
+
|
|
141
|
+
// Add execution time and performance metrics if available
|
|
142
|
+
if (data.execution_time) {
|
|
143
|
+
searchText += `\n\nExecution time: ${data.execution_time}s`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (data.timings) {
|
|
147
|
+
searchText += `\n\nPerformance breakdown:\n${Object.entries(data.timings)
|
|
148
|
+
.filter(([key]) => key !== 'total')
|
|
149
|
+
.map(([key, value]) => ` ${key}: ${value}s`)
|
|
150
|
+
.join('\n')}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (data.optimization_info) {
|
|
154
|
+
searchText += `\n\nOptimization: ${data.optimization_info.fetch_strategy}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: searchText,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: `Error searching: ${error.message}`,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Answer tool
|
|
179
|
+
export const answerTool = {
|
|
180
|
+
name: "answer",
|
|
181
|
+
description: "Get AI-generated answers based on data in a namespace using text queries. This tool provides intelligent, context-aware responses by searching through your stored text documents and generating comprehensive answers using advanced language models. Supports two modes: Search Mode (with namespace) and Direct AI Mode (empty namespace).",
|
|
182
|
+
parameters: {
|
|
183
|
+
namespace: z.string().describe("Namespace to answer questions from. For Search Mode: provide a text namespace containing documents to search for context. For Direct AI Mode: provide empty string \"\" to make direct AI model calls without searching your data."),
|
|
184
|
+
query: z.string().min(1).describe("Text query for AI answer generation. Provide a natural language question or prompt that you want the AI to answer. The AI will search through your namespace content and generate a comprehensive response based on the relevant information found."),
|
|
185
|
+
top_k: z.number().int().positive().optional().describe("Number of top results to return. Controls how many relevant documents the AI considers when generating an answer. Default is 5. Use lower values (3-5) for focused answers, higher values (8-10) for comprehensive responses that consider more context."),
|
|
186
|
+
threshold: z.number().min(0).max(1).optional().describe("Similarity threshold for results. A value between 0 and 1 that filters documents based on relevance before generating the answer. Higher values (0.7-0.9) ensure only highly relevant content is used, lower values (0.3-0.5) include more context. Required when kiosk_mode is true."),
|
|
187
|
+
kiosk_mode: z.boolean().optional().describe("Kiosk mode for restricted search. When true, search is restricted to specific namespaces with threshold filtering, providing more controlled and focused answers suitable for production environments."),
|
|
188
|
+
aiModel: z.string().optional().describe("AI model to use for answer generation. Different models may have different capabilities, response styles, and performance characteristics. Supported AI models include: 'anthropic.claude-3-7-sonnet-20250219-v1:0' (Claude 3.7 Sonnet), 'anthropic.claude-sonnet-4-20250514-v1:0' (Claude Sonnet 4), 'meta.llama4-maverick-17b-instruct-v1:0' (Llama 4 Maverick), 'meta.llama3-3-70b-instruct-v1:0' (Llama 3.3 70B), 'deepseek.r1-v1:0' (DeepSeek R1). If not specified, defaults to Claude 3.7 Sonnet."),
|
|
189
|
+
chatHistory: z.array(z.object({
|
|
190
|
+
role: z.string().describe("Role of the message in the conversation. Use 'user' for user messages and 'assistant' for AI responses. This helps maintain conversation context and allows the AI to reference previous exchanges."),
|
|
191
|
+
content: z.string()
|
|
192
|
+
})).optional().describe("Chat history for AI answer generation. Provide previous conversation context to help the AI maintain continuity and reference earlier parts of the conversation. This enables more coherent multi-turn conversations."),
|
|
193
|
+
headerPrompt: z.string().optional().describe("Header prompt for AI answer generation. Custom instructions that define the AI's role, style, and behavior. Use this to create specialized assistants (e.g., technical support, friendly helper, formal advisor) or set specific guidelines for response generation."),
|
|
194
|
+
footerPrompt: z.string().optional().describe("Footer prompt for AI answer generation. Additional instructions that are applied after the main response generation. Useful for formatting requirements, citation styles, or specific response patterns that should be consistently applied."),
|
|
195
|
+
temperature: z.number().min(0).max(2.0).optional().describe("Temperature for AI answer generation. Controls the creativity and randomness of responses. Lower values (0.1-0.3) produce more focused, deterministic answers. Higher values (0.7-1.0) produce more creative, varied responses. Default is 0.7."),
|
|
196
|
+
},
|
|
197
|
+
handler: async ({ namespace, query, top_k = 5, threshold, kiosk_mode = false, aiModel, chatHistory = [], headerPrompt, footerPrompt, temperature = 0.7 }) => {
|
|
198
|
+
try {
|
|
199
|
+
// Determine if this is Direct AI Mode (empty namespace) or Search Mode (with namespace)
|
|
200
|
+
const isDirectAIMode = namespace === "";
|
|
201
|
+
|
|
202
|
+
const requestBody = {
|
|
203
|
+
namespace,
|
|
204
|
+
query,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (isDirectAIMode) {
|
|
208
|
+
// Direct AI Mode: Only allow basic AI fields
|
|
209
|
+
if (aiModel) {
|
|
210
|
+
requestBody.aiModel = aiModel;
|
|
211
|
+
}
|
|
212
|
+
if (chatHistory && chatHistory.length > 0) {
|
|
213
|
+
requestBody.chatHistory = chatHistory;
|
|
214
|
+
}
|
|
215
|
+
if (headerPrompt) {
|
|
216
|
+
requestBody.headerPrompt = headerPrompt;
|
|
217
|
+
}
|
|
218
|
+
if (footerPrompt) {
|
|
219
|
+
requestBody.footerPrompt = footerPrompt;
|
|
220
|
+
}
|
|
221
|
+
if (temperature !== undefined) {
|
|
222
|
+
requestBody.temperature = temperature;
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// Search Mode: Allow all fields including search parameters
|
|
226
|
+
requestBody.top_k = top_k;
|
|
227
|
+
requestBody.kiosk_mode = kiosk_mode;
|
|
228
|
+
|
|
229
|
+
if (threshold !== undefined) {
|
|
230
|
+
requestBody.threshold = threshold;
|
|
231
|
+
}
|
|
232
|
+
if (aiModel) {
|
|
233
|
+
requestBody.aiModel = aiModel;
|
|
234
|
+
}
|
|
235
|
+
if (chatHistory && chatHistory.length > 0) {
|
|
236
|
+
requestBody.chatHistory = chatHistory;
|
|
237
|
+
}
|
|
238
|
+
if (headerPrompt) {
|
|
239
|
+
requestBody.headerPrompt = headerPrompt;
|
|
240
|
+
}
|
|
241
|
+
if (footerPrompt) {
|
|
242
|
+
requestBody.footerPrompt = footerPrompt;
|
|
243
|
+
}
|
|
244
|
+
if (temperature !== undefined) {
|
|
245
|
+
requestBody.temperature = temperature;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const data = await makeApiRequest('POST', API_ENDPOINTS.answer, requestBody);
|
|
250
|
+
|
|
251
|
+
const mode = isDirectAIMode ? "Direct AI Mode" : "Search Mode";
|
|
252
|
+
const namespaceInfo = isDirectAIMode ? "no namespace (direct AI call)" : `namespace "${namespace}"`;
|
|
253
|
+
const resultText = `AI Answer (${mode}) for "${query}" using ${namespaceInfo}:\n\n${data.answer || data.response || JSON.stringify(data, null, 2)}`;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: resultText,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: `Error getting AI answer: ${error.message}`,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
},
|
|
230
274
|
};
|