@retrivora-ai/rag-engine 0.4.3 → 0.4.4
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.
- package/dist/{DocumentChunker-BICIjSuG.d.mts → DocumentChunker-3yElxTO3.d.mts} +9 -2
- package/dist/{DocumentChunker-BICIjSuG.d.ts → DocumentChunker-3yElxTO3.d.ts} +9 -2
- package/dist/{RagConfig-CVt24lbC.d.mts → RagConfig-BgRDL9Vy.d.mts} +37 -1
- package/dist/{RagConfig-CVt24lbC.d.ts → RagConfig-BgRDL9Vy.d.ts} +37 -1
- package/dist/SimpleGraphProvider-M6T7SE7D.mjs +62 -0
- package/dist/{chunk-UUZ3F4WK.mjs → chunk-OKY5P6RA.mjs} +179 -40
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +254 -40
- package/dist/handlers/index.mjs +1 -1
- package/dist/{index-OI2--lvT.d.mts → index-7qeLTPBL.d.mts} +1 -1
- package/dist/{index-D1hoNXMT.d.ts → index-DowY4_K0.d.ts} +1 -1
- package/dist/index.d.mts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.js +161 -1
- package/dist/index.mjs +158 -1
- package/dist/server.d.mts +52 -12
- package/dist/server.d.ts +52 -12
- package/dist/server.js +254 -40
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/components/ConfigProvider.tsx +1 -0
- package/src/components/DocumentUpload.tsx +192 -0
- package/src/config/RagConfig.ts +27 -0
- package/src/config/constants.ts +7 -0
- package/src/config/serverConfig.ts +1 -0
- package/src/core/Pipeline.ts +71 -10
- package/src/core/ProviderRegistry.ts +40 -8
- package/src/index.ts +1 -0
- package/src/providers/graphdb/BaseGraphProvider.ts +43 -0
- package/src/providers/graphdb/SimpleGraphProvider.ts +66 -0
- package/src/rag/DocumentChunker.ts +77 -34
- package/src/rag/EntityExtractor.ts +43 -0
- package/src/types/index.ts +19 -0
package/src/config/constants.ts
CHANGED
|
@@ -39,6 +39,13 @@ export const EMBEDDING_PROVIDERS = [
|
|
|
39
39
|
'custom',
|
|
40
40
|
] as const;
|
|
41
41
|
|
|
42
|
+
export const GRAPH_DB_PROVIDERS = [
|
|
43
|
+
'neo4j',
|
|
44
|
+
'memgraph',
|
|
45
|
+
'simple',
|
|
46
|
+
'custom',
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
42
49
|
export const PROVIDERS_WITH_EMBEDDINGS = [
|
|
43
50
|
'openai',
|
|
44
51
|
'ollama',
|
|
@@ -169,6 +169,7 @@ export function getRagConfig(env: Record<string, string | undefined> = process.e
|
|
|
169
169
|
"Hello! I'm your AI assistant. Ask me anything about your documents.",
|
|
170
170
|
visualStyle: (readString(env, 'NEXT_PUBLIC_UI_VISUAL_STYLE') ?? readString(env, 'UI_VISUAL_STYLE') ?? 'glass') as 'glass' | 'solid',
|
|
171
171
|
borderRadius: (readString(env, 'NEXT_PUBLIC_UI_BORDER_RADIUS') ?? readString(env, 'UI_BORDER_RADIUS') ?? 'xl') as 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full',
|
|
172
|
+
allowUpload: (readString(env, 'NEXT_PUBLIC_ALLOW_UPLOAD') ?? readString(env, 'UI_ALLOW_UPLOAD') ?? 'false') === 'true',
|
|
172
173
|
},
|
|
173
174
|
rag: {
|
|
174
175
|
topK: readNumber(env, 'RAG_TOP_K', 5),
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { BaseVectorProvider } from '../providers/vectordb/BaseVectorProvider';
|
|
2
|
+
import { BaseGraphProvider } from '../providers/graphdb/BaseGraphProvider';
|
|
2
3
|
import { ILLMProvider, ChatMessage } from '../llm/ILLMProvider';
|
|
3
4
|
import { DocumentChunker } from '../rag/DocumentChunker';
|
|
5
|
+
import { EntityExtractor } from '../rag/EntityExtractor';
|
|
4
6
|
import { RagConfig } from '../config/RagConfig';
|
|
5
7
|
import { ProviderRegistry } from './ProviderRegistry';
|
|
6
8
|
import { BatchProcessor, BatchOptions } from './BatchProcessor';
|
|
7
9
|
import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
|
|
8
|
-
import { IngestDocument, ChatResponse } from '../types';
|
|
10
|
+
import { IngestDocument, ChatResponse, GraphSearchResult } from '../types';
|
|
9
11
|
|
|
10
12
|
interface QueryFieldHint {
|
|
11
13
|
field?: string;
|
|
@@ -171,9 +173,11 @@ function buildQueryFilter(question: string, hints: QueryFieldHint[]): QueryFilte
|
|
|
171
173
|
*/
|
|
172
174
|
export class Pipeline {
|
|
173
175
|
private vectorDB!: BaseVectorProvider;
|
|
176
|
+
private graphDB?: BaseGraphProvider;
|
|
174
177
|
private llmProvider!: ILLMProvider;
|
|
175
178
|
private embeddingProvider!: ILLMProvider;
|
|
176
179
|
private chunker: DocumentChunker;
|
|
180
|
+
private entityExtractor?: EntityExtractor;
|
|
177
181
|
private config: RagConfig;
|
|
178
182
|
private initialised = false;
|
|
179
183
|
|
|
@@ -202,6 +206,12 @@ export class Pipeline {
|
|
|
202
206
|
this.llmProvider = llmProvider;
|
|
203
207
|
this.embeddingProvider = embeddingProvider;
|
|
204
208
|
|
|
209
|
+
if (this.config.graphDb) {
|
|
210
|
+
this.graphDB = await ProviderRegistry.createGraphProvider(this.config.graphDb);
|
|
211
|
+
await this.graphDB.initialize();
|
|
212
|
+
this.entityExtractor = new EntityExtractor(this.llmProvider);
|
|
213
|
+
}
|
|
214
|
+
|
|
205
215
|
await this.vectorDB.initialize();
|
|
206
216
|
this.initialised = true;
|
|
207
217
|
}
|
|
@@ -270,6 +280,16 @@ export class Pipeline {
|
|
|
270
280
|
docId: doc.docId,
|
|
271
281
|
chunksIngested: upsertResult.totalProcessed,
|
|
272
282
|
});
|
|
283
|
+
|
|
284
|
+
// Graph ingestion
|
|
285
|
+
if (this.graphDB && this.entityExtractor) {
|
|
286
|
+
console.log(`[Pipeline] Extracting entities for doc ${doc.docId}...`);
|
|
287
|
+
for (const chunk of chunks) {
|
|
288
|
+
const { nodes, edges } = await this.entityExtractor.extract(chunk.content);
|
|
289
|
+
if (nodes.length > 0) await this.graphDB.addNodes(nodes);
|
|
290
|
+
if (edges.length > 0) await this.graphDB.addEdges(edges);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
273
293
|
} catch (error) {
|
|
274
294
|
console.error(`[Pipeline] Failed to ingest document ${doc.docId}:`, error);
|
|
275
295
|
results.push({ docId: doc.docId, chunksIngested: 0 });
|
|
@@ -286,29 +306,70 @@ export class Pipeline {
|
|
|
286
306
|
const scoreThreshold = this.config.rag?.scoreThreshold ?? 0.0;
|
|
287
307
|
|
|
288
308
|
try {
|
|
289
|
-
|
|
309
|
+
let searchQuery = question;
|
|
310
|
+
|
|
311
|
+
// 1. Query Transformation
|
|
312
|
+
if (this.config.rag?.useQueryTransformation) {
|
|
313
|
+
searchQuery = await this.rewriteQuery(question, history);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 2. Vector Retrieval
|
|
317
|
+
const queryVector = await this.embeddingProvider.embed(searchQuery, { taskType: 'query' });
|
|
290
318
|
const fieldHints = extractQueryFieldHints(question);
|
|
291
|
-
// Build a provider-agnostic filter from hints + original query text. Providers
|
|
292
|
-
// may choose to use `queryText` for hybrid keyword matching and `metadata`
|
|
293
|
-
// for exact/filtered metadata queries.
|
|
294
319
|
const filter = buildQueryFilter(question, fieldHints) as Record<string, unknown>;
|
|
295
|
-
|
|
296
|
-
// Keep the raw hints available as well for providers that want richer parsing
|
|
297
320
|
filter.__entityHints = fieldHints;
|
|
298
321
|
|
|
299
322
|
const rawMatches = await this.vectorDB.query(queryVector, topK, ns, filter);
|
|
300
|
-
|
|
301
323
|
const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
|
|
302
|
-
|
|
324
|
+
|
|
325
|
+
// 3. Graph Retrieval
|
|
326
|
+
let graphData: GraphSearchResult | undefined;
|
|
327
|
+
if (this.graphDB && this.config.rag?.useGraphRetrieval) {
|
|
328
|
+
graphData = await this.graphDB.query(searchQuery);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 4. Context Augmentation
|
|
332
|
+
let context = sources.length
|
|
303
333
|
? sources.map((m, i) => `[Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n')
|
|
304
334
|
: 'No relevant context found.';
|
|
305
335
|
|
|
336
|
+
if (graphData && graphData.nodes.length > 0) {
|
|
337
|
+
const graphContext = graphData.nodes.map(n =>
|
|
338
|
+
`Entity: ${n.label} (${n.id})${n.properties ? ' - ' + JSON.stringify(n.properties) : ''}`
|
|
339
|
+
).join('\n');
|
|
340
|
+
context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 5. Generation
|
|
306
344
|
const messages: ChatMessage[] = [...history, { role: 'user', content: question }];
|
|
307
345
|
const reply = await this.llmProvider.chat(messages, context);
|
|
308
346
|
|
|
309
|
-
return { reply, sources };
|
|
347
|
+
return { reply, sources, graphData };
|
|
310
348
|
} catch (error) {
|
|
311
349
|
throw new Error(`[Pipeline] Chat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
312
350
|
}
|
|
313
351
|
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Rewrite the user query for better retrieval performance.
|
|
355
|
+
*/
|
|
356
|
+
private async rewriteQuery(question: string, history: ChatMessage[]): Promise<string> {
|
|
357
|
+
const prompt = `
|
|
358
|
+
Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
359
|
+
Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
|
|
360
|
+
|
|
361
|
+
History:
|
|
362
|
+
${history.map(m => `${m.role}: ${m.content}`).join('\n')}
|
|
363
|
+
|
|
364
|
+
New Question: ${question}
|
|
365
|
+
|
|
366
|
+
Optimized Search Query:`;
|
|
367
|
+
|
|
368
|
+
const rewrite = await this.llmProvider.chat([
|
|
369
|
+
{ role: 'system', content: 'You are an assistant that optimizes search queries for RAG systems.' },
|
|
370
|
+
{ role: 'user', content: prompt }
|
|
371
|
+
], '');
|
|
372
|
+
|
|
373
|
+
return rewrite.trim() || question;
|
|
374
|
+
}
|
|
314
375
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseVectorProvider } from '../providers/vectordb/BaseVectorProvider';
|
|
2
|
-
import {
|
|
2
|
+
import { BaseGraphProvider } from '@/providers/graphdb/BaseGraphProvider';
|
|
3
|
+
import { VectorDBConfig, GraphDBConfig, LLMConfig, EmbeddingConfig } from '../config/RagConfig';
|
|
3
4
|
import { LLMFactory } from '../llm/LLMFactory';
|
|
4
5
|
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
5
6
|
|
|
@@ -12,18 +13,19 @@ import { ILLMProvider } from '../llm/ILLMProvider';
|
|
|
12
13
|
*/
|
|
13
14
|
export class ProviderRegistry {
|
|
14
15
|
private static vectorProviders: Record<string, new (config: VectorDBConfig) => BaseVectorProvider> = {};
|
|
16
|
+
private static graphProviders: Record<string, new (config: GraphDBConfig) => BaseGraphProvider> = {};
|
|
15
17
|
|
|
16
|
-
/**
|
|
17
|
-
* Register a custom vector provider class by name.
|
|
18
|
-
* The name must match the provider value used in VectorDBConfig.provider.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ProviderRegistry.registerVectorProvider('my-db', MyCustomProvider);
|
|
22
|
-
*/
|
|
23
18
|
static registerVectorProvider(name: string, providerClass: new (config: VectorDBConfig) => BaseVectorProvider) {
|
|
24
19
|
this.vectorProviders[name] = providerClass;
|
|
25
20
|
}
|
|
26
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Register a custom graph provider class by name.
|
|
24
|
+
*/
|
|
25
|
+
static registerGraphProvider(name: string, providerClass: new (config: GraphDBConfig) => BaseGraphProvider) {
|
|
26
|
+
this.graphProviders[name] = providerClass;
|
|
27
|
+
}
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* Creates a vector database provider based on the configuration.
|
|
29
31
|
* Built-in providers are dynamically imported to avoid bundling all SDKs.
|
|
@@ -85,6 +87,36 @@ export class ProviderRegistry {
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Creates a graph database provider based on the configuration.
|
|
92
|
+
*/
|
|
93
|
+
static async createGraphProvider(config: GraphDBConfig): Promise<BaseGraphProvider> {
|
|
94
|
+
const { provider } = config;
|
|
95
|
+
|
|
96
|
+
// Custom registered provider takes priority
|
|
97
|
+
if (this.graphProviders[provider]) {
|
|
98
|
+
return new this.graphProviders[provider](config);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Built-in providers — lazy-loaded
|
|
102
|
+
switch (provider) {
|
|
103
|
+
case 'neo4j': {
|
|
104
|
+
// Placeholder for real Neo4j provider
|
|
105
|
+
throw new Error('[ProviderRegistry] Neo4j provider not implemented yet.');
|
|
106
|
+
}
|
|
107
|
+
case 'simple': {
|
|
108
|
+
const { SimpleGraphProvider } = await import('@/providers/graphdb/SimpleGraphProvider');
|
|
109
|
+
return new SimpleGraphProvider(config);
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(
|
|
113
|
+
`[ProviderRegistry] Unsupported graph provider: "${provider}". ` +
|
|
114
|
+
`Built-in providers: simple. ` +
|
|
115
|
+
`For custom providers, call ProviderRegistry.registerGraphProvider("${provider}", YourClass).`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
88
120
|
/**
|
|
89
121
|
* Creates an LLM provider based on the configuration.
|
|
90
122
|
*/
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// ── React Components ─────────────────────────────────────────
|
|
7
7
|
export { ChatWidget } from './components/ChatWidget';
|
|
8
8
|
export { ChatWindow } from './components/ChatWindow';
|
|
9
|
+
export { DocumentUpload } from './components/DocumentUpload';
|
|
9
10
|
export { MessageBubble } from './components/MessageBubble';
|
|
10
11
|
export { SourceCard } from './components/SourceCard';
|
|
11
12
|
export { ConfigProvider, useConfig } from './components/ConfigProvider';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { GraphDBConfig } from '../../config/RagConfig';
|
|
2
|
+
import { GraphNode, Edge, GraphSearchResult } from '../../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BaseGraphProvider — Abstract base class for Graph DB providers.
|
|
6
|
+
*/
|
|
7
|
+
export abstract class BaseGraphProvider {
|
|
8
|
+
protected config: GraphDBConfig;
|
|
9
|
+
|
|
10
|
+
constructor(config: GraphDBConfig) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialise connection to the graph database.
|
|
16
|
+
*/
|
|
17
|
+
abstract initialize(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add nodes to the graph.
|
|
21
|
+
*/
|
|
22
|
+
abstract addNodes(nodes: GraphNode[]): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add edges (relationships) to the graph.
|
|
26
|
+
*/
|
|
27
|
+
abstract addEdges(edges: Edge[]): Promise<void>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Query the graph for relevant nodes and edges.
|
|
31
|
+
*/
|
|
32
|
+
abstract query(queryText: string, limit?: number): Promise<GraphSearchResult>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if the database is reachable.
|
|
36
|
+
*/
|
|
37
|
+
abstract ping(): Promise<boolean>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Close connection.
|
|
41
|
+
*/
|
|
42
|
+
abstract disconnect(): Promise<void>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { BaseGraphProvider } from './BaseGraphProvider';
|
|
2
|
+
import { GraphNode, Edge, GraphSearchResult } from '../../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SimpleGraphProvider — In-memory graph storage for demonstration and local dev.
|
|
6
|
+
*/
|
|
7
|
+
export class SimpleGraphProvider extends BaseGraphProvider {
|
|
8
|
+
private nodes: Map<string, GraphNode> = new Map();
|
|
9
|
+
private edges: Edge[] = [];
|
|
10
|
+
|
|
11
|
+
async initialize(): Promise<void> {
|
|
12
|
+
console.log('[SimpleGraphProvider] Initialised in-memory graph store.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async addNodes(nodes: GraphNode[]): Promise<void> {
|
|
16
|
+
for (const node of nodes) {
|
|
17
|
+
this.nodes.set(node.id, node);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async addEdges(edges: Edge[]): Promise<void> {
|
|
22
|
+
this.edges.push(...edges);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async query(queryText: string, limit = 5): Promise<GraphSearchResult> {
|
|
26
|
+
const q = queryText.toLowerCase();
|
|
27
|
+
|
|
28
|
+
// Simple keyword matching for demonstration
|
|
29
|
+
const matchedNodes = Array.from(this.nodes.values()).filter(node =>
|
|
30
|
+
node.id.toLowerCase().includes(q) ||
|
|
31
|
+
node.label.toLowerCase().includes(q) ||
|
|
32
|
+
JSON.stringify(node.properties).toLowerCase().includes(q)
|
|
33
|
+
).slice(0, limit);
|
|
34
|
+
|
|
35
|
+
const matchedNodeIds = new Set(matchedNodes.map(n => n.id));
|
|
36
|
+
const matchedEdges = this.edges.filter(edge =>
|
|
37
|
+
matchedNodeIds.has(edge.source) || matchedNodeIds.has(edge.target)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Also include connected nodes
|
|
41
|
+
for (const edge of matchedEdges) {
|
|
42
|
+
if (!matchedNodeIds.has(edge.source)) {
|
|
43
|
+
const source = this.nodes.get(edge.source);
|
|
44
|
+
if (source) matchedNodes.push(source);
|
|
45
|
+
}
|
|
46
|
+
if (!matchedNodeIds.has(edge.target)) {
|
|
47
|
+
const target = this.nodes.get(edge.target);
|
|
48
|
+
if (target) matchedNodes.push(target);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
nodes: Array.from(new Set(matchedNodes)),
|
|
54
|
+
edges: matchedEdges
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async ping(): Promise<boolean> {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async disconnect(): Promise<void> {
|
|
63
|
+
this.nodes.clear();
|
|
64
|
+
this.edges = [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -23,19 +23,23 @@ export interface ChunkOptions {
|
|
|
23
23
|
docId?: string | number;
|
|
24
24
|
/** Extra metadata to attach to every chunk */
|
|
25
25
|
metadata?: Record<string, unknown>;
|
|
26
|
+
/** Characters used to split text, in order of priority */
|
|
27
|
+
separators?: string[];
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export class DocumentChunker {
|
|
29
31
|
private readonly chunkSize: number;
|
|
30
32
|
private readonly chunkOverlap: number;
|
|
33
|
+
private readonly separators: string[];
|
|
31
34
|
|
|
32
|
-
constructor(chunkSize = 1000, chunkOverlap = 200) {
|
|
35
|
+
constructor(chunkSize = 1000, chunkOverlap = 200, separators = ['\n\n', '\n', ' ', '']) {
|
|
33
36
|
this.chunkSize = chunkSize;
|
|
34
37
|
this.chunkOverlap = chunkOverlap;
|
|
38
|
+
this.separators = separators;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
* Split a single text string into overlapping chunks.
|
|
42
|
+
* Split a single text string into overlapping chunks using a recursive strategy.
|
|
39
43
|
*/
|
|
40
44
|
chunk(text: string, options: ChunkOptions = {}): Chunk[] {
|
|
41
45
|
const {
|
|
@@ -43,53 +47,92 @@ export class DocumentChunker {
|
|
|
43
47
|
chunkOverlap = this.chunkOverlap,
|
|
44
48
|
docId = `doc_${Date.now()}`,
|
|
45
49
|
metadata = {},
|
|
50
|
+
separators = this.separators,
|
|
46
51
|
} = options;
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const sentences = cleaned
|
|
54
|
-
.split(/(?<=[.!?])\s+|\n{2,}/)
|
|
55
|
-
.map((s) => s.trim())
|
|
56
|
-
.filter(Boolean);
|
|
57
|
-
|
|
58
|
-
const chunks: Chunk[] = [];
|
|
59
|
-
let current = '';
|
|
53
|
+
const finalChunks: Chunk[] = [];
|
|
54
|
+
const splits = this.recursiveSplit(text, separators, chunkSize);
|
|
55
|
+
|
|
56
|
+
let currentChunk: string[] = [];
|
|
57
|
+
let currentLength = 0;
|
|
60
58
|
let chunkIndex = 0;
|
|
61
59
|
|
|
62
|
-
for (const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
metadata: { ...metadata, docId, chunkIndex: chunkIndex - 1 },
|
|
71
|
-
});
|
|
72
|
-
}
|
|
60
|
+
for (const split of splits) {
|
|
61
|
+
if (currentLength + split.length > chunkSize && currentChunk.length > 0) {
|
|
62
|
+
// Current chunk is full, push it
|
|
63
|
+
finalChunks.push({
|
|
64
|
+
id: `${docId}_chunk_${chunkIndex++}`,
|
|
65
|
+
content: currentChunk.join('').trim(),
|
|
66
|
+
metadata: { ...metadata, docId, chunkIndex: chunkIndex - 1 },
|
|
67
|
+
});
|
|
73
68
|
|
|
74
|
-
// Start new chunk with overlap
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
// Start new chunk with overlap
|
|
70
|
+
// This is a simplified overlap: we keep items from the end of the previous chunk
|
|
71
|
+
// until we reach the overlap size.
|
|
72
|
+
const overlapItems = [];
|
|
73
|
+
let overlapLen = 0;
|
|
74
|
+
for (let i = currentChunk.length - 1; i >= 0; i--) {
|
|
75
|
+
if (overlapLen + currentChunk[i].length <= chunkOverlap) {
|
|
76
|
+
overlapItems.unshift(currentChunk[i]);
|
|
77
|
+
overlapLen += currentChunk[i].length;
|
|
78
|
+
} else {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
79
81
|
}
|
|
82
|
+
currentChunk = overlapItems;
|
|
83
|
+
currentLength = overlapLen;
|
|
80
84
|
}
|
|
85
|
+
|
|
86
|
+
currentChunk.push(split);
|
|
87
|
+
currentLength += split.length;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
// Push the last remaining chunk
|
|
84
|
-
if (
|
|
85
|
-
|
|
91
|
+
if (currentChunk.length > 0) {
|
|
92
|
+
finalChunks.push({
|
|
86
93
|
id: `${docId}_chunk_${chunkIndex}`,
|
|
87
|
-
content:
|
|
94
|
+
content: currentChunk.join('').trim(),
|
|
88
95
|
metadata: { ...metadata, docId, chunkIndex },
|
|
89
96
|
});
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
return
|
|
99
|
+
return finalChunks;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Recursively split text based on separators.
|
|
104
|
+
*/
|
|
105
|
+
private recursiveSplit(text: string, separators: string[], chunkSize: number): string[] {
|
|
106
|
+
const finalSplits: string[] = [];
|
|
107
|
+
|
|
108
|
+
// Find the first separator that exists in the text
|
|
109
|
+
let separator = separators[separators.length - 1]; // default to last (usually empty string)
|
|
110
|
+
let nextSeparators: string[] = [];
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < separators.length; i++) {
|
|
113
|
+
if (text.includes(separators[i])) {
|
|
114
|
+
separator = separators[i];
|
|
115
|
+
nextSeparators = separators.slice(i + 1);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Split the text
|
|
121
|
+
const parts = text.split(separator);
|
|
122
|
+
|
|
123
|
+
for (const part of parts) {
|
|
124
|
+
if (part.length <= chunkSize) {
|
|
125
|
+
finalSplits.push(part + separator);
|
|
126
|
+
} else if (nextSeparators.length > 0) {
|
|
127
|
+
// Part is too long, try splitting with next level separators
|
|
128
|
+
finalSplits.push(...this.recursiveSplit(part, nextSeparators, chunkSize));
|
|
129
|
+
} else {
|
|
130
|
+
// No more separators, just hard split (should not happen if empty string is the last separator)
|
|
131
|
+
finalSplits.push(part);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return finalSplits;
|
|
93
136
|
}
|
|
94
137
|
|
|
95
138
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
2
|
+
import { GraphNode, Edge } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EntityExtractor — Uses an LLM to extract entities and relations from text.
|
|
6
|
+
*/
|
|
7
|
+
export class EntityExtractor {
|
|
8
|
+
private llm: ILLMProvider;
|
|
9
|
+
|
|
10
|
+
constructor(llm: ILLMProvider) {
|
|
11
|
+
this.llm = llm;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract nodes and edges from a text chunk.
|
|
16
|
+
*/
|
|
17
|
+
async extract(text: string): Promise<{ nodes: GraphNode[]; edges: Edge[] }> {
|
|
18
|
+
const prompt = `
|
|
19
|
+
Extract entities and relationships from the following text.
|
|
20
|
+
Format the output as JSON with two keys: "nodes" (array of {id, label, properties}) and "edges" (array of {source, target, type, properties}).
|
|
21
|
+
Use the same ID for the same entity.
|
|
22
|
+
|
|
23
|
+
Text:
|
|
24
|
+
"${text}"
|
|
25
|
+
|
|
26
|
+
Output JSON:
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const response = await this.llm.chat([
|
|
30
|
+
{ role: 'system', content: 'You are an expert at knowledge graph extraction. Respond only with valid JSON.' },
|
|
31
|
+
{ role: 'user', content: prompt }
|
|
32
|
+
], '');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Clean up LLM response (sometimes it adds markdown blocks)
|
|
36
|
+
const cleanJson = response.replace(/```json\n?|\n?```/g, '').trim();
|
|
37
|
+
return JSON.parse(cleanJson);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn('[EntityExtractor] Failed to parse LLM response as JSON:', response);
|
|
40
|
+
return { nodes: [], edges: [] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -21,4 +21,23 @@ export interface IngestDocument {
|
|
|
21
21
|
export interface ChatResponse {
|
|
22
22
|
reply: string;
|
|
23
23
|
sources: VectorMatch[];
|
|
24
|
+
graphData?: GraphSearchResult;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GraphNode {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
30
|
+
properties?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Edge {
|
|
34
|
+
source: string;
|
|
35
|
+
target: string;
|
|
36
|
+
type: string;
|
|
37
|
+
properties?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GraphSearchResult {
|
|
41
|
+
nodes: GraphNode[];
|
|
42
|
+
edges: Edge[];
|
|
24
43
|
}
|