UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

432 lines (370 loc) 11.8 kB
import { VectorStoreType, VectorStoreConfig, Document, ChatbotError, } from "../contexts/ChatbotContext"; import { createClient } from "@supabase/supabase-js"; // Abstract base class for vector store operations export abstract class BaseVectorStore { protected config: VectorStoreConfig; protected isConnected: boolean = false; constructor(config: VectorStoreConfig) { this.config = config; } abstract connect(): Promise<void>; abstract disconnect(): Promise<void>; abstract addDocuments(documents: Document[]): Promise<void>; abstract removeDocuments(documentIds: string[]): Promise<void>; abstract similaritySearch(query: string, limit?: number): Promise<Document[]>; abstract getCollections(): Promise<string[]>; abstract getDocumentCount(): Promise<number>; abstract healthCheck(): Promise<boolean>; public getStatus(): { isConnected: boolean; config: VectorStoreConfig } { return { isConnected: this.isConnected, config: this.config, }; } } // Supabase Vector Store Implementation export class SupabaseVectorStore extends BaseVectorStore { private client: any = null; private tableName: string = "documents"; constructor(config: VectorStoreConfig) { super(config); this.tableName = config.collection || "documents"; } async connect(): Promise<void> { try { if (!this.config.url || !this.config.apiKey) { throw new Error("Supabase URL and API key are required"); } this.client = createClient(this.config.url, this.config.apiKey); // Test connection const { error } = await this.client .from(this.tableName) .select("id") .limit(1); if (error && error.code !== "PGRST116") { // PGRST116 is "not found" which is ok for empty table throw error; } this.isConnected = true; } catch (error) { this.isConnected = false; const chatbotError: ChatbotError = { code: "SUPABASE_CONNECTION_ERROR", message: `Failed to connect to Supabase: ${ error instanceof Error ? error.message : "Unknown error" }`, details: error, timestamp: new Date(), }; throw new Error(chatbotError.message); } } async disconnect(): Promise<void> { this.client = null; this.isConnected = false; } async addDocuments(documents: Document[]): Promise<void> { if (!this.isConnected || !this.client) { throw new Error("Vector store is not connected"); } try { const supabaseDocuments = documents.map((doc) => ({ id: doc.id, title: doc.title, content: doc.content, metadata: doc.metadata, embedding: doc.embedding, status: doc.status, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), })); const { error } = await this.client .from(this.tableName) .upsert(supabaseDocuments); if (error) { throw error; } } catch (error) { const chatbotError: ChatbotError = { code: "SUPABASE_ADD_DOCUMENTS_ERROR", message: `Failed to add documents to Supabase: ${ error instanceof Error ? error.message : "Unknown error" }`, details: error, timestamp: new Date(), }; throw new Error(chatbotError.message); } } async removeDocuments(documentIds: string[]): Promise<void> { if (!this.isConnected || !this.client) { throw new Error("Vector store is not connected"); } try { const { error } = await this.client .from(this.tableName) .delete() .in("id", documentIds); if (error) { throw error; } } catch (error) { const chatbotError: ChatbotError = { code: "SUPABASE_REMOVE_DOCUMENTS_ERROR", message: `Failed to remove documents from Supabase: ${ error instanceof Error ? error.message : "Unknown error" }`, details: error, timestamp: new Date(), }; throw new Error(chatbotError.message); } } async similaritySearch(query: string, limit = 5): Promise<Document[]> { if (!this.isConnected || !this.client) { throw new Error("Vector store is not connected"); } try { // For now, we'll do a simple text search. // In a real implementation, you'd convert query to embedding and use vector similarity const { data, error } = await this.client .from(this.tableName) .select("*") .textSearch("content", query) .limit(limit); if (error) { throw error; } return (data || []).map(this.mapSupabaseToDocument); } catch (error) { const chatbotError: ChatbotError = { code: "SUPABASE_SIMILARITY_SEARCH_ERROR", message: `Failed to perform similarity search in Supabase: ${ error instanceof Error ? error.message : "Unknown error" }`, details: error, timestamp: new Date(), }; throw new Error(chatbotError.message); } } async getCollections(): Promise<string[]> { // For Supabase, we'll return the table name as the collection return [this.tableName]; } async getDocumentCount(): Promise<number> { if (!this.isConnected || !this.client) { throw new Error("Vector store is not connected"); } try { const { count, error } = await this.client .from(this.tableName) .select("*", { count: "exact", head: true }); if (error) { throw error; } return count || 0; } catch (error) { console.error("Failed to get document count:", error); return 0; } } async healthCheck(): Promise<boolean> { try { if (!this.client) return false; const { error } = await this.client .from(this.tableName) .select("id") .limit(1); return !error || error.code === "PGRST116"; // PGRST116 is "not found" which is ok } catch { return false; } } private mapSupabaseToDocument(row: any): Document { return { id: row.id, title: row.title, content: row.content, metadata: { ...row.metadata, uploadedAt: new Date(row.created_at || row.metadata?.uploadedAt), size: row.metadata?.size || 0, type: row.metadata?.type || "unknown", }, embedding: row.embedding, status: row.status || "ready", }; } } // Qdrant Vector Store Implementation (placeholder) export class QdrantVectorStore extends BaseVectorStore { async connect(): Promise<void> { // TODO: Implement Qdrant connection throw new Error("Qdrant vector store not implemented yet"); } async disconnect(): Promise<void> { this.isConnected = false; } async addDocuments(documents: Document[]): Promise<void> { throw new Error("Qdrant vector store not implemented yet"); } async removeDocuments(documentIds: string[]): Promise<void> { throw new Error("Qdrant vector store not implemented yet"); } async similaritySearch(query: string, limit = 5): Promise<Document[]> { throw new Error("Qdrant vector store not implemented yet"); } async getCollections(): Promise<string[]> { return []; } async getDocumentCount(): Promise<number> { return 0; } async healthCheck(): Promise<boolean> { return false; } } // Pinecone Vector Store Implementation (placeholder) export class PineconeVectorStore extends BaseVectorStore { async connect(): Promise<void> { // TODO: Implement Pinecone connection throw new Error("Pinecone vector store not implemented yet"); } async disconnect(): Promise<void> { this.isConnected = false; } async addDocuments(documents: Document[]): Promise<void> { throw new Error("Pinecone vector store not implemented yet"); } async removeDocuments(documentIds: string[]): Promise<void> { throw new Error("Pinecone vector store not implemented yet"); } async similaritySearch(query: string, limit = 5): Promise<Document[]> { throw new Error("Pinecone vector store not implemented yet"); } async getCollections(): Promise<string[]> { return []; } async getDocumentCount(): Promise<number> { return 0; } async healthCheck(): Promise<boolean> { return false; } } // Factory function to create vector store instances export function createVectorStore(config: VectorStoreConfig): BaseVectorStore { switch (config.type) { case VectorStoreType.SUPABASE: return new SupabaseVectorStore(config); case VectorStoreType.QDRANT: return new QdrantVectorStore(config); case VectorStoreType.PINECONE: return new PineconeVectorStore(config); default: throw new Error(`Unsupported vector store type: ${config.type}`); } } // Embedding service (placeholder for now) export class EmbeddingService { static async generateEmbedding(text: string): Promise<number[]> { // TODO: Implement actual embedding generation using OpenAI, Cohere, or other embedding API // For now, return a mock embedding console.warn( "EmbeddingService: Using mock embeddings - implement actual embedding generation" ); // Mock embedding - in reality, this would be a call to an embedding API const mockEmbedding = new Array(1536) .fill(0) .map(() => Math.random() * 2 - 1); return mockEmbedding; } static async generateEmbeddings(texts: string[]): Promise<number[][]> { // TODO: Batch embedding generation for efficiency console.warn( "EmbeddingService: Using mock embeddings - implement actual batch embedding generation" ); return Promise.all(texts.map((text) => this.generateEmbedding(text))); } } // Vector store utilities export class VectorStoreUtils { static async prepareDocumentsForStorage( documents: Document[] ): Promise<Document[]> { const preparedDocuments: Document[] = []; for (const doc of documents) { const preparedDoc = { ...doc }; // Generate embedding if not present if (!preparedDoc.embedding) { try { preparedDoc.embedding = await EmbeddingService.generateEmbedding( doc.content ); } catch (error) { console.error( `Failed to generate embedding for document ${doc.id}:`, error ); preparedDoc.status = "error"; } } preparedDocuments.push(preparedDoc); } return preparedDocuments; } static validateConfig(config: VectorStoreConfig): boolean { if (!config.type) return false; switch (config.type) { case VectorStoreType.SUPABASE: return !!(config.url && config.apiKey); case VectorStoreType.QDRANT: return !!(config.url && config.apiKey); case VectorStoreType.PINECONE: return !!config.apiKey; default: return false; } } static getDefaultConfig(type: VectorStoreType): Partial<VectorStoreConfig> { const baseConfig = { type, dimensions: 1536, // OpenAI embedding dimension similarity: "cosine" as const, }; switch (type) { case VectorStoreType.SUPABASE: return { ...baseConfig, collection: "documents", }; case VectorStoreType.QDRANT: return { ...baseConfig, collection: "documents", }; case VectorStoreType.PINECONE: return { ...baseConfig, namespace: "default", }; default: return baseConfig; } } } export default { BaseVectorStore, SupabaseVectorStore, QdrantVectorStore, PineconeVectorStore, createVectorStore, EmbeddingService, VectorStoreUtils, };