@yamo/memory-mesh 2.2.0 → 2.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/README.md +1 -7
- package/bin/setup.js +1 -3
- package/lib/index.js +0 -12
- package/lib/memory/index.js +0 -2
- package/lib/search/index.js +1 -3
- package/lib/utils/index.js +1 -4
- package/package.json +1 -2
- package/skills/SKILL.md +550 -1
- package/bin/scrubber.js +0 -81
- package/lib/adapters/index.js +0 -3
- package/lib/memory/memory-context-manager.js +0 -388
- package/lib/memory/memory-translator.js +0 -130
- package/lib/memory/migrate-memory.js +0 -227
- package/lib/memory/migrate-to-v2.js +0 -120
- package/lib/memory/scorer.js +0 -85
- package/lib/memory/vector-memory.js +0 -364
- package/lib/privacy/audit-logger.js +0 -176
- package/lib/privacy/dlp-redactor.js +0 -72
- package/lib/privacy/index.js +0 -10
- package/lib/reporting/skill-report-generator.js +0 -283
- package/lib/search/filter.js +0 -275
- package/lib/search/hybrid.js +0 -137
- package/lib/search/pattern-miner.js +0 -160
- package/lib/utils/error-sanitizer.js +0 -84
- package/lib/utils/handoff-validator.js +0 -85
- package/lib/utils/spinner.js +0 -190
- package/lib/utils/streaming-client.js +0 -128
- package/skills/skill-scrubber.yamo +0 -41
- package/skills/skill-super.yamo +0 -548
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Skill Execution Report Generator
|
|
6
|
-
*
|
|
7
|
-
* Generates JSON reports for skill executions, capturing:
|
|
8
|
-
* - Skill metadata (name, version, type)
|
|
9
|
-
* - Execution details (duration, status, provider)
|
|
10
|
-
* - Input/output metrics
|
|
11
|
-
* - Quality indicators
|
|
12
|
-
*/
|
|
13
|
-
export class SkillReportGenerator {
|
|
14
|
-
constructor(options = {}) {
|
|
15
|
-
this.reportsDir = options.reportsDir || this._getReportsDir();
|
|
16
|
-
this.version = '1.0.0';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
_getReportsDir() {
|
|
20
|
-
// @ts-ignore
|
|
21
|
-
const home = process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
22
|
-
return path.join(home, '.yamo', 'reports');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Generate a unique report ID
|
|
27
|
-
* @param {string} sessionId - Session identifier
|
|
28
|
-
* @returns {string} Report ID
|
|
29
|
-
*/
|
|
30
|
-
_generateReportId(sessionId) {
|
|
31
|
-
const timestamp = Date.now();
|
|
32
|
-
const shortSession = sessionId ? sessionId.substring(0, 8) : 'unknown';
|
|
33
|
-
return `skill_execution_${timestamp}_${shortSession}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Extract skill type from file path or name
|
|
38
|
-
* @param {string} skillName - Name of the skill
|
|
39
|
-
* @param {string[]} contextFiles - Context files used
|
|
40
|
-
* @returns {string} Skill type
|
|
41
|
-
*/
|
|
42
|
-
_getSkillType(skillName, contextFiles = []) {
|
|
43
|
-
if (skillName === 'LLMClient') return 'direct';
|
|
44
|
-
|
|
45
|
-
const skillFile = contextFiles.find(f => f.endsWith('.yamo'));
|
|
46
|
-
if (!skillFile) return 'unknown';
|
|
47
|
-
|
|
48
|
-
if (skillFile.includes('utility/')) return 'utility';
|
|
49
|
-
if (skillFile.includes('generator/')) return 'generator';
|
|
50
|
-
if (skillFile.includes('protocol/')) return 'protocol';
|
|
51
|
-
if (skillFile.includes('system-skills/')) return 'system';
|
|
52
|
-
|
|
53
|
-
return 'custom';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Parse skill metadata from .yamo file path
|
|
58
|
-
* @param {string[]} contextFiles - Context files
|
|
59
|
-
* @returns {Object} Skill metadata
|
|
60
|
-
*/
|
|
61
|
-
_parseSkillMetadata(contextFiles = []) {
|
|
62
|
-
const skillFile = contextFiles.find(f => f.endsWith('.yamo'));
|
|
63
|
-
if (!skillFile) {
|
|
64
|
-
return { version: null, description: null };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Return basic info - full parsing would require reading the file
|
|
68
|
-
return {
|
|
69
|
-
version: '1.0.0', // Default version
|
|
70
|
-
file: skillFile
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Create a report object from execution data
|
|
76
|
-
* @param {Object} executionData - Data from skill execution
|
|
77
|
-
* @returns {Object} Report object
|
|
78
|
-
*/
|
|
79
|
-
createReport(executionData) {
|
|
80
|
-
const {
|
|
81
|
-
skill,
|
|
82
|
-
sessionId,
|
|
83
|
-
duration,
|
|
84
|
-
provider,
|
|
85
|
-
model,
|
|
86
|
-
promptLength,
|
|
87
|
-
responseLength,
|
|
88
|
-
contextFiles = [],
|
|
89
|
-
parameters = {},
|
|
90
|
-
status = 'success',
|
|
91
|
-
error = null,
|
|
92
|
-
artifactsCreated = [],
|
|
93
|
-
memoryCaptured = false
|
|
94
|
-
} = executionData;
|
|
95
|
-
|
|
96
|
-
const reportId = this._generateReportId(sessionId);
|
|
97
|
-
const skillMeta = this._parseSkillMetadata(contextFiles);
|
|
98
|
-
const skillType = this._getSkillType(skill, contextFiles);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
report_id: reportId,
|
|
102
|
-
timestamp: new Date().toISOString(),
|
|
103
|
-
skill: {
|
|
104
|
-
name: skill,
|
|
105
|
-
version: skillMeta.version,
|
|
106
|
-
type: skillType,
|
|
107
|
-
file: skillMeta.file || null
|
|
108
|
-
},
|
|
109
|
-
execution: {
|
|
110
|
-
session_id: sessionId,
|
|
111
|
-
duration_ms: Math.round(duration),
|
|
112
|
-
status,
|
|
113
|
-
error: error ? String(error) : null,
|
|
114
|
-
provider,
|
|
115
|
-
model
|
|
116
|
-
},
|
|
117
|
-
input: {
|
|
118
|
-
prompt_length: promptLength,
|
|
119
|
-
context_files: contextFiles,
|
|
120
|
-
parameters
|
|
121
|
-
},
|
|
122
|
-
output: {
|
|
123
|
-
response_length: responseLength,
|
|
124
|
-
artifacts_created: artifactsCreated,
|
|
125
|
-
tokens_used: null // Could be populated if provider returns token count
|
|
126
|
-
},
|
|
127
|
-
quality: {
|
|
128
|
-
memory_captured: memoryCaptured,
|
|
129
|
-
artifacts_saved: artifactsCreated.length > 0
|
|
130
|
-
},
|
|
131
|
-
meta: {
|
|
132
|
-
generator: 'yamo-skills',
|
|
133
|
-
version: this.version
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Generate filename for a report
|
|
140
|
-
* @param {Object} report - Report object
|
|
141
|
-
* @returns {string} Filename
|
|
142
|
-
*/
|
|
143
|
-
getReportFilename(report) {
|
|
144
|
-
// Format: skill-{name}_{timestamp}_{ms}.json
|
|
145
|
-
// Include milliseconds for uniqueness when multiple reports per second
|
|
146
|
-
const safeName = report.skill.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
147
|
-
const timestamp = report.timestamp.replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
148
|
-
const ms = report.timestamp.slice(20, 23) || '000';
|
|
149
|
-
return `skill-${safeName}_${timestamp}-${ms}.json`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Ensure reports directory exists
|
|
154
|
-
*/
|
|
155
|
-
async _ensureReportsDir() {
|
|
156
|
-
try {
|
|
157
|
-
await fs.mkdir(this.reportsDir, { recursive: true });
|
|
158
|
-
} catch (error) {
|
|
159
|
-
const e = error instanceof Error ? error : new Error(String(error));
|
|
160
|
-
// @ts-ignore
|
|
161
|
-
if (e.code !== 'EEXIST') {
|
|
162
|
-
throw e;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Save a report to disk
|
|
169
|
-
* @param {Object} report - Report object to save
|
|
170
|
-
* @returns {Promise<string>} Path to saved report
|
|
171
|
-
*/
|
|
172
|
-
async saveReport(report) {
|
|
173
|
-
await this._ensureReportsDir();
|
|
174
|
-
|
|
175
|
-
const filename = this.getReportFilename(report);
|
|
176
|
-
const filepath = path.join(this.reportsDir, filename);
|
|
177
|
-
|
|
178
|
-
await fs.writeFile(filepath, JSON.stringify(report, null, 2), 'utf8');
|
|
179
|
-
|
|
180
|
-
return filepath;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Generate and save a report in one call
|
|
185
|
-
* @param {Object} executionData - Data from skill execution
|
|
186
|
-
* @returns {Promise<Object>} Report object with filepath
|
|
187
|
-
*/
|
|
188
|
-
async generateAndSave(executionData) {
|
|
189
|
-
const report = this.createReport(executionData);
|
|
190
|
-
const filepath = await this.saveReport(report);
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
...report,
|
|
194
|
-
_filepath: filepath
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* List recent reports
|
|
200
|
-
* @param {number} limit - Maximum number of reports to return
|
|
201
|
-
* @returns {Promise<string[]>} Array of report filenames
|
|
202
|
-
*/
|
|
203
|
-
async listReports(limit = 10) {
|
|
204
|
-
try {
|
|
205
|
-
await this._ensureReportsDir();
|
|
206
|
-
const files = await fs.readdir(this.reportsDir);
|
|
207
|
-
|
|
208
|
-
// Filter JSON files and sort by name (descending = newest first)
|
|
209
|
-
const reports = files
|
|
210
|
-
.filter(f => f.endsWith('.json'))
|
|
211
|
-
.sort((a, b) => b.localeCompare(a))
|
|
212
|
-
.slice(0, limit);
|
|
213
|
-
|
|
214
|
-
return reports;
|
|
215
|
-
} catch (error) {
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Read a specific report
|
|
222
|
-
* @param {string} filename - Report filename
|
|
223
|
-
* @returns {Promise<Object|null>} Report object or null
|
|
224
|
-
*/
|
|
225
|
-
async readReport(filename) {
|
|
226
|
-
try {
|
|
227
|
-
const filepath = path.join(this.reportsDir, filename);
|
|
228
|
-
const content = await fs.readFile(filepath, 'utf8');
|
|
229
|
-
return JSON.parse(content);
|
|
230
|
-
} catch (error) {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Get aggregate statistics from recent reports
|
|
237
|
-
* @param {number} limit - Number of reports to analyze
|
|
238
|
-
* @returns {Promise<Object>} Statistics object
|
|
239
|
-
*/
|
|
240
|
-
async getStats(limit = 100) {
|
|
241
|
-
const reportFiles = await this.listReports(limit);
|
|
242
|
-
const stats = {
|
|
243
|
-
total_reports: reportFiles.length,
|
|
244
|
-
skills_used: {},
|
|
245
|
-
providers_used: {},
|
|
246
|
-
success_count: 0,
|
|
247
|
-
error_count: 0,
|
|
248
|
-
total_duration_ms: 0,
|
|
249
|
-
avg_duration_ms: 0
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
for (const filename of reportFiles) {
|
|
253
|
-
const report = await this.readReport(filename);
|
|
254
|
-
if (!report) continue;
|
|
255
|
-
|
|
256
|
-
// Count skills
|
|
257
|
-
const skillName = report.skill?.name || 'unknown';
|
|
258
|
-
stats.skills_used[skillName] = (stats.skills_used[skillName] || 0) + 1;
|
|
259
|
-
|
|
260
|
-
// Count providers
|
|
261
|
-
const provider = report.execution?.provider || 'unknown';
|
|
262
|
-
stats.providers_used[provider] = (stats.providers_used[provider] || 0) + 1;
|
|
263
|
-
|
|
264
|
-
// Count success/error
|
|
265
|
-
if (report.execution?.status === 'success') {
|
|
266
|
-
stats.success_count++;
|
|
267
|
-
} else {
|
|
268
|
-
stats.error_count++;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Sum duration
|
|
272
|
-
stats.total_duration_ms += report.execution?.duration_ms || 0;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (stats.total_reports > 0) {
|
|
276
|
-
stats.avg_duration_ms = Math.round(stats.total_duration_ms / stats.total_reports);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return stats;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export default SkillReportGenerator;
|
package/lib/search/filter.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FilterBuilder - Fluent API for building LanceDB filter expressions
|
|
3
|
-
* Provides type-safe filter construction for metadata queries
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class FilterBuilder {
|
|
7
|
-
/**
|
|
8
|
-
* Create a new FilterBuilder
|
|
9
|
-
*/
|
|
10
|
-
constructor() {
|
|
11
|
-
this.filters = [];
|
|
12
|
-
this.operator = 'AND';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Add equality filter
|
|
17
|
-
* @param {string} field - Field name
|
|
18
|
-
* @param {*} value - Value to compare
|
|
19
|
-
* @returns {FilterBuilder} this for chaining
|
|
20
|
-
*/
|
|
21
|
-
equals(field, value) {
|
|
22
|
-
this.filters.push(`${field} = ${this._quote(value)}`);
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Add inequality filter
|
|
28
|
-
* @param {string} field - Field name
|
|
29
|
-
* @param {*} value - Value to compare
|
|
30
|
-
* @returns {FilterBuilder} this for chaining
|
|
31
|
-
*/
|
|
32
|
-
notEquals(field, value) {
|
|
33
|
-
this.filters.push(`${field} != ${this._quote(value)}`);
|
|
34
|
-
return this;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Add greater than filter
|
|
39
|
-
* @param {string} field - Field name
|
|
40
|
-
* @param {number} value - Value to compare
|
|
41
|
-
* @returns {FilterBuilder} this for chaining
|
|
42
|
-
*/
|
|
43
|
-
gt(field, value) {
|
|
44
|
-
this.filters.push(`${field} > ${value}`);
|
|
45
|
-
return this;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Add greater than or equal filter
|
|
50
|
-
* @param {string} field - Field name
|
|
51
|
-
* @param {number} value - Value to compare
|
|
52
|
-
* @returns {FilterBuilder} this for chaining
|
|
53
|
-
*/
|
|
54
|
-
gte(field, value) {
|
|
55
|
-
this.filters.push(`${field} >= ${value}`);
|
|
56
|
-
return this;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Add less than filter
|
|
61
|
-
* @param {string} field - Field name
|
|
62
|
-
* @param {number} value - Value to compare
|
|
63
|
-
* @returns {FilterBuilder} this for chaining
|
|
64
|
-
*/
|
|
65
|
-
lt(field, value) {
|
|
66
|
-
this.filters.push(`${field} < ${value}`);
|
|
67
|
-
return this;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Add less than or equal filter
|
|
72
|
-
* @param {string} field - Field name
|
|
73
|
-
* @param {number} value - Value to compare
|
|
74
|
-
* @returns {FilterBuilder} this for chaining
|
|
75
|
-
*/
|
|
76
|
-
lte(field, value) {
|
|
77
|
-
this.filters.push(`${field} <= ${value}`);
|
|
78
|
-
return this;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Add contains filter (LIKE)
|
|
83
|
-
* @param {string} field - Field name
|
|
84
|
-
* @param {string} value - Value to search for
|
|
85
|
-
* @returns {FilterBuilder} this for chaining
|
|
86
|
-
*/
|
|
87
|
-
contains(field, value) {
|
|
88
|
-
this.filters.push(`${field} LIKE '%${this._escapeLike(value)}%'`);
|
|
89
|
-
return this;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Add starts with filter
|
|
94
|
-
* @param {string} field - Field name
|
|
95
|
-
* @param {string} value - Value to match
|
|
96
|
-
* @returns {FilterBuilder} this for chaining
|
|
97
|
-
*/
|
|
98
|
-
startsWith(field, value) {
|
|
99
|
-
this.filters.push(`${field} LIKE '${this._escapeLike(value)}%'`);
|
|
100
|
-
return this;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Add ends with filter
|
|
105
|
-
* @param {string} field - Field name
|
|
106
|
-
* @param {string} value - Value to match
|
|
107
|
-
* @returns {FilterBuilder} this for chaining
|
|
108
|
-
*/
|
|
109
|
-
endsWith(field, value) {
|
|
110
|
-
this.filters.push(`${field} LIKE '%${this._escapeLike(value)}'`);
|
|
111
|
-
return this;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Add IN array filter
|
|
116
|
-
* @param {string} field - Field name
|
|
117
|
-
* @param {Array} values - Array of values
|
|
118
|
-
* @returns {FilterBuilder} this for chaining
|
|
119
|
-
*/
|
|
120
|
-
in(field, values) {
|
|
121
|
-
if (!Array.isArray(values) || values.length === 0) {
|
|
122
|
-
throw new Error('IN filter requires non-empty array');
|
|
123
|
-
}
|
|
124
|
-
const quoted = values.map(v => this._quote(v)).join(', ');
|
|
125
|
-
this.filters.push(`${field} IN [${quoted}]`);
|
|
126
|
-
return this;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Add NOT IN array filter
|
|
131
|
-
* @param {string} field - Field name
|
|
132
|
-
* @param {Array} values - Array of values
|
|
133
|
-
* @returns {FilterBuilder} this for chaining
|
|
134
|
-
*/
|
|
135
|
-
notIn(field, values) {
|
|
136
|
-
if (!Array.isArray(values) || values.length === 0) {
|
|
137
|
-
throw new Error('NOT IN filter requires non-empty array');
|
|
138
|
-
}
|
|
139
|
-
const quoted = values.map(v => this._quote(v)).join(', ');
|
|
140
|
-
this.filters.push(`${field} NOT IN [${quoted}]`);
|
|
141
|
-
return this;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Add range filter (inclusive)
|
|
146
|
-
* @param {string} field - Field name
|
|
147
|
-
* @param {number} min - Minimum value
|
|
148
|
-
* @param {number} max - Maximum value
|
|
149
|
-
* @returns {FilterBuilder} this for chaining
|
|
150
|
-
*/
|
|
151
|
-
range(field, min, max) {
|
|
152
|
-
this.filters.push(`${field} >= ${min} AND ${field} <= ${max}`);
|
|
153
|
-
return this;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Add date range filter
|
|
158
|
-
* @param {string} field - Field name
|
|
159
|
-
* @param {string|Date} startDate - Start date
|
|
160
|
-
* @param {string|Date} endDate - End date
|
|
161
|
-
* @returns {FilterBuilder} this for chaining
|
|
162
|
-
*/
|
|
163
|
-
dateRange(field, startDate, endDate) {
|
|
164
|
-
const start = startDate instanceof Date ? startDate.toISOString() : startDate;
|
|
165
|
-
const end = endDate instanceof Date ? endDate.toISOString() : endDate;
|
|
166
|
-
this.filters.push(`${field} >= '${start}' AND ${field} <= '${end}'`);
|
|
167
|
-
return this;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Add nested metadata filter
|
|
172
|
-
* @param {string} field - Metadata field name
|
|
173
|
-
* @param {*} value - Value to compare
|
|
174
|
-
* @returns {FilterBuilder} this for chaining
|
|
175
|
-
*/
|
|
176
|
-
metadata(field, value) {
|
|
177
|
-
this.filters.push(`metadata.${field} = ${this._quote(value)}`);
|
|
178
|
-
return this;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Combine filters with AND
|
|
183
|
-
* @returns {FilterBuilder} this for chaining
|
|
184
|
-
*/
|
|
185
|
-
and() {
|
|
186
|
-
this.operator = 'AND';
|
|
187
|
-
return this;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Combine filters with OR
|
|
192
|
-
* @returns {FilterBuilder} this for chaining
|
|
193
|
-
*/
|
|
194
|
-
or() {
|
|
195
|
-
this.operator = 'OR';
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Build and return the filter string
|
|
201
|
-
* @returns {string} Filter expression
|
|
202
|
-
*/
|
|
203
|
-
build() {
|
|
204
|
-
if (this.filters.length === 0) {
|
|
205
|
-
return '';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return this.filters.join(` ${this.operator} `);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Check if builder has any filters
|
|
213
|
-
* @returns {boolean} True if filters exist
|
|
214
|
-
*/
|
|
215
|
-
hasFilters() {
|
|
216
|
-
return this.filters.length > 0;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Get filter count
|
|
221
|
-
* @returns {number} Number of filters
|
|
222
|
-
*/
|
|
223
|
-
count() {
|
|
224
|
-
return this.filters.length;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Reset all filters
|
|
229
|
-
* @returns {FilterBuilder} this for chaining
|
|
230
|
-
*/
|
|
231
|
-
reset() {
|
|
232
|
-
this.filters = [];
|
|
233
|
-
this.operator = 'AND';
|
|
234
|
-
return this;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Quote string values for SQL
|
|
239
|
-
* @private
|
|
240
|
-
* @param {*} value - Value to quote
|
|
241
|
-
* @returns {string} Quoted value
|
|
242
|
-
*/
|
|
243
|
-
_quote(value) {
|
|
244
|
-
if (typeof value === 'string') {
|
|
245
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
246
|
-
}
|
|
247
|
-
if (value === null) {
|
|
248
|
-
return 'NULL';
|
|
249
|
-
}
|
|
250
|
-
if (typeof value === 'boolean') {
|
|
251
|
-
return value ? 'true' : 'false';
|
|
252
|
-
}
|
|
253
|
-
return String(value);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Escape special LIKE characters
|
|
258
|
-
* @private
|
|
259
|
-
* @param {string} value - Value to escape
|
|
260
|
-
* @returns {string} Escaped value
|
|
261
|
-
*/
|
|
262
|
-
_escapeLike(value) {
|
|
263
|
-
return value.replace(/'/g, "''").replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Create a new builder instance
|
|
268
|
-
* @returns {FilterBuilder} New builder
|
|
269
|
-
*/
|
|
270
|
-
static create() {
|
|
271
|
-
return new FilterBuilder();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export default FilterBuilder;
|
package/lib/search/hybrid.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HybridSearch - Combines vector and keyword search
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { QueryError } from "../lancedb/errors.js";
|
|
6
|
-
|
|
7
|
-
class HybridSearch {
|
|
8
|
-
constructor(client, embeddingFactory, options = {}) {
|
|
9
|
-
this.client = client;
|
|
10
|
-
this.embeddingFactory = embeddingFactory;
|
|
11
|
-
this.alpha = options.alpha !== undefined
|
|
12
|
-
? options.alpha
|
|
13
|
-
: parseFloat(process.env.HYBRID_SEARCH_ALPHA || '0.5');
|
|
14
|
-
this.rrfK = options.rrfK || 60;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async search(query, options = {}) {
|
|
18
|
-
// @ts-ignore
|
|
19
|
-
const limit = options.limit || 10;
|
|
20
|
-
// @ts-ignore
|
|
21
|
-
const alpha = options.alpha !== undefined ? options.alpha : this.alpha;
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const [vectorResults, keywordResults] = await Promise.all([
|
|
25
|
-
// @ts-ignore
|
|
26
|
-
this._vectorSearch(query, limit * 2, options.filter),
|
|
27
|
-
// @ts-ignore
|
|
28
|
-
this._keywordSearch(query, limit * 2, options.filter)
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
const mergedResults = this._reciprocalRankFusion(
|
|
32
|
-
vectorResults,
|
|
33
|
-
keywordResults,
|
|
34
|
-
alpha,
|
|
35
|
-
this.rrfK
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
return mergedResults.slice(0, limit);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
-
throw new QueryError('Hybrid search failed', {
|
|
42
|
-
query,
|
|
43
|
-
alpha,
|
|
44
|
-
originalError: message
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async _vectorSearch(query, limit, filter = null) {
|
|
50
|
-
const embedding = await this.embeddingFactory.embed(query);
|
|
51
|
-
const searchOptions = { limit, metric: 'cosine' };
|
|
52
|
-
if (filter) searchOptions.filter = filter;
|
|
53
|
-
|
|
54
|
-
const result = await this.client.search(embedding, searchOptions);
|
|
55
|
-
return result.map(r => ({
|
|
56
|
-
...r,
|
|
57
|
-
score: 1 - (r.score || 0),
|
|
58
|
-
searchType: 'vector'
|
|
59
|
-
}));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async _keywordSearch(query, limit, filter = null) {
|
|
63
|
-
try {
|
|
64
|
-
const embedding = await this.embeddingFactory.embed(query);
|
|
65
|
-
const searchOptions = { limit: limit * 3, metric: 'cosine' };
|
|
66
|
-
if (filter) searchOptions.filter = filter;
|
|
67
|
-
|
|
68
|
-
const result = await this.client.search(embedding, searchOptions);
|
|
69
|
-
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
70
|
-
|
|
71
|
-
return result
|
|
72
|
-
.filter(r => {
|
|
73
|
-
const content = r.content.toLowerCase();
|
|
74
|
-
return queryTerms.some(term => content.includes(term));
|
|
75
|
-
})
|
|
76
|
-
.slice(0, limit)
|
|
77
|
-
.map(r => ({
|
|
78
|
-
...r,
|
|
79
|
-
score: r.score || 0,
|
|
80
|
-
searchType: 'keyword'
|
|
81
|
-
}));
|
|
82
|
-
} catch (error) {
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
_reciprocalRankFusion(vectorResults, keywordResults, alpha, k = 60) {
|
|
88
|
-
const scores = new Map();
|
|
89
|
-
vectorResults.forEach((result, index) => {
|
|
90
|
-
const rr = 1 / (k + index + 1);
|
|
91
|
-
scores.set(result.id, {
|
|
92
|
-
result,
|
|
93
|
-
vectorScore: rr * (1 - alpha),
|
|
94
|
-
keywordScore: 0,
|
|
95
|
-
vectorRank: index + 1,
|
|
96
|
-
keywordRank: null
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
keywordResults.forEach((result, index) => {
|
|
101
|
-
const rr = 1 / (k + index + 1);
|
|
102
|
-
if (scores.has(result.id)) {
|
|
103
|
-
const entry = scores.get(result.id);
|
|
104
|
-
entry.keywordScore = rr * alpha;
|
|
105
|
-
entry.keywordRank = index + 1;
|
|
106
|
-
} else {
|
|
107
|
-
scores.set(result.id, {
|
|
108
|
-
result,
|
|
109
|
-
vectorScore: 0,
|
|
110
|
-
keywordScore: rr * alpha,
|
|
111
|
-
vectorRank: null,
|
|
112
|
-
keywordRank: index + 1
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
return Array.from(scores.values())
|
|
118
|
-
.map(({ result, vectorScore, keywordScore, vectorRank, keywordRank }) => ({
|
|
119
|
-
...result,
|
|
120
|
-
combinedScore: vectorScore + keywordScore,
|
|
121
|
-
vectorScore,
|
|
122
|
-
keywordScore,
|
|
123
|
-
vectorRank,
|
|
124
|
-
keywordRank,
|
|
125
|
-
searchType: vectorRank !== null && keywordRank !== null
|
|
126
|
-
? 'hybrid'
|
|
127
|
-
: vectorRank !== null ? 'vector' : 'keyword'
|
|
128
|
-
}))
|
|
129
|
-
.sort((a, b) => b.combinedScore - a.combinedScore);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
getStats() {
|
|
133
|
-
return { alpha: this.alpha, rrfK: this.rrfK, type: 'hybrid' };
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export default HybridSearch;
|