postgres-mcp-tools
Version:
PostgreSQL-based memory system with vector search capabilities for AI applications, including MCP integration for Claude
242 lines (210 loc) • 6.42 kB
text/typescript
import { query } from '../db/client.js';
import { logger } from '../utils/logger.js';
import { generateEmbedding } from './embedding-service.js';
/**
* Add a memory to the database
*
* @param conversationId - Unique identifier for the conversation
* @param userId - Identifier for the user
* @param content - The content of the memory
* @param metadata - Additional metadata for the memory
* @returns The ID of the created memory
*/
export const addMemory = async (
conversationId: string,
userId: string,
content: string,
metadata: any = {}
): Promise<number> => {
try {
logger.info(`Adding memory for conversation ${conversationId}`);
// Generate an embedding for the content
const embedding = await generateEmbedding(content);
// Insert the memory into the database
const result = await query(`
INSERT INTO memory.conversations
(conversation_id, user_id, content, embedding, metadata)
VALUES
($1, $2, $3, $4, $5)
RETURNING id
`, [
conversationId,
userId,
content,
embedding,
metadata
]);
const memoryId = result.rows[0].id;
logger.info(`Memory added with ID: ${memoryId}`);
return memoryId;
} catch (error) {
logger.error('Error adding memory:', error);
throw error;
}
};
/**
* Search for memories by similarity
*
* @param queryText - The text to search for
* @param limit - Maximum number of results to return
* @param userId - Optional user ID to filter by
* @param conversationId - Optional conversation ID to filter by
* @returns Array of memory objects
*/
export const searchMemories = async (
queryText: string,
limit: number = 5,
userId: string | null = null,
conversationId: string | null = null
): Promise<any[]> => {
try {
logger.info(`Searching memories similar to: "${queryText.substring(0, 50)}..."`);
// Generate an embedding for the query text
const queryEmbedding = await generateEmbedding(queryText);
// Build the SQL query with optional filters
let sql = `
SELECT
id,
conversation_id,
user_id,
content,
timestamp,
metadata,
embedding <=> $1 as similarity
FROM memory.conversations
WHERE is_archived = FALSE
`;
const params: any[] = [queryEmbedding];
let paramIndex = 2;
// Add filters if provided
if (userId) {
sql += ` AND user_id = $${paramIndex}`;
params.push(userId);
paramIndex++;
}
if (conversationId) {
sql += ` AND conversation_id = $${paramIndex}`;
params.push(conversationId);
paramIndex++;
}
// Add ordering and limit
sql += `
ORDER BY similarity ASC
LIMIT $${paramIndex}
`;
params.push(limit);
// Execute the query
const result = await query(sql, params);
// Update last_accessed timestamp for retrieved memories
if (result.rows.length > 0) {
const ids = result.rows.map((row: any) => row.id);
await query(`
UPDATE memory.conversations
SET last_accessed = NOW()
WHERE id = ANY($1)
`, [ids]);
}
logger.info(`Found ${result.rows.length} similar memories`);
return result.rows;
} catch (error) {
logger.error('Error searching memories:', error);
throw error;
}
};
/**
* Get all memories for a specific conversation
*
* @param conversationId - The conversation ID to get memories for
* @returns Array of memory objects
*/
export const getConversationMemories = async (conversationId: string): Promise<any[]> => {
try {
logger.info(`Getting memories for conversation ${conversationId}`);
const result = await query(`
SELECT
id,
user_id,
content,
timestamp,
metadata
FROM memory.conversations
WHERE conversation_id = $1
AND is_archived = FALSE
ORDER BY timestamp ASC
`, [conversationId]);
logger.info(`Found ${result.rows.length} memories for conversation ${conversationId}`);
// Update last_accessed timestamp for retrieved memories
if (result.rows.length > 0) {
const ids = result.rows.map((row: any) => row.id);
await query(`
UPDATE memory.conversations
SET last_accessed = NOW()
WHERE id = ANY($1)
`, [ids]);
}
return result.rows;
} catch (error) {
logger.error(`Error getting memories for conversation ${conversationId}:`, error);
throw error;
}
};
/**
* Delete a memory by ID
*
* @param id - The ID of the memory to delete
* @returns True if the memory was deleted
*/
export const deleteMemory = async (id: number): Promise<boolean> => {
try {
logger.info(`Deleting memory ${id}`);
const result = await query(`
DELETE FROM memory.conversations
WHERE id = $1
RETURNING id
`, [id]);
const deleted = result.rows.length > 0;
logger.info(deleted ? `Memory ${id} deleted` : `Memory ${id} not found`);
return deleted;
} catch (error) {
logger.error(`Error deleting memory ${id}:`, error);
throw error;
}
};
/**
* Archive memories older than a certain number of days
*
* @param daysToKeep - Number of days to keep memories before archiving
* @returns Number of memories archived
*/
export const archiveOldMemories = async (daysToKeep: number = 90): Promise<number> => {
try {
logger.info(`Archiving memories older than ${daysToKeep} days`);
const result = await query(`
SELECT memory.archive_old_memories($1) as archived_count
`, [daysToKeep]);
const archivedCount = result.rows[0].archived_count;
logger.info(`Archived ${archivedCount} memories`);
return archivedCount;
} catch (error) {
logger.error(`Error archiving old memories:`, error);
throw error;
}
};
/**
* Optimize the vector index for better performance
*
* @returns True if optimization was successful
*/
export const optimizeVectorIndex = async (): Promise<boolean> => {
try {
logger.info('Optimizing vector index');
await query(`
SELECT memory.optimize_vector_index()
`);
logger.info('Vector index optimization completed');
return true;
} catch (error) {
logger.error('Error optimizing vector index:', error);
throw error;
}
};