@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
432 lines (370 loc) • 11.8 kB
text/typescript
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,
};