@prmichaelsen/remember-mcp 0.1.0 → 0.2.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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * remember_query_memory tool
3
+ * RAG (Retrieval-Augmented Generation) queries with natural language
4
+ */
5
+
6
+ import type { Memory, SearchFilters } from '../types/memory.js';
7
+ import { getMemoryCollection } from '../weaviate/schema.js';
8
+ import { logger } from '../utils/logger.js';
9
+
10
+ /**
11
+ * Tool definition for remember_query_memory
12
+ */
13
+ export const queryMemoryTool = {
14
+ name: 'remember_query_memory',
15
+ description: `Query memories using natural language for RAG (Retrieval-Augmented Generation).
16
+
17
+ This tool is optimized for LLM context retrieval. It returns relevant memories
18
+ with their full content and context, formatted for easy consumption by LLMs.
19
+
20
+ Use this when you need to:
21
+ - Answer questions based on stored memories
22
+ - Provide context for conversations
23
+ - Retrieve information for decision-making
24
+ - Build responses using past knowledge
25
+
26
+ Examples:
27
+ - "What do I know about camping?"
28
+ - "Tell me about the recipes I've saved"
29
+ - "What meetings did I have last week?"
30
+ - "What are my project goals?"
31
+ `,
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ query: {
36
+ type: 'string',
37
+ description: 'Natural language query',
38
+ },
39
+ limit: {
40
+ type: 'number',
41
+ description: 'Maximum number of memories to retrieve. Default: 5',
42
+ minimum: 1,
43
+ maximum: 50,
44
+ default: 5,
45
+ },
46
+ min_relevance: {
47
+ type: 'number',
48
+ description: 'Minimum relevance score (0-1). Default: 0.6',
49
+ minimum: 0,
50
+ maximum: 1,
51
+ default: 0.6,
52
+ },
53
+ filters: {
54
+ type: 'object',
55
+ description: 'Optional filters to narrow results',
56
+ properties: {
57
+ types: {
58
+ type: 'array',
59
+ items: { type: 'string' },
60
+ description: 'Filter by content types',
61
+ },
62
+ tags: {
63
+ type: 'array',
64
+ items: { type: 'string' },
65
+ description: 'Filter by tags',
66
+ },
67
+ weight_min: {
68
+ type: 'number',
69
+ description: 'Minimum weight (0-1)',
70
+ },
71
+ trust_min: {
72
+ type: 'number',
73
+ description: 'Minimum trust level (0-1)',
74
+ },
75
+ date_from: {
76
+ type: 'string',
77
+ description: 'Start date (ISO 8601)',
78
+ },
79
+ date_to: {
80
+ type: 'string',
81
+ description: 'End date (ISO 8601)',
82
+ },
83
+ },
84
+ },
85
+ include_context: {
86
+ type: 'boolean',
87
+ description: 'Include full context metadata. Default: true',
88
+ default: true,
89
+ },
90
+ format: {
91
+ type: 'string',
92
+ description: 'Output format: "detailed" (full objects) or "compact" (text summary). Default: detailed',
93
+ enum: ['detailed', 'compact'],
94
+ default: 'detailed',
95
+ },
96
+ },
97
+ required: ['query'],
98
+ },
99
+ };
100
+
101
+ /**
102
+ * Query memory arguments
103
+ */
104
+ export interface QueryMemoryArgs {
105
+ query: string;
106
+ limit?: number;
107
+ min_relevance?: number;
108
+ filters?: SearchFilters;
109
+ include_context?: boolean;
110
+ format?: 'detailed' | 'compact';
111
+ }
112
+
113
+ /**
114
+ * Memory with relevance score
115
+ */
116
+ export interface RelevantMemory extends Partial<Memory> {
117
+ relevance: number; // 0-1 relevance score
118
+ }
119
+
120
+ /**
121
+ * Query memory result
122
+ */
123
+ export interface QueryMemoryResult {
124
+ query: string;
125
+ memories: RelevantMemory[] | string; // Array for detailed, string for compact
126
+ total: number;
127
+ min_relevance: number;
128
+ context_summary?: string;
129
+ }
130
+
131
+ /**
132
+ * Handle remember_query_memory tool
133
+ */
134
+ export async function handleQueryMemory(
135
+ args: QueryMemoryArgs,
136
+ userId: string
137
+ ): Promise<string> {
138
+ try {
139
+ logger.info('Querying memories', { userId, query: args.query });
140
+
141
+ const collection = getMemoryCollection(userId);
142
+ const limit = args.limit ?? 5;
143
+ const minRelevance = args.min_relevance ?? 0.6;
144
+ const includeContext = args.include_context ?? true;
145
+ const format = args.format ?? 'detailed';
146
+
147
+ // Build where filter for memories only
148
+ const whereFilters: any[] = [
149
+ {
150
+ path: 'doc_type',
151
+ operator: 'Equal',
152
+ valueText: 'memory',
153
+ },
154
+ ];
155
+
156
+ // Add type filter
157
+ if (args.filters?.types && args.filters.types.length > 0) {
158
+ whereFilters.push({
159
+ path: 'type',
160
+ operator: 'ContainsAny',
161
+ valueTextArray: args.filters.types,
162
+ });
163
+ }
164
+
165
+ // Add weight filter
166
+ if (args.filters?.weight_min !== undefined) {
167
+ whereFilters.push({
168
+ path: 'weight',
169
+ operator: 'GreaterThanEqual',
170
+ valueNumber: args.filters.weight_min,
171
+ });
172
+ }
173
+
174
+ // Add trust filter
175
+ if (args.filters?.trust_min !== undefined) {
176
+ whereFilters.push({
177
+ path: 'trust',
178
+ operator: 'GreaterThanEqual',
179
+ valueNumber: args.filters.trust_min,
180
+ });
181
+ }
182
+
183
+ // Add date range filters
184
+ if (args.filters?.date_from) {
185
+ whereFilters.push({
186
+ path: 'created_at',
187
+ operator: 'GreaterThanEqual',
188
+ valueDate: new Date(args.filters.date_from),
189
+ });
190
+ }
191
+
192
+ if (args.filters?.date_to) {
193
+ whereFilters.push({
194
+ path: 'created_at',
195
+ operator: 'LessThanEqual',
196
+ valueDate: new Date(args.filters.date_to),
197
+ });
198
+ }
199
+
200
+ // Build search options
201
+ const searchOptions: any = {
202
+ limit: limit,
203
+ distance: 1 - minRelevance, // Convert relevance to distance
204
+ returnMetadata: ['distance'],
205
+ };
206
+
207
+ // Add filters if present
208
+ if (whereFilters.length > 0) {
209
+ searchOptions.filters = whereFilters.length > 1 ? {
210
+ operator: 'And' as const,
211
+ operands: whereFilters,
212
+ } : whereFilters[0];
213
+ }
214
+
215
+ // Perform semantic search using nearText
216
+ const results = await collection.query.nearText(args.query, searchOptions);
217
+
218
+ // Format memories with relevance scores
219
+ const relevantMemories: RelevantMemory[] = results.objects.map((obj: any) => {
220
+ const relevance = 1 - (obj.metadata?.distance ?? 0); // Convert distance to relevance
221
+ const memory: RelevantMemory = {
222
+ id: obj.uuid,
223
+ ...obj.properties,
224
+ relevance: Math.max(0, Math.min(1, relevance)), // Clamp to [0, 1]
225
+ };
226
+
227
+ // Remove context if not requested
228
+ if (!includeContext) {
229
+ delete memory.context;
230
+ delete memory.location;
231
+ }
232
+
233
+ return memory;
234
+ });
235
+
236
+ // Sort by relevance (highest first)
237
+ relevantMemories.sort((a, b) => (b.relevance ?? 0) - (a.relevance ?? 0));
238
+
239
+ logger.info('Query completed', {
240
+ userId,
241
+ query: args.query,
242
+ results: relevantMemories.length,
243
+ });
244
+
245
+ // Format output based on requested format
246
+ let formattedMemories: RelevantMemory[] | string;
247
+ let contextSummary: string | undefined;
248
+
249
+ if (format === 'compact') {
250
+ // Compact format: text summary for easy LLM consumption
251
+ const summaryParts = relevantMemories.map((mem, idx) => {
252
+ const title = mem.title ? `"${mem.title}"` : `Memory ${idx + 1}`;
253
+ const type = mem.type ? ` [${mem.type}]` : '';
254
+ const relevancePercent = Math.round((mem.relevance ?? 0) * 100);
255
+ const content = mem.content || '(no content)';
256
+ const tags = mem.tags && mem.tags.length > 0 ? `\nTags: ${mem.tags.join(', ')}` : '';
257
+
258
+ return `${idx + 1}. ${title}${type} (${relevancePercent}% relevant)\n${content}${tags}`;
259
+ });
260
+
261
+ formattedMemories = summaryParts.join('\n\n---\n\n');
262
+
263
+ contextSummary = `Found ${relevantMemories.length} relevant memories for query: "${args.query}"`;
264
+ } else {
265
+ // Detailed format: full objects
266
+ formattedMemories = relevantMemories;
267
+
268
+ contextSummary = `Retrieved ${relevantMemories.length} memories with relevance >= ${Math.round(minRelevance * 100)}%`;
269
+ }
270
+
271
+ const result: QueryMemoryResult = {
272
+ query: args.query,
273
+ memories: formattedMemories,
274
+ total: relevantMemories.length,
275
+ min_relevance: minRelevance,
276
+ context_summary: contextSummary,
277
+ };
278
+
279
+ return JSON.stringify(result, null, 2);
280
+ } catch (error) {
281
+ logger.error('Failed to query memories:', error);
282
+ throw new Error(`Failed to query memories: ${error instanceof Error ? error.message : String(error)}`);
283
+ }
284
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * remember_search_relationship tool
3
+ * Search relationships by observation text or type
4
+ */
5
+
6
+ import type { Relationship } from '../types/memory.js';
7
+ import { getMemoryCollection } from '../weaviate/schema.js';
8
+ import { logger } from '../utils/logger.js';
9
+
10
+ /**
11
+ * Tool definition for remember_search_relationship
12
+ */
13
+ export const searchRelationshipTool = {
14
+ name: 'remember_search_relationship',
15
+ description: `Search relationships by observation text or relationship type.
16
+
17
+ Uses semantic search on relationship observations to find connections.
18
+ Can filter by relationship type, strength, and tags.
19
+ Returns relationships with their connected memory IDs.
20
+
21
+ Examples:
22
+ - "Find relationships about inspiration"
23
+ - "Search for contradicting relationships"
24
+ - "Show me all 'caused_by' relationships"
25
+ `,
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ query: {
30
+ type: 'string',
31
+ description: 'Search query (semantic search on observation field)',
32
+ },
33
+ relationship_types: {
34
+ type: 'array',
35
+ items: { type: 'string' },
36
+ description: 'Filter by relationship types (e.g., ["inspired_by", "caused_by"])',
37
+ },
38
+ strength_min: {
39
+ type: 'number',
40
+ description: 'Minimum strength (0-1)',
41
+ minimum: 0,
42
+ maximum: 1,
43
+ },
44
+ confidence_min: {
45
+ type: 'number',
46
+ description: 'Minimum confidence (0-1)',
47
+ minimum: 0,
48
+ maximum: 1,
49
+ },
50
+ tags: {
51
+ type: 'array',
52
+ items: { type: 'string' },
53
+ description: 'Filter by tags',
54
+ },
55
+ limit: {
56
+ type: 'number',
57
+ description: 'Maximum number of results (default: 10)',
58
+ minimum: 1,
59
+ maximum: 100,
60
+ },
61
+ offset: {
62
+ type: 'number',
63
+ description: 'Offset for pagination (default: 0)',
64
+ minimum: 0,
65
+ },
66
+ },
67
+ required: ['query'],
68
+ },
69
+ };
70
+
71
+ /**
72
+ * Search relationship arguments
73
+ */
74
+ export interface SearchRelationshipArgs {
75
+ query: string;
76
+ relationship_types?: string[];
77
+ strength_min?: number;
78
+ confidence_min?: number;
79
+ tags?: string[];
80
+ limit?: number;
81
+ offset?: number;
82
+ }
83
+
84
+ /**
85
+ * Search relationship result
86
+ */
87
+ export interface SearchRelationshipResult {
88
+ relationships: Relationship[];
89
+ total: number;
90
+ offset: number;
91
+ limit: number;
92
+ message: string;
93
+ }
94
+
95
+ /**
96
+ * Handle remember_search_relationship tool
97
+ */
98
+ export async function handleSearchRelationship(
99
+ args: SearchRelationshipArgs,
100
+ userId: string
101
+ ): Promise<string> {
102
+ try {
103
+ logger.info('Searching relationships', {
104
+ userId,
105
+ query: args.query,
106
+ types: args.relationship_types
107
+ });
108
+
109
+ const collection = getMemoryCollection(userId);
110
+ const limit = args.limit ?? 10;
111
+ const offset = args.offset ?? 0;
112
+
113
+ // Build where filter for doc_type and other filters
114
+ const whereFilters: any[] = [
115
+ {
116
+ path: 'doc_type',
117
+ operator: 'Equal',
118
+ valueText: 'relationship',
119
+ },
120
+ ];
121
+
122
+ // Add relationship type filter
123
+ if (args.relationship_types && args.relationship_types.length > 0) {
124
+ if (args.relationship_types.length === 1) {
125
+ whereFilters.push({
126
+ path: 'relationship_type',
127
+ operator: 'Equal',
128
+ valueText: args.relationship_types[0],
129
+ });
130
+ } else {
131
+ whereFilters.push({
132
+ operator: 'Or',
133
+ operands: args.relationship_types.map(type => ({
134
+ path: 'relationship_type',
135
+ operator: 'Equal',
136
+ valueText: type,
137
+ })),
138
+ });
139
+ }
140
+ }
141
+
142
+ // Add strength filter
143
+ if (args.strength_min !== undefined) {
144
+ whereFilters.push({
145
+ path: 'strength',
146
+ operator: 'GreaterThanEqual',
147
+ valueNumber: args.strength_min,
148
+ });
149
+ }
150
+
151
+ // Add confidence filter
152
+ if (args.confidence_min !== undefined) {
153
+ whereFilters.push({
154
+ path: 'confidence',
155
+ operator: 'GreaterThanEqual',
156
+ valueNumber: args.confidence_min,
157
+ });
158
+ }
159
+
160
+ // Add tags filter
161
+ if (args.tags && args.tags.length > 0) {
162
+ whereFilters.push({
163
+ path: 'tags',
164
+ operator: 'ContainsAny',
165
+ valueTextArray: args.tags,
166
+ });
167
+ }
168
+
169
+ // Build search options
170
+ const searchOptions: any = {
171
+ alpha: 1.0, // Pure semantic search for relationships
172
+ limit: limit + offset, // Get extra for offset
173
+ };
174
+
175
+ // Add filters if present
176
+ if (whereFilters.length > 0) {
177
+ searchOptions.filters = whereFilters.length > 1 ? {
178
+ operator: 'And' as const,
179
+ operands: whereFilters,
180
+ } : whereFilters[0];
181
+ }
182
+
183
+ // Perform hybrid search (semantic search on observation field)
184
+ const results = await collection.query.hybrid(args.query, searchOptions);
185
+
186
+ // Apply offset manually (Weaviate v4 doesn't have built-in offset for nearText)
187
+ const paginatedResults = results.objects.slice(offset, offset + limit);
188
+
189
+ // Map results to Relationship type
190
+ const relationships: Relationship[] = paginatedResults.map((obj: any) => ({
191
+ id: obj.uuid,
192
+ user_id: obj.properties.user_id,
193
+ doc_type: 'relationship',
194
+ memory_ids: obj.properties.memory_ids || [],
195
+ relationship_type: obj.properties.relationship_type,
196
+ observation: obj.properties.observation,
197
+ strength: obj.properties.strength,
198
+ confidence: obj.properties.confidence,
199
+ context: obj.properties.context || {
200
+ timestamp: obj.properties.created_at,
201
+ source: { type: 'api', platform: 'mcp' },
202
+ },
203
+ created_at: obj.properties.created_at,
204
+ updated_at: obj.properties.updated_at,
205
+ version: obj.properties.version,
206
+ tags: obj.properties.tags || [],
207
+ }));
208
+
209
+ logger.info('Relationship search completed', {
210
+ userId,
211
+ found: relationships.length,
212
+ total: results.objects.length
213
+ });
214
+
215
+ const result: SearchRelationshipResult = {
216
+ relationships,
217
+ total: results.objects.length,
218
+ offset,
219
+ limit,
220
+ message: `Found ${relationships.length} relationship(s) matching query "${args.query}"`,
221
+ };
222
+
223
+ return JSON.stringify(result, null, 2);
224
+ } catch (error) {
225
+ logger.error('Failed to search relationships:', error);
226
+ throw new Error(`Failed to search relationships: ${error instanceof Error ? error.message : String(error)}`);
227
+ }
228
+ }