@rws-framework/ai-tools 2.2.0 → 3.0.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,378 @@
1
+ import { Injectable } from '@rws-framework/server/nest';
2
+ import { Document } from '@langchain/core/documents';
3
+ import { LangChainEmbeddingService } from './LangChainEmbeddingService';
4
+ import RWSVectorStore, { VectorDocType, IVectorStoreConfig as RWSVectorStoreConfig } from '../models/convo/VectorStore';
5
+
6
+ export interface ISearchResult {
7
+ content: string;
8
+ score: number;
9
+ metadata: any;
10
+ chunkId: string;
11
+ }
12
+
13
+ export interface IVectorStoreConfig {
14
+ type: 'memory' | 'faiss';
15
+ maxResults?: number;
16
+ autoSave?: boolean;
17
+ similarityThreshold?: number;
18
+ persistPath?: string;
19
+ }
20
+
21
+ export interface IDocumentChunk {
22
+ id: string;
23
+ content: string;
24
+ embedding?: number[];
25
+ metadata?: {
26
+ documentId: string;
27
+ chunkIndex: number;
28
+ source?: string;
29
+ title?: string;
30
+ knowledgeId?: string;
31
+ [key: string]: any;
32
+ };
33
+ }
34
+
35
+ export interface IVectorSearchRequest {
36
+ query: string;
37
+ maxResults?: number;
38
+ similarityThreshold?: number;
39
+ filter?: {
40
+ knowledgeIds?: string[];
41
+ documentIds?: string[];
42
+ [key: string]: any;
43
+ };
44
+ }
45
+
46
+ export interface IVectorSearchResponse {
47
+ results: ISearchResult[];
48
+ totalResults: number;
49
+ }
50
+
51
+ /**
52
+ * LangChain-based vector store service that provides document storage and similarity search
53
+ * Now uses RWSVectorStore for unified vector storage like the LangChain tutorial
54
+ */
55
+ @Injectable()
56
+ export class LangChainVectorStoreService {
57
+ private vectorStore: RWSVectorStore;
58
+ private documents: Map<string, Document> = new Map();
59
+ private config: IVectorStoreConfig;
60
+ private isInitialized = false;
61
+ private documentCount = 0;
62
+ private embeddingService: LangChainEmbeddingService;
63
+
64
+ constructor() {
65
+ // Empty constructor for NestJS dependency injection
66
+ }
67
+
68
+ /**
69
+ * Initialize the service with configuration
70
+ */
71
+ async initialize(embeddingService: LangChainEmbeddingService, config: IVectorStoreConfig): Promise<void> {
72
+ if (this.isInitialized) {
73
+ return;
74
+ }
75
+
76
+ this.embeddingService = embeddingService;
77
+ this.config = {
78
+ type: 'memory',
79
+ maxResults: 10,
80
+ similarityThreshold: 0.1, // Use lower threshold like we configured
81
+ ...config,
82
+ };
83
+
84
+ const embeddings = this.embeddingService.getEmbeddingsInstance();
85
+
86
+ // Initialize with empty documents first, then create RWSVectorStore
87
+ const initialDocs: VectorDocType = [];
88
+ this.vectorStore = await new RWSVectorStore(
89
+ initialDocs,
90
+ embeddings,
91
+ {
92
+ type: this.config.type,
93
+ persistPath: this.config.persistPath
94
+ }
95
+ ).init();
96
+
97
+ console.log(`Created new ${this.config.type} vector store using RWSVectorStore`);
98
+ this.isInitialized = true;
99
+ }
100
+
101
+ /**
102
+ * Add documents to the vector store
103
+ */
104
+ async addDocuments(documents: Document[]): Promise<string[]> {
105
+ this.ensureInitialized();
106
+
107
+ try {
108
+ const ids = await this.vectorStore.addDocuments(documents);
109
+ const docIds: string[] = [];
110
+
111
+ // Store documents in our map for retrieval
112
+ documents.forEach((doc, index) => {
113
+ const id = `doc_${Date.now()}_${index}`;
114
+ this.documents.set(id, doc);
115
+ docIds.push(id);
116
+ });
117
+
118
+ this.documentCount += documents.length;
119
+ return docIds;
120
+ } catch (error) {
121
+ console.error('Failed to add documents to vector store:', error);
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Index a single document (split into chunks if needed)
128
+ */
129
+ async indexDocument(
130
+ content: string,
131
+ documentId: string,
132
+ metadata: Record<string, any> = {}
133
+ ): Promise<{ success: boolean; chunkIds: string[]; error?: string }> {
134
+ try {
135
+ this.ensureInitialized();
136
+
137
+ // Remove existing chunks for this document
138
+ await this.deleteDocument(documentId);
139
+
140
+ // Create document chunks
141
+ const documents = await this.embeddingService.createDocuments(content, {
142
+ documentId,
143
+ ...metadata,
144
+ });
145
+
146
+ // Add to vector store
147
+ const chunkIds = await this.addDocuments(documents);
148
+
149
+ return {
150
+ success: true,
151
+ chunkIds,
152
+ };
153
+ } catch (error: any) {
154
+ console.error(`Failed to index document ${documentId}:`, error);
155
+ return {
156
+ success: false,
157
+ chunkIds: [],
158
+ error: error?.message || 'Unknown error',
159
+ };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Search for similar documents using RWSVectorStore like the LangChain tutorial
165
+ */
166
+ async searchSimilar(request: IVectorSearchRequest): Promise<IVectorSearchResponse> {
167
+ this.ensureInitialized();
168
+
169
+ try {
170
+ const {
171
+ query,
172
+ maxResults = this.config.maxResults,
173
+ similarityThreshold = this.config.similarityThreshold,
174
+ filter,
175
+ } = request;
176
+
177
+ // Use RWSVectorStore's similaritySearchWithScore method like the tutorial
178
+ const searchResults = await this.vectorStore.similaritySearchWithScore(
179
+ query,
180
+ maxResults || 10
181
+ );
182
+
183
+ // Filter by similarity threshold and metadata filters
184
+ let filteredResults = searchResults
185
+ .filter(([_doc, score]: [any, any]) => score >= (similarityThreshold || 0.1));
186
+
187
+ // Apply knowledge/document ID filters if provided
188
+ if (filter) {
189
+ filteredResults = filteredResults.filter(([doc, _score]: [any, any]) => {
190
+ // Check knowledge IDs
191
+ if (filter.knowledgeIds && filter.knowledgeIds.length > 0) {
192
+ const docKnowledgeId = doc.metadata?.knowledgeId;
193
+ if (!docKnowledgeId || !filter.knowledgeIds.includes(docKnowledgeId)) {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ // Check document IDs
199
+ if (filter.documentIds && filter.documentIds.length > 0) {
200
+ const docId = doc.metadata?.documentId;
201
+ if (!docId || !filter.documentIds.includes(docId)) {
202
+ return false;
203
+ }
204
+ }
205
+
206
+ return true;
207
+ });
208
+ }
209
+
210
+ // Format results to match our interface
211
+ const results: ISearchResult[] = filteredResults
212
+ .map(([doc, score]: [any, any], index: number) => ({
213
+ content: doc.pageContent,
214
+ score,
215
+ metadata: doc.metadata,
216
+ chunkId: doc.metadata?.id || `chunk_${index}`,
217
+ }));
218
+
219
+ return {
220
+ results,
221
+ totalResults: results.length,
222
+ };
223
+ } catch (error) {
224
+ console.error('Failed to search similar documents:', error);
225
+ return {
226
+ results: [],
227
+ totalResults: 0,
228
+ };
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Delete a document and all its chunks
234
+ */
235
+ async deleteDocument(documentId: string): Promise<boolean> {
236
+ try {
237
+ await this.ensureInitialized();
238
+
239
+ // Find all chunks for this document
240
+ const docsToDelete: string[] = [];
241
+ for (const [id, doc] of this.documents.entries()) {
242
+ if (doc.metadata?.documentId === documentId) {
243
+ docsToDelete.push(id);
244
+ }
245
+ }
246
+
247
+ if (docsToDelete.length > 0) {
248
+ // Use RWSVectorStore's deleteDocuments method
249
+ await this.vectorStore.deleteDocuments(docsToDelete);
250
+
251
+ // Remove from our document map
252
+ docsToDelete.forEach(id => this.documents.delete(id));
253
+
254
+ if (this.config.autoSave) {
255
+ await this.save();
256
+ }
257
+ }
258
+
259
+ return true;
260
+ } catch (error) {
261
+ console.error(`Failed to delete document ${documentId}:`, error);
262
+ return false;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get documents by filter
268
+ */
269
+ async getDocuments(filter?: Record<string, any>): Promise<Document[]> {
270
+ const docs: Document[] = [];
271
+
272
+ for (const doc of this.documents.values()) {
273
+ if (!filter) {
274
+ docs.push(doc);
275
+ continue;
276
+ }
277
+
278
+ let matches = true;
279
+ for (const [key, value] of Object.entries(filter)) {
280
+ if (doc.metadata?.[key] !== value) {
281
+ matches = false;
282
+ break;
283
+ }
284
+ }
285
+
286
+ if (matches) {
287
+ docs.push(doc);
288
+ }
289
+ }
290
+
291
+ return docs;
292
+ }
293
+
294
+ /**
295
+ * Save the vector store to disk (not needed for memory store)
296
+ */
297
+ async save(): Promise<boolean> {
298
+ return true; // Memory store doesn't need saving
299
+ }
300
+
301
+ /**
302
+ * Get statistics about the vector store
303
+ */
304
+ getStats(): { totalChunks: number; totalDocuments: number } {
305
+ const documentIds = new Set<string>();
306
+
307
+ for (const doc of this.documents.values()) {
308
+ if (doc.metadata?.documentId) {
309
+ documentIds.add(doc.metadata.documentId);
310
+ }
311
+ }
312
+
313
+ return {
314
+ totalChunks: this.documentCount,
315
+ totalDocuments: documentIds.size,
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Clear all documents from the vector store
321
+ */
322
+ async clear(): Promise<boolean> {
323
+ try {
324
+ await this.ensureInitialized();
325
+
326
+ // Recreate RWSVectorStore with empty documents
327
+ const embeddings = this.embeddingService.getEmbeddingsInstance();
328
+ const initialDocs: VectorDocType = [];
329
+ this.vectorStore = await new RWSVectorStore(
330
+ initialDocs,
331
+ embeddings,
332
+ {
333
+ type: this.config.type,
334
+ persistPath: this.config.persistPath
335
+ }
336
+ ).init();
337
+
338
+ // Clear our document map
339
+ this.documents.clear();
340
+ this.documentCount = 0;
341
+
342
+ return true;
343
+ } catch (error) {
344
+ console.error('Failed to clear vector store:', error);
345
+ return false;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Ensure the vector store is initialized
351
+ */
352
+ private ensureInitialized(): void {
353
+ if (!this.isInitialized) {
354
+ throw new Error('LangChainVectorStoreService not initialized. Call initialize() first.');
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Get the underlying RWSVectorStore instance
360
+ */
361
+ getVectorStore(): RWSVectorStore {
362
+ return this.vectorStore;
363
+ }
364
+
365
+ /**
366
+ * Get the underlying LangChain vector store (memory or faiss)
367
+ */
368
+ getLangChainVectorStore() {
369
+ return this.vectorStore.getVectorStore();
370
+ }
371
+
372
+ /**
373
+ * Update configuration
374
+ */
375
+ updateConfig(newConfig: Partial<IVectorStoreConfig>): void {
376
+ this.config = { ...this.config, ...newConfig };
377
+ }
378
+ }
@@ -0,0 +1,324 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { LangChainEmbeddingService } from './LangChainEmbeddingService';
3
+ import {
4
+ IOptimizedSearchRequest,
5
+ IOptimizedSearchResult,
6
+ IOptimizedSearchResponse,
7
+ IVectorSearchRequest,
8
+ IVectorSearchResponse,
9
+ ISearchResult
10
+ } from '../types';
11
+
12
+ /**
13
+ * Optimized vector search service for lightning-fast similarity searches
14
+ * Uses pre-computed embeddings and direct cosine similarity calculations
15
+ */
16
+ @Injectable()
17
+ export class OptimizedVectorSearchService {
18
+ private queryEmbeddingCache = new Map<string, number[]>();
19
+ private maxCacheSize = 100;
20
+
21
+ constructor(private embeddingService: LangChainEmbeddingService) {}
22
+
23
+ /**
24
+ * Perform optimized similarity search across pre-computed vectors
25
+ */
26
+ async searchSimilar(request: IOptimizedSearchRequest): Promise<IOptimizedSearchResponse> {
27
+ const startTime = Date.now();
28
+ const { query, knowledgeVectors, maxResults = 5, threshold = 0.1 } = request;
29
+
30
+ // Get or compute query embedding
31
+ const queryEmbedding = await this.getQueryEmbedding(query);
32
+
33
+ // Collect all candidates with parallel processing
34
+ const allCandidates: IOptimizedSearchResult[] = [];
35
+ let totalCandidates = 0;
36
+
37
+ // Process all knowledge vectors in parallel
38
+ const searchPromises = knowledgeVectors.map(async (knowledgeVector) => {
39
+ const candidates: IOptimizedSearchResult[] = [];
40
+ const similarities: number[] = []; // Track all similarities for debugging
41
+
42
+ for (const chunk of knowledgeVector.chunks) {
43
+ totalCandidates++;
44
+
45
+ if (!chunk.embedding || !Array.isArray(chunk.embedding)) {
46
+ continue;
47
+ }
48
+
49
+ // Compute cosine similarity
50
+ const similarity = this.embeddingService.cosineSimilarity(queryEmbedding, chunk.embedding);
51
+ similarities.push(similarity);
52
+
53
+ if (similarity >= threshold) {
54
+ candidates.push({
55
+ content: chunk.content,
56
+ score: similarity,
57
+ metadata: chunk.metadata,
58
+ knowledgeId: knowledgeVector.knowledgeId,
59
+ chunkId: chunk.metadata?.id || `${knowledgeVector.knowledgeId}_chunk_${Date.now()}`
60
+ });
61
+ }
62
+ }
63
+
64
+ // Log similarity statistics for debugging
65
+ if (similarities.length > 0) {
66
+ const maxSim = Math.max(...similarities);
67
+ const avgSim = similarities.reduce((a, b) => a + b, 0) / similarities.length;
68
+ console.log(`[VECTOR SEARCH] Knowledge ${knowledgeVector.knowledgeId}: Max similarity: ${maxSim.toFixed(4)}, Avg: ${avgSim.toFixed(4)}, Candidates above ${threshold}: ${candidates.length}`);
69
+ }
70
+
71
+ return candidates;
72
+ });
73
+
74
+ // Wait for all searches to complete
75
+ const allCandidateArrays = await Promise.all(searchPromises);
76
+
77
+ // Flatten results
78
+ for (const candidates of allCandidateArrays) {
79
+ allCandidates.push(...candidates);
80
+ }
81
+
82
+ // Sort by similarity score and take top results
83
+ const results = allCandidates
84
+ .sort((a, b) => b.score - a.score)
85
+ .slice(0, maxResults);
86
+
87
+ const searchTime = Date.now() - startTime;
88
+
89
+ return {
90
+ results,
91
+ searchTime,
92
+ totalCandidates
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Get query embedding with caching
98
+ */
99
+ private async getQueryEmbedding(query: string): Promise<number[]> {
100
+ // Check cache first
101
+ if (this.queryEmbeddingCache.has(query)) {
102
+ return this.queryEmbeddingCache.get(query)!;
103
+ }
104
+
105
+ // Generate embedding
106
+ const embedding = await this.embeddingService.embedText(query);
107
+
108
+ // Cache the embedding (with size limit)
109
+ if (this.queryEmbeddingCache.size >= this.maxCacheSize) {
110
+ // Remove oldest entry
111
+ const firstKey = this.queryEmbeddingCache.keys().next().value;
112
+ this.queryEmbeddingCache.delete(firstKey);
113
+ }
114
+
115
+ this.queryEmbeddingCache.set(query, embedding);
116
+ return embedding;
117
+ }
118
+
119
+ /**
120
+ * Batch search multiple queries efficiently
121
+ */
122
+ async batchSearch(
123
+ queries: string[],
124
+ knowledgeVectors: Array<{
125
+ knowledgeId: string | number;
126
+ chunks: Array<{
127
+ content: string;
128
+ embedding: number[];
129
+ metadata: any;
130
+ }>;
131
+ }>,
132
+ maxResults = 5,
133
+ threshold = 0.1 // Updated to match other defaults
134
+ ): Promise<Map<string, IOptimizedSearchResponse>> {
135
+ const results = new Map<string, IOptimizedSearchResponse>();
136
+
137
+ // Generate embeddings for all queries in batch
138
+ const queryEmbeddings = await this.embeddingService.embedTexts(queries);
139
+
140
+ // Process each query
141
+ for (let i = 0; i < queries.length; i++) {
142
+ const query = queries[i];
143
+ const queryEmbedding = queryEmbeddings[i];
144
+
145
+ // Cache the embedding
146
+ this.queryEmbeddingCache.set(query, queryEmbedding);
147
+
148
+ // Perform search with pre-computed embedding
149
+ const response = await this.searchWithEmbedding({
150
+ queryEmbedding,
151
+ knowledgeVectors,
152
+ maxResults,
153
+ threshold
154
+ });
155
+
156
+ results.set(query, response);
157
+ }
158
+
159
+ return results;
160
+ }
161
+
162
+ /**
163
+ * Search with pre-computed query embedding
164
+ */
165
+ private async searchWithEmbedding(request: {
166
+ queryEmbedding: number[];
167
+ knowledgeVectors: Array<{
168
+ knowledgeId: string | number;
169
+ chunks: Array<{
170
+ content: string;
171
+ embedding: number[];
172
+ metadata: any;
173
+ }>;
174
+ }>;
175
+ maxResults: number;
176
+ threshold: number;
177
+ }): Promise<IOptimizedSearchResponse> {
178
+ const startTime = Date.now();
179
+ const { queryEmbedding, knowledgeVectors, maxResults, threshold } = request;
180
+
181
+ const allCandidates: IOptimizedSearchResult[] = [];
182
+ let totalCandidates = 0;
183
+
184
+ // Process all knowledge vectors in parallel
185
+ const searchPromises = knowledgeVectors.map(async (knowledgeVector) => {
186
+ const candidates: IOptimizedSearchResult[] = [];
187
+
188
+ for (const chunk of knowledgeVector.chunks) {
189
+ totalCandidates++;
190
+
191
+ if (!chunk.embedding || !Array.isArray(chunk.embedding)) {
192
+ continue;
193
+ }
194
+
195
+ // Compute cosine similarity
196
+ const similarity = this.embeddingService.cosineSimilarity(queryEmbedding, chunk.embedding);
197
+
198
+ if (similarity >= threshold) {
199
+ candidates.push({
200
+ content: chunk.content,
201
+ score: similarity,
202
+ metadata: chunk.metadata,
203
+ knowledgeId: knowledgeVector.knowledgeId,
204
+ chunkId: chunk.metadata?.id || `${knowledgeVector.knowledgeId}_chunk_${Date.now()}`
205
+ });
206
+ }
207
+ }
208
+
209
+ return candidates;
210
+ });
211
+
212
+ // Wait for all searches to complete
213
+ const allCandidateArrays = await Promise.all(searchPromises);
214
+
215
+ // Flatten results
216
+ for (const candidates of allCandidateArrays) {
217
+ allCandidates.push(...candidates);
218
+ }
219
+
220
+ // Sort by similarity score and take top results
221
+ const results = allCandidates
222
+ .sort((a, b) => b.score - a.score)
223
+ .slice(0, maxResults);
224
+
225
+ const searchTime = Date.now() - startTime;
226
+
227
+ return {
228
+ results,
229
+ searchTime,
230
+ totalCandidates
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Clear query embedding cache
236
+ */
237
+ clearCache(): void {
238
+ this.queryEmbeddingCache.clear();
239
+ }
240
+
241
+ /**
242
+ * Get cache statistics
243
+ */
244
+ getCacheStats(): { size: number; maxSize: number } {
245
+ return {
246
+ size: this.queryEmbeddingCache.size,
247
+ maxSize: this.maxCacheSize
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Search similar documents (compatibility method from LangChainVectorStoreService)
253
+ */
254
+ async searchSimilarCompat(request: IVectorSearchRequest, knowledgeVectors: Array<{
255
+ knowledgeId: string | number;
256
+ chunks: Array<{
257
+ content: string;
258
+ embedding: number[];
259
+ metadata: any;
260
+ }>;
261
+ }>): Promise<IVectorSearchResponse> {
262
+ try {
263
+ const {
264
+ query,
265
+ maxResults = 10,
266
+ similarityThreshold = 0.1, // Updated to match other defaults
267
+ filter,
268
+ } = request;
269
+
270
+ // Filter knowledge vectors if needed
271
+ let filteredVectors = knowledgeVectors;
272
+ if (filter) {
273
+ filteredVectors = knowledgeVectors.filter(vector => {
274
+ // Check knowledge IDs
275
+ if (filter.knowledgeIds && filter.knowledgeIds.length > 0) {
276
+ return filter.knowledgeIds.includes(String(vector.knowledgeId));
277
+ }
278
+ return true;
279
+ });
280
+ }
281
+
282
+ // Use our optimized search
283
+ const searchResponse = await this.searchSimilar({
284
+ query,
285
+ knowledgeVectors: filteredVectors,
286
+ maxResults,
287
+ threshold: similarityThreshold
288
+ });
289
+
290
+ // Convert to IVectorSearchResponse format
291
+ const results: ISearchResult[] = searchResponse.results.map(result => ({
292
+ content: result.content,
293
+ score: result.score,
294
+ metadata: result.metadata,
295
+ chunkId: result.chunkId
296
+ }));
297
+
298
+ return {
299
+ results,
300
+ totalResults: results.length,
301
+ };
302
+ } catch (error) {
303
+ console.error('Failed to search similar documents:', error);
304
+ return {
305
+ results: [],
306
+ totalResults: 0,
307
+ };
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Get search statistics
313
+ */
314
+ getStats(knowledgeVectors: Array<{
315
+ knowledgeId: string | number;
316
+ chunks: Array<{ content: string; embedding: number[]; metadata: any; }>;
317
+ }>): { totalChunks: number; totalKnowledge: number } {
318
+ const totalChunks = knowledgeVectors.reduce((total, vector) => total + vector.chunks.length, 0);
319
+ return {
320
+ totalChunks,
321
+ totalKnowledge: knowledgeVectors.length,
322
+ };
323
+ }
324
+ }