@retrivora-ai/rag-engine 2.0.2 → 2.0.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/FREE_TIER_ARCHITECTURE.md +232 -0
- package/README.md +9 -0
- package/dist/{ILLMProvider-DMxLyTdq.d.mts → ILLMProvider-0rRBYbVW.d.mts} +133 -1
- package/dist/{ILLMProvider-DMxLyTdq.d.ts → ILLMProvider-0rRBYbVW.d.ts} +133 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +681 -17
- package/dist/handlers/index.mjs +681 -17
- package/dist/index-B1wGUlSL.d.mts +532 -0
- package/dist/{index-tzwc7UhY.d.ts → index-BIHHp_f6.d.ts} +1 -1
- package/dist/{index-DHsFLJXQ.d.mts → index-BvODr57d.d.mts} +1 -1
- package/dist/index-DcklhThn.d.ts +532 -0
- package/dist/index.css +51 -3
- package/dist/index.d.mts +6 -105
- package/dist/index.d.ts +6 -105
- package/dist/index.js +1918 -8
- package/dist/index.mjs +1913 -8
- package/dist/server.d.mts +51 -7
- package/dist/server.d.ts +51 -7
- package/dist/server.js +723 -17
- package/dist/server.mjs +706 -17
- package/package.json +9 -7
- package/src/app/page.tsx +54 -18
- package/src/components/ChatWindow.tsx +2 -2
- package/src/components/DocumentUpload.tsx +5 -4
- package/src/components/MessageBubble.tsx +2 -4
- package/src/core/DatabaseStorage.ts +5 -1
- package/src/core/LicenseVerifier.ts +1 -1
- package/src/core/Pipeline.ts +23 -5
- package/src/handlers/index.ts +48 -1
- package/src/index.ts +22 -4
- package/src/rendering/IntentClassifier.ts +167 -0
- package/src/rendering/RendererRegistry.ts +164 -0
- package/src/rendering/RuleEngine.ts +67 -0
- package/src/rendering/VisualizationDecisionEngine.ts +164 -0
- package/src/rendering/__tests__/VisualizationDecisionEngine.test.ts +237 -0
- package/src/rendering/index.ts +12 -0
- package/src/rendering/rules/Rule1SpecificInfoRule.ts +28 -0
- package/src/rendering/rules/Rule2ComparisonRule.ts +24 -0
- package/src/rendering/rules/Rule3ProductDiscoveryRule.ts +28 -0
- package/src/rendering/rules/Rule4AnalyticalRule.ts +38 -0
- package/src/rendering/rules/Rule5MixedResponseRule.ts +29 -0
- package/src/rendering/rules/Rule6SmallResultSetRule.ts +24 -0
- package/src/rendering/rules/Rule7LargeTableRule.ts +35 -0
- package/src/rendering/types.ts +96 -0
- package/src/server.ts +3 -2
- package/src/types/index.ts +48 -0
- package/src/utils/DocumentParser.ts +64 -28
- package/src/utils/UITransformer.ts +15 -4
- package/dist/index-CfkqZd2Y.d.ts +0 -197
- package/dist/index-xygonxpW.d.mts +0 -197
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Rule2ComparisonRule implements IRenderRule {
|
|
4
|
+
readonly name = 'Rule2Comparison';
|
|
5
|
+
readonly priority = 90;
|
|
6
|
+
|
|
7
|
+
evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
|
|
8
|
+
const q = (context.userQuery || '').toLowerCase().trim();
|
|
9
|
+
|
|
10
|
+
const isComparison =
|
|
11
|
+
intent === 'comparison' ||
|
|
12
|
+
/\b(compare|comparison|vs\.?|versus|difference between|differ|contrast|against)\b/i.test(q);
|
|
13
|
+
|
|
14
|
+
if (isComparison) {
|
|
15
|
+
return {
|
|
16
|
+
renderType: 'table',
|
|
17
|
+
confidence: 0.90,
|
|
18
|
+
reason: 'Rule 2: Comparison query contrasting multiple entities requires structured table rendering.',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Rule3ProductDiscoveryRule implements IRenderRule {
|
|
4
|
+
readonly name = 'Rule3ProductDiscovery';
|
|
5
|
+
readonly priority = 85;
|
|
6
|
+
|
|
7
|
+
evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
|
|
8
|
+
const q = (context.userQuery || '').toLowerCase().trim();
|
|
9
|
+
|
|
10
|
+
const isDiscovery =
|
|
11
|
+
intent === 'product_search' ||
|
|
12
|
+
intent === 'recommendation' ||
|
|
13
|
+
/\b(best|recommend|top|buy|shopping|under \$|cheap|shoes|laptops|chairs|headphones|sneakers|apparel|products|show available)\b/i.test(q);
|
|
14
|
+
|
|
15
|
+
const isNotChart = !/\b(chart|sales|revenue|monthly|growth|trend|over time)\b/i.test(q);
|
|
16
|
+
const isAnalyticalRanking = /\b(top performing|top selling|best performing|performing|sales|revenue|ranking)\b/i.test(q);
|
|
17
|
+
|
|
18
|
+
if (isDiscovery && isNotChart && !isAnalyticalRanking) {
|
|
19
|
+
return {
|
|
20
|
+
renderType: 'carousel',
|
|
21
|
+
confidence: 0.90,
|
|
22
|
+
reason: 'Rule 3: Product discovery/recommendation query requires product carousel rendering.',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ChartType, DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Rule4AnalyticalRule implements IRenderRule {
|
|
4
|
+
readonly name = 'Rule4Analytical';
|
|
5
|
+
readonly priority = 80;
|
|
6
|
+
|
|
7
|
+
evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
|
|
8
|
+
const q = (context.userQuery || '').toLowerCase().trim();
|
|
9
|
+
|
|
10
|
+
const isAnalytical =
|
|
11
|
+
intent === 'analytics' ||
|
|
12
|
+
intent === 'trend_analysis' ||
|
|
13
|
+
intent === 'ranking' ||
|
|
14
|
+
/\b(monthly sales|revenue growth|top performing|population trends|sales by|growth|distribution|share|percentage|breakdown|correlation|scatter|trend)\b/i.test(q);
|
|
15
|
+
|
|
16
|
+
if (!isAnalytical) return null;
|
|
17
|
+
|
|
18
|
+
// Determine specific chart type based on query intent signals
|
|
19
|
+
let chartType: ChartType = 'bar'; // Default category comparison
|
|
20
|
+
|
|
21
|
+
if (intent === 'trend_analysis' || /\b(monthly|yearly|weekly|growth|over time|trend|timeline|historical)\b/i.test(q)) {
|
|
22
|
+
chartType = 'line';
|
|
23
|
+
} else if (/\b(share|percentage|percent|breakup|breakdown|composition|pie|donut|proportion)\b/i.test(q)) {
|
|
24
|
+
chartType = 'pie';
|
|
25
|
+
} else if (/\b(correlation|scatter|relationship|relation|impact of|depends on)\b/i.test(q)) {
|
|
26
|
+
chartType = 'scatter';
|
|
27
|
+
} else {
|
|
28
|
+
chartType = 'bar';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
renderType: 'chart',
|
|
33
|
+
chartType,
|
|
34
|
+
confidence: 0.85,
|
|
35
|
+
reason: `Rule 4: Analytical query with numerical insights selects ${chartType} chart.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Rule5MixedResponseRule implements IRenderRule {
|
|
4
|
+
readonly name = 'Rule5MixedResponse';
|
|
5
|
+
readonly priority = 95;
|
|
6
|
+
|
|
7
|
+
evaluate(context: DecisionContext, _intent: IntentCategory): RenderDecision | null {
|
|
8
|
+
const q = (context.userQuery || '').toLowerCase().trim();
|
|
9
|
+
|
|
10
|
+
// Specific mixed intent match: "top selling products this month" / multi-section requests
|
|
11
|
+
const isTopSellingThisMonth = /\btop\s*(selling|performing)?\s*products?\s*(this|last)?\s*(month|year|week)\b/i.test(q);
|
|
12
|
+
const hasMultipleSectionsRequest = /\b(summary|overview)\b/i.test(q) && /\b(carousel|products?|items?)\b/i.test(q) && /\b(chart|sales|trend|revenue)\b/i.test(q);
|
|
13
|
+
|
|
14
|
+
if (isTopSellingThisMonth || hasMultipleSectionsRequest) {
|
|
15
|
+
return {
|
|
16
|
+
renderType: 'mixed',
|
|
17
|
+
confidence: 0.95,
|
|
18
|
+
reason: 'Rule 5: Mixed response query requires multi-section composition (text, carousel, chart).',
|
|
19
|
+
sections: [
|
|
20
|
+
{ type: 'text', title: 'Summary' },
|
|
21
|
+
{ type: 'carousel', title: 'Top Products' },
|
|
22
|
+
{ type: 'chart', chartType: 'bar', title: 'Sales Performance' },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Rule6SmallResultSetRule implements IRenderRule {
|
|
4
|
+
readonly name = 'Rule6SmallResultSet';
|
|
5
|
+
readonly priority = 110;
|
|
6
|
+
|
|
7
|
+
evaluate(context: DecisionContext, _intent: IntentCategory): RenderDecision | null {
|
|
8
|
+
const docs = context.retrievedDocuments || [];
|
|
9
|
+
|
|
10
|
+
// Only apply if documents are present and fewer than 3
|
|
11
|
+
if (docs.length > 0 && docs.length < 3) {
|
|
12
|
+
const q = (context.userQuery || '').toLowerCase().trim();
|
|
13
|
+
const isExplicitComparison = /\b(compare|versus|vs\.?)\b/i.test(q);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
renderType: isExplicitComparison ? 'table' : 'text',
|
|
17
|
+
confidence: 0.98,
|
|
18
|
+
reason: `Rule 6: Small result set (${docs.length} rows < 3). Charts avoided to prevent sparse or uninformative visualizations.`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
|
|
2
|
+
|
|
3
|
+
export class Rule7LargeTableRule implements IRenderRule {
|
|
4
|
+
readonly name = 'Rule7LargeTable';
|
|
5
|
+
readonly priority = 75;
|
|
6
|
+
|
|
7
|
+
evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
|
|
8
|
+
const docs = context.retrievedDocuments || [];
|
|
9
|
+
|
|
10
|
+
if (docs.length > 5) {
|
|
11
|
+
const q = (context.userQuery || '').toLowerCase().trim();
|
|
12
|
+
const hasNumericAgg = intent === 'analytics' || intent === 'ranking' || /\b(sales|revenue|growth|count|total|average)\b/i.test(q);
|
|
13
|
+
|
|
14
|
+
if (hasNumericAgg) {
|
|
15
|
+
return {
|
|
16
|
+
renderType: 'mixed',
|
|
17
|
+
confidence: 0.88,
|
|
18
|
+
reason: `Rule 7: Large result set (${docs.length} rows > 5) with numeric aggregation rendered as Table + Chart.`,
|
|
19
|
+
sections: [
|
|
20
|
+
{ type: 'table', title: 'Detailed Data Table' },
|
|
21
|
+
{ type: 'chart', chartType: 'bar', title: 'Numerical Breakdown' },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
renderType: 'table',
|
|
28
|
+
confidence: 0.85,
|
|
29
|
+
reason: `Rule 7: Large result set (${docs.length} rows > 5) rendered as structured table for readability.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { VectorMatch, UITransformationResponse } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Primary rendering outcome types supported by the decision engine.
|
|
5
|
+
*/
|
|
6
|
+
export type RenderType =
|
|
7
|
+
| 'text'
|
|
8
|
+
| 'table'
|
|
9
|
+
| 'carousel'
|
|
10
|
+
| 'chart'
|
|
11
|
+
| 'mixed'
|
|
12
|
+
| 'card'
|
|
13
|
+
| 'none';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Chart sub-types available when renderType === 'chart'.
|
|
17
|
+
*/
|
|
18
|
+
export type ChartType =
|
|
19
|
+
| 'bar'
|
|
20
|
+
| 'line'
|
|
21
|
+
| 'pie'
|
|
22
|
+
| 'scatter';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* High-level intent classification categories.
|
|
26
|
+
*/
|
|
27
|
+
export type IntentCategory =
|
|
28
|
+
| 'information_lookup'
|
|
29
|
+
| 'product_search'
|
|
30
|
+
| 'recommendation'
|
|
31
|
+
| 'comparison'
|
|
32
|
+
| 'analytics'
|
|
33
|
+
| 'trend_analysis'
|
|
34
|
+
| 'ranking'
|
|
35
|
+
| 'summarization';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Individual section specifier for multi-section (mixed) responses.
|
|
39
|
+
*/
|
|
40
|
+
export interface RenderSectionDecision {
|
|
41
|
+
type: RenderType;
|
|
42
|
+
chartType?: ChartType;
|
|
43
|
+
title?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Final decision produced by the Visualization Decision Engine.
|
|
48
|
+
*/
|
|
49
|
+
export interface RenderDecision {
|
|
50
|
+
/** Primary rendering format selected */
|
|
51
|
+
renderType: RenderType;
|
|
52
|
+
/** Specific chart sub-type if renderType === 'chart' */
|
|
53
|
+
chartType?: ChartType;
|
|
54
|
+
/** Confidence score between 0.0 and 1.0 */
|
|
55
|
+
confidence: number;
|
|
56
|
+
/** Human-readable explanation of why this decision was reached */
|
|
57
|
+
reason: string;
|
|
58
|
+
/** Sub-sections if renderType === 'mixed' */
|
|
59
|
+
sections?: RenderSectionDecision[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Context provided to the intent classifier & decision engine rules.
|
|
64
|
+
*/
|
|
65
|
+
export interface DecisionContext {
|
|
66
|
+
/** Original user prompt/query string */
|
|
67
|
+
userQuery: string;
|
|
68
|
+
/** Vector DB retrieved documents / context records */
|
|
69
|
+
retrievedDocuments?: VectorMatch[];
|
|
70
|
+
/** Optional LLM text response if already available */
|
|
71
|
+
llmResponse?: string;
|
|
72
|
+
/** Additional custom metadata or config settings */
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Interface implemented by rule strategies in the Rule Engine.
|
|
78
|
+
*/
|
|
79
|
+
export interface IRenderRule {
|
|
80
|
+
/** Unique name of the decision rule */
|
|
81
|
+
name: string;
|
|
82
|
+
/** Precedence score (higher priority rules run first) */
|
|
83
|
+
priority: number;
|
|
84
|
+
/** Evaluate the rule against current query context and classified intent */
|
|
85
|
+
evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Interface implemented by pluggable Renderer Strategies in RendererRegistry.
|
|
90
|
+
*/
|
|
91
|
+
export interface IRendererStrategy {
|
|
92
|
+
/** Render type identifier handled by this strategy */
|
|
93
|
+
type: RenderType | string;
|
|
94
|
+
/** Transform input data into a standardized UITransformationResponse payload */
|
|
95
|
+
render(data: unknown, options?: Record<string, unknown>): UITransformationResponse;
|
|
96
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type {
|
|
|
20
20
|
LLMProvider,
|
|
21
21
|
EmbeddingProvider,
|
|
22
22
|
} from './config/RagConfig';
|
|
23
|
-
export type
|
|
23
|
+
export type * from './types';
|
|
24
24
|
export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
|
|
25
25
|
export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
|
|
26
26
|
export type { HealthCheckResult, IProviderValidator, IProviderHealthChecker } from './core/ProviderInterfaces';
|
|
@@ -45,9 +45,10 @@ export { ConfigBuilder, PRESETS, createFromPreset } from './config/ConfigBuilder
|
|
|
45
45
|
export { EmbeddingStrategyResolver, EmbeddingStrategy } from './config/EmbeddingStrategy';
|
|
46
46
|
export { LLM_PROFILES, VECTOR_PROFILES } from './config/UniversalProfiles';
|
|
47
47
|
|
|
48
|
-
// ── RAG Utilities ──────────────────────────────────────────────
|
|
48
|
+
// ── RAG Utilities & Visualization ──────────────────────────────────────────────
|
|
49
49
|
export { DocumentChunker } from './rag/DocumentChunker';
|
|
50
50
|
export { DocumentParser } from './utils/DocumentParser';
|
|
51
|
+
export * from './rendering';
|
|
51
52
|
|
|
52
53
|
// ── Vector DB Providers ────────────────────────────────────────
|
|
53
54
|
export { BaseVectorProvider } from './providers/vectordb/BaseVectorProvider';
|
package/src/types/index.ts
CHANGED
|
@@ -226,3 +226,51 @@ export interface CarouselProduct {
|
|
|
226
226
|
|
|
227
227
|
export * from './props';
|
|
228
228
|
export * from './chat';
|
|
229
|
+
|
|
230
|
+
// ─── Global Retrivora Metadata Schema ───────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Standardized Retrivora Metadata Schema for document vectors and storage.
|
|
234
|
+
* Guarantees uniform metadata structure across vector DB backends (Pinecone, MongoDB, etc.).
|
|
235
|
+
*/
|
|
236
|
+
export interface RetrivoraChunkMetadata {
|
|
237
|
+
/** Document unique identifier */
|
|
238
|
+
docId: string;
|
|
239
|
+
/** Original file name (e.g. report.pdf, data.json, sheets.xlsx) */
|
|
240
|
+
fileName: string;
|
|
241
|
+
/** File format type identifier */
|
|
242
|
+
fileType: 'txt' | 'json' | 'pdf' | 'excel' | 'word' | 'md' | 'csv' | string;
|
|
243
|
+
/** MIME type string */
|
|
244
|
+
mimeType: string;
|
|
245
|
+
/** 0-indexed position of chunk within the parent document */
|
|
246
|
+
chunkIndex: number;
|
|
247
|
+
/** Total count of chunks in the document */
|
|
248
|
+
totalChunks: number;
|
|
249
|
+
/** Extracted text content of the chunk */
|
|
250
|
+
content: string;
|
|
251
|
+
/** Character length of the chunk content */
|
|
252
|
+
characterCount: number;
|
|
253
|
+
/** Workspace or Project ID */
|
|
254
|
+
workspaceId: string;
|
|
255
|
+
/** Isolated vector database namespace (e.g. Pinecone namespace) */
|
|
256
|
+
pineconeNamespace: string;
|
|
257
|
+
/** Retrivora Tier level */
|
|
258
|
+
tier: 'free' | 'enterprise';
|
|
259
|
+
/** ISO 8601 timestamp when the document was processed */
|
|
260
|
+
ingestedAt: string;
|
|
261
|
+
/** Additional custom metadata Key-Value pairs */
|
|
262
|
+
customMetadata?: Record<string, unknown>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface RetrivoraIngestedDocument {
|
|
266
|
+
docId: string;
|
|
267
|
+
fileName: string;
|
|
268
|
+
fileType: string;
|
|
269
|
+
mimeType: string;
|
|
270
|
+
fileSizeBytes?: number;
|
|
271
|
+
totalChunks: number;
|
|
272
|
+
workspaceId: string;
|
|
273
|
+
pineconeNamespace: string;
|
|
274
|
+
ingestedAt: string;
|
|
275
|
+
chunks: RetrivoraChunkMetadata[];
|
|
276
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DocumentParser — handles text extraction from various file formats.
|
|
3
|
-
* Supported: .txt, .md, .json, .csv, .pdf
|
|
3
|
+
* Supported: .txt, .md, .json, .csv, .pdf, .docx, .doc, .xlsx, .xls
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class DocumentParser {
|
|
@@ -8,14 +8,14 @@ export class DocumentParser {
|
|
|
8
8
|
* Extract text from a File or Buffer based on its type.
|
|
9
9
|
*/
|
|
10
10
|
static async parse(file: File | Buffer, fileName: string, mimeType: string): Promise<string> {
|
|
11
|
-
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
11
|
+
const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
|
12
12
|
|
|
13
|
-
// 1. Basic text formats
|
|
13
|
+
// 1. Basic text & Markdown formats
|
|
14
14
|
if (extension === 'txt' || extension === 'md' || mimeType === 'text/plain' || mimeType === 'text/markdown') {
|
|
15
15
|
return this.readAsText(file);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// 2. JSON
|
|
18
|
+
// 2. JSON files
|
|
19
19
|
if (extension === 'json' || mimeType === 'application/json') {
|
|
20
20
|
const text = await this.readAsText(file);
|
|
21
21
|
try {
|
|
@@ -26,39 +26,75 @@ export class DocumentParser {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// 3. CSV
|
|
29
|
+
// 3. CSV files
|
|
30
30
|
if (extension === 'csv' || mimeType === 'text/csv') {
|
|
31
31
|
return this.readAsText(file);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// 4.
|
|
34
|
+
// 4. Excel spreadsheets (.xlsx, .xls)
|
|
35
|
+
if (
|
|
36
|
+
extension === 'xlsx' ||
|
|
37
|
+
extension === 'xls' ||
|
|
38
|
+
mimeType.includes('spreadsheet') ||
|
|
39
|
+
mimeType.includes('excel')
|
|
40
|
+
) {
|
|
41
|
+
try {
|
|
42
|
+
// Dynamic import with ts-ignore to allow optional resolution
|
|
43
|
+
const xlsx = await import('xlsx');
|
|
44
|
+
const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
|
|
45
|
+
const workbook = xlsx.read(buffer, { type: 'buffer' });
|
|
46
|
+
const sheetsText: string[] = [];
|
|
47
|
+
|
|
48
|
+
for (const sheetName of workbook.SheetNames) {
|
|
49
|
+
const sheet = workbook.Sheets[sheetName];
|
|
50
|
+
const csv = xlsx.utils.sheet_to_csv(sheet);
|
|
51
|
+
if (csv && csv.trim()) {
|
|
52
|
+
sheetsText.push(`--- Sheet: ${sheetName} ---\n${csv.trim()}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return sheetsText.join('\n\n') || `[Empty Excel Workbook: ${fileName}]`;
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall back to reading text lines if xlsx module is not present
|
|
58
|
+
try {
|
|
59
|
+
return await this.readAsText(file);
|
|
60
|
+
} catch {
|
|
61
|
+
return `[Excel File Content: ${fileName}]`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 5. PDF Documents (Requires pdf-parse)
|
|
35
67
|
if (extension === 'pdf' || mimeType === 'application/pdf') {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
68
|
+
try {
|
|
69
|
+
const pdf = await import('pdf-parse');
|
|
70
|
+
const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
|
|
71
|
+
const data = await pdf.default(buffer);
|
|
72
|
+
return data.text;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn('[DocumentParser] PDF parsing failed. Make sure "pdf-parse" is installed.', err);
|
|
75
|
+
return `[PDF Parsing Error for ${fileName}]`;
|
|
76
|
+
}
|
|
46
77
|
}
|
|
47
78
|
|
|
48
|
-
//
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
// 6. Word Documents (.docx, .doc) (Requires mammoth)
|
|
80
|
+
if (
|
|
81
|
+
extension === 'docx' ||
|
|
82
|
+
extension === 'doc' ||
|
|
83
|
+
mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
84
|
+
mimeType === 'application/msword'
|
|
85
|
+
) {
|
|
86
|
+
try {
|
|
87
|
+
const mammoth = await import('mammoth');
|
|
88
|
+
const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
|
|
89
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
90
|
+
return result.value;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.warn('[DocumentParser] Word parsing failed. Make sure "mammoth" is installed.', err);
|
|
93
|
+
return `[Word Parsing Error for ${fileName}]`;
|
|
94
|
+
}
|
|
59
95
|
}
|
|
60
96
|
|
|
61
|
-
// Fallback: try reading as text
|
|
97
|
+
// Fallback: try reading as plain UTF-8 text
|
|
62
98
|
try {
|
|
63
99
|
return await this.readAsText(file);
|
|
64
100
|
} catch {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../types';
|
|
13
13
|
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
14
14
|
import { resolveMetadataValue } from './synonyms';
|
|
15
|
+
import { VisualizationDecisionEngine } from '../rendering/VisualizationDecisionEngine';
|
|
15
16
|
|
|
16
17
|
// ─── Intent Types ─────────────────────────────────────────────────────────────
|
|
17
18
|
|
|
@@ -192,6 +193,9 @@ export class UITransformer {
|
|
|
192
193
|
|
|
193
194
|
// Priority 7 – Product carousel only for explicit product intent.
|
|
194
195
|
if ((hasProducts || this.isProductQuery(userQuery)) && resolvedIntent.visualizationHint === 'product_browse') {
|
|
196
|
+
if (!this.isProductQuery(userQuery)) {
|
|
197
|
+
return this.transformToText(filteredData);
|
|
198
|
+
}
|
|
195
199
|
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
196
200
|
}
|
|
197
201
|
|
|
@@ -214,6 +218,13 @@ export class UITransformer {
|
|
|
214
218
|
sources: VectorMatch[],
|
|
215
219
|
llm: ILLMProvider,
|
|
216
220
|
): Promise<UITransformationResponse> {
|
|
221
|
+
// ── Step 0: Fast Visualization Decision Engine check ────────────────────
|
|
222
|
+
const decision = VisualizationDecisionEngine.decideVisualization(query, sources);
|
|
223
|
+
if (decision.renderType === 'text') {
|
|
224
|
+
console.debug('[UITransformer] Decision Engine selected plain text rendering — skipping visual LLM calls.');
|
|
225
|
+
return this.transformToText(sources);
|
|
226
|
+
}
|
|
227
|
+
|
|
217
228
|
// ── Step 1: Detect intent ───────────────────────────────────────────────
|
|
218
229
|
let intent: QueryIntent;
|
|
219
230
|
try {
|
|
@@ -768,7 +779,7 @@ RULES:
|
|
|
768
779
|
|
|
769
780
|
private static transformToText(data: VectorMatch[]): UITransformationResponse {
|
|
770
781
|
return this.createTextResponse(
|
|
771
|
-
'
|
|
782
|
+
'Retrieved Context',
|
|
772
783
|
data.map(item => item.content).join('\n\n'),
|
|
773
784
|
`Found ${data.length} relevant results`,
|
|
774
785
|
);
|
|
@@ -974,8 +985,8 @@ RULES:
|
|
|
974
985
|
private static isProductData(item: VectorMatch): boolean {
|
|
975
986
|
const content = (item.content || '').toLowerCase();
|
|
976
987
|
const productKeywords = [
|
|
977
|
-
'product', 'price', 'stock', '
|
|
978
|
-
'inventory', 'buy', 'shop', 'beauty', '
|
|
988
|
+
'product', 'price', 'stock', 'sku', 'brand', 'msrp',
|
|
989
|
+
'inventory', 'buy', 'shop', 'beauty', 'cosmetic', 'facial',
|
|
979
990
|
'cream', 'serum', 'mask', 'makeup', 'fragrance',
|
|
980
991
|
];
|
|
981
992
|
const hasKeywords = productKeywords.some(kw => content.includes(kw));
|
|
@@ -984,7 +995,7 @@ RULES:
|
|
|
984
995
|
const hasMetadataKey = Object.keys(metadata).some(k => {
|
|
985
996
|
const val = metadata[k];
|
|
986
997
|
return (
|
|
987
|
-
['price', 'product', 'sku', 'brand', '
|
|
998
|
+
['price', 'product', 'sku', 'brand', 'cost'].includes(k.toLowerCase()) &&
|
|
988
999
|
val !== null && val !== undefined && val !== ''
|
|
989
1000
|
);
|
|
990
1001
|
});
|