@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.
@@ -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;
@@ -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;
@@ -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;