@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.
Files changed (51) hide show
  1. package/FREE_TIER_ARCHITECTURE.md +232 -0
  2. package/README.md +9 -0
  3. package/dist/{ILLMProvider-DMxLyTdq.d.mts → ILLMProvider-0rRBYbVW.d.mts} +133 -1
  4. package/dist/{ILLMProvider-DMxLyTdq.d.ts → ILLMProvider-0rRBYbVW.d.ts} +133 -1
  5. package/dist/handlers/index.d.mts +2 -2
  6. package/dist/handlers/index.d.ts +2 -2
  7. package/dist/handlers/index.js +681 -17
  8. package/dist/handlers/index.mjs +681 -17
  9. package/dist/index-B1wGUlSL.d.mts +532 -0
  10. package/dist/{index-tzwc7UhY.d.ts → index-BIHHp_f6.d.ts} +1 -1
  11. package/dist/{index-DHsFLJXQ.d.mts → index-BvODr57d.d.mts} +1 -1
  12. package/dist/index-DcklhThn.d.ts +532 -0
  13. package/dist/index.css +51 -3
  14. package/dist/index.d.mts +6 -105
  15. package/dist/index.d.ts +6 -105
  16. package/dist/index.js +1918 -8
  17. package/dist/index.mjs +1913 -8
  18. package/dist/server.d.mts +51 -7
  19. package/dist/server.d.ts +51 -7
  20. package/dist/server.js +723 -17
  21. package/dist/server.mjs +706 -17
  22. package/package.json +9 -7
  23. package/src/app/page.tsx +54 -18
  24. package/src/components/ChatWindow.tsx +2 -2
  25. package/src/components/DocumentUpload.tsx +5 -4
  26. package/src/components/MessageBubble.tsx +2 -4
  27. package/src/core/DatabaseStorage.ts +5 -1
  28. package/src/core/LicenseVerifier.ts +1 -1
  29. package/src/core/Pipeline.ts +23 -5
  30. package/src/handlers/index.ts +48 -1
  31. package/src/index.ts +22 -4
  32. package/src/rendering/IntentClassifier.ts +167 -0
  33. package/src/rendering/RendererRegistry.ts +164 -0
  34. package/src/rendering/RuleEngine.ts +67 -0
  35. package/src/rendering/VisualizationDecisionEngine.ts +164 -0
  36. package/src/rendering/__tests__/VisualizationDecisionEngine.test.ts +237 -0
  37. package/src/rendering/index.ts +12 -0
  38. package/src/rendering/rules/Rule1SpecificInfoRule.ts +28 -0
  39. package/src/rendering/rules/Rule2ComparisonRule.ts +24 -0
  40. package/src/rendering/rules/Rule3ProductDiscoveryRule.ts +28 -0
  41. package/src/rendering/rules/Rule4AnalyticalRule.ts +38 -0
  42. package/src/rendering/rules/Rule5MixedResponseRule.ts +29 -0
  43. package/src/rendering/rules/Rule6SmallResultSetRule.ts +24 -0
  44. package/src/rendering/rules/Rule7LargeTableRule.ts +35 -0
  45. package/src/rendering/types.ts +96 -0
  46. package/src/server.ts +3 -2
  47. package/src/types/index.ts +48 -0
  48. package/src/utils/DocumentParser.ts +64 -28
  49. package/src/utils/UITransformer.ts +15 -4
  50. package/dist/index-CfkqZd2Y.d.ts +0 -197
  51. package/dist/index-xygonxpW.d.mts +0 -197
@@ -5654,6 +5654,598 @@ var LLMRouter = class {
5654
5654
 
5655
5655
  // src/utils/UITransformer.ts
5656
5656
  init_synonyms();
5657
+
5658
+ // src/rendering/IntentClassifier.ts
5659
+ var IntentClassifier = class {
5660
+ /**
5661
+ * Fast, zero-latency classifier that maps a user query and optional data context
5662
+ * into a high-level IntentCategory.
5663
+ */
5664
+ static classify(context) {
5665
+ const q = (context.userQuery || "").toLowerCase().trim();
5666
+ const signals = this.extractDataSignals(context);
5667
+ if (this.isInformationLookup(q)) {
5668
+ return { intent: "information_lookup", signals };
5669
+ }
5670
+ if (this.isComparisonQuery(q)) {
5671
+ return { intent: "comparison", signals };
5672
+ }
5673
+ if (this.isProductQuery(q) || signals.isProductLike && this.isRecommendationQuery(q)) {
5674
+ return { intent: "product_search", signals };
5675
+ }
5676
+ if (this.isTrendQuery(q) || signals.hasDateFields && signals.hasNumericFields && /\b(growth|trend|over time|monthly|yearly|weekly)\b/.test(q)) {
5677
+ return { intent: "trend_analysis", signals };
5678
+ }
5679
+ if (this.isRankingQuery(q)) {
5680
+ return { intent: "ranking", signals };
5681
+ }
5682
+ if (this.isAnalyticsQuery(q) || signals.hasNumericFields && signals.hasCategoricalFields && /\b(sales|revenue|performance|count|total|average|breakdown|share|percentage|correlation)\b/.test(q)) {
5683
+ return { intent: "analytics", signals };
5684
+ }
5685
+ if (this.isRecommendationQuery(q)) {
5686
+ return { intent: "recommendation", signals };
5687
+ }
5688
+ return { intent: "information_lookup", signals };
5689
+ }
5690
+ /**
5691
+ * Extract key data structure signals from retrieved documents.
5692
+ */
5693
+ static extractDataSignals(context) {
5694
+ const docs = context.retrievedDocuments || [];
5695
+ const rowCount = docs.length;
5696
+ let hasNumericFields = false;
5697
+ let hasDateFields = false;
5698
+ let hasCategoricalFields = false;
5699
+ let isProductLike = false;
5700
+ let numericFieldCount = 0;
5701
+ let categoricalFieldCount = 0;
5702
+ if (docs.length > 0) {
5703
+ const keys = /* @__PURE__ */ new Set();
5704
+ let numericKeys = /* @__PURE__ */ new Set();
5705
+ let dateKeys = /* @__PURE__ */ new Set();
5706
+ let catKeys = /* @__PURE__ */ new Set();
5707
+ let productKeyMatches = 0;
5708
+ docs.forEach((doc) => {
5709
+ const meta = doc.metadata || {};
5710
+ Object.entries(meta).forEach(([key, val]) => {
5711
+ keys.add(key);
5712
+ const kLower = key.toLowerCase();
5713
+ if (["price", "image", "brand", "in_stock", "rating", "sku", "product"].some((p) => kLower.includes(p))) {
5714
+ productKeyMatches++;
5715
+ }
5716
+ if (typeof val === "number") {
5717
+ numericKeys.add(key);
5718
+ } else if (typeof val === "string") {
5719
+ if (/^\d{4}-\d{2}-\d{2}/.test(val) || !isNaN(Date.parse(val)) && val.length > 5) {
5720
+ dateKeys.add(key);
5721
+ } else if (val.length < 50 && !val.includes("\n")) {
5722
+ catKeys.add(key);
5723
+ }
5724
+ }
5725
+ });
5726
+ });
5727
+ hasNumericFields = numericKeys.size > 0;
5728
+ hasDateFields = dateKeys.size > 0;
5729
+ hasCategoricalFields = catKeys.size > 0;
5730
+ numericFieldCount = numericKeys.size;
5731
+ categoricalFieldCount = catKeys.size;
5732
+ if (productKeyMatches >= 2 || docs.some((d) => d.content.toLowerCase().includes("price") || d.content.toLowerCase().includes("brand"))) {
5733
+ isProductLike = true;
5734
+ }
5735
+ }
5736
+ return {
5737
+ rowCount,
5738
+ hasNumericFields,
5739
+ hasDateFields,
5740
+ hasCategoricalFields,
5741
+ isProductLike,
5742
+ numericFieldCount,
5743
+ categoricalFieldCount
5744
+ };
5745
+ }
5746
+ // ─── Intent Heuristic Checkers ─────────────────────────────────────────────
5747
+ static isInformationLookup(query) {
5748
+ if (/^(what|who|explain|define|where|when|why|how|how does|can|could|does|do|is|are|has|have|will|should|would|meaning of)\s+/i.test(query)) {
5749
+ if (!/\b(compare|versus|vs\.?|trend|monthly|revenue|top selling|best)\b/i.test(query)) {
5750
+ return true;
5751
+ }
5752
+ }
5753
+ if (/^(what is the )?(price|cost|fee|rate) of\b/i.test(query)) {
5754
+ return true;
5755
+ }
5756
+ return false;
5757
+ }
5758
+ static isComparisonQuery(query) {
5759
+ return /\b(compare|comparison|vs\.?|versus|difference between|differ|contrast|against)\b/i.test(query);
5760
+ }
5761
+ static isProductQuery(query) {
5762
+ return /\b(best|recommend|top|buy|shopping|under \$|cheap|shoes|laptops|chairs|headphones|phone|sneakers|apparel|products)\b/i.test(query) && !/\b(compare|chart|sales|revenue|trend)\b/i.test(query);
5763
+ }
5764
+ static isRecommendationQuery(query) {
5765
+ return /\b(recommend|suggest|what should i (buy|get|choose)|best options|top picks)\b/i.test(query);
5766
+ }
5767
+ static isTrendQuery(query) {
5768
+ return /\b(trend|trends|growth|over time|historical|timeline|monthly|yearly|weekly|daily|revenue growth|sales growth)\b/i.test(query);
5769
+ }
5770
+ static isRankingQuery(query) {
5771
+ return /\b(highest|lowest|top\s*\d+|bottom\s*\d+|rank|ranking|top performing|best performing)\b/i.test(query);
5772
+ }
5773
+ static isAnalyticsQuery(query) {
5774
+ return /\b(sales|revenue|analytics|insights|distribution|percentage|share|breakdown|breakup|scatter|correlation|metrics|performance|by region|by category)\b/i.test(query);
5775
+ }
5776
+ };
5777
+
5778
+ // src/rendering/rules/Rule1SpecificInfoRule.ts
5779
+ var Rule1SpecificInfoRule = class {
5780
+ constructor() {
5781
+ this.name = "Rule1SpecificInfo";
5782
+ this.priority = 100;
5783
+ }
5784
+ evaluate(context, intent) {
5785
+ const q = (context.userQuery || "").toLowerCase().trim();
5786
+ const isFactQuery = intent === "information_lookup" || /^(what|who|explain|define|where|when|why|meaning of)\s+/i.test(q) || /^(what is the price of|price of|cost of)\b/i.test(q);
5787
+ const isNonVisual = !/\b(compare|versus|vs\.?|trend|monthly|revenue|best|top 10|chart|table|list|grid)\b/i.test(q);
5788
+ if (isFactQuery && isNonVisual) {
5789
+ return {
5790
+ renderType: "text",
5791
+ confidence: 0.95,
5792
+ reason: "Rule 1: Specific information request (fact, explanation, or definition) requires plain text rendering."
5793
+ };
5794
+ }
5795
+ return null;
5796
+ }
5797
+ };
5798
+
5799
+ // src/rendering/rules/Rule2ComparisonRule.ts
5800
+ var Rule2ComparisonRule = class {
5801
+ constructor() {
5802
+ this.name = "Rule2Comparison";
5803
+ this.priority = 90;
5804
+ }
5805
+ evaluate(context, intent) {
5806
+ const q = (context.userQuery || "").toLowerCase().trim();
5807
+ const isComparison = intent === "comparison" || /\b(compare|comparison|vs\.?|versus|difference between|differ|contrast|against)\b/i.test(q);
5808
+ if (isComparison) {
5809
+ return {
5810
+ renderType: "table",
5811
+ confidence: 0.9,
5812
+ reason: "Rule 2: Comparison query contrasting multiple entities requires structured table rendering."
5813
+ };
5814
+ }
5815
+ return null;
5816
+ }
5817
+ };
5818
+
5819
+ // src/rendering/rules/Rule3ProductDiscoveryRule.ts
5820
+ var Rule3ProductDiscoveryRule = class {
5821
+ constructor() {
5822
+ this.name = "Rule3ProductDiscovery";
5823
+ this.priority = 85;
5824
+ }
5825
+ evaluate(context, intent) {
5826
+ const q = (context.userQuery || "").toLowerCase().trim();
5827
+ const isDiscovery = intent === "product_search" || intent === "recommendation" || /\b(best|recommend|top|buy|shopping|under \$|cheap|shoes|laptops|chairs|headphones|sneakers|apparel|products|show available)\b/i.test(q);
5828
+ const isNotChart = !/\b(chart|sales|revenue|monthly|growth|trend|over time)\b/i.test(q);
5829
+ const isAnalyticalRanking = /\b(top performing|top selling|best performing|performing|sales|revenue|ranking)\b/i.test(q);
5830
+ if (isDiscovery && isNotChart && !isAnalyticalRanking) {
5831
+ return {
5832
+ renderType: "carousel",
5833
+ confidence: 0.9,
5834
+ reason: "Rule 3: Product discovery/recommendation query requires product carousel rendering."
5835
+ };
5836
+ }
5837
+ return null;
5838
+ }
5839
+ };
5840
+
5841
+ // src/rendering/rules/Rule4AnalyticalRule.ts
5842
+ var Rule4AnalyticalRule = class {
5843
+ constructor() {
5844
+ this.name = "Rule4Analytical";
5845
+ this.priority = 80;
5846
+ }
5847
+ evaluate(context, intent) {
5848
+ const q = (context.userQuery || "").toLowerCase().trim();
5849
+ const isAnalytical = intent === "analytics" || intent === "trend_analysis" || intent === "ranking" || /\b(monthly sales|revenue growth|top performing|population trends|sales by|growth|distribution|share|percentage|breakdown|correlation|scatter|trend)\b/i.test(q);
5850
+ if (!isAnalytical) return null;
5851
+ let chartType = "bar";
5852
+ if (intent === "trend_analysis" || /\b(monthly|yearly|weekly|growth|over time|trend|timeline|historical)\b/i.test(q)) {
5853
+ chartType = "line";
5854
+ } else if (/\b(share|percentage|percent|breakup|breakdown|composition|pie|donut|proportion)\b/i.test(q)) {
5855
+ chartType = "pie";
5856
+ } else if (/\b(correlation|scatter|relationship|relation|impact of|depends on)\b/i.test(q)) {
5857
+ chartType = "scatter";
5858
+ } else {
5859
+ chartType = "bar";
5860
+ }
5861
+ return {
5862
+ renderType: "chart",
5863
+ chartType,
5864
+ confidence: 0.85,
5865
+ reason: `Rule 4: Analytical query with numerical insights selects ${chartType} chart.`
5866
+ };
5867
+ }
5868
+ };
5869
+
5870
+ // src/rendering/rules/Rule5MixedResponseRule.ts
5871
+ var Rule5MixedResponseRule = class {
5872
+ constructor() {
5873
+ this.name = "Rule5MixedResponse";
5874
+ this.priority = 95;
5875
+ }
5876
+ evaluate(context, _intent) {
5877
+ const q = (context.userQuery || "").toLowerCase().trim();
5878
+ const isTopSellingThisMonth = /\btop\s*(selling|performing)?\s*products?\s*(this|last)?\s*(month|year|week)\b/i.test(q);
5879
+ 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);
5880
+ if (isTopSellingThisMonth || hasMultipleSectionsRequest) {
5881
+ return {
5882
+ renderType: "mixed",
5883
+ confidence: 0.95,
5884
+ reason: "Rule 5: Mixed response query requires multi-section composition (text, carousel, chart).",
5885
+ sections: [
5886
+ { type: "text", title: "Summary" },
5887
+ { type: "carousel", title: "Top Products" },
5888
+ { type: "chart", chartType: "bar", title: "Sales Performance" }
5889
+ ]
5890
+ };
5891
+ }
5892
+ return null;
5893
+ }
5894
+ };
5895
+
5896
+ // src/rendering/rules/Rule6SmallResultSetRule.ts
5897
+ var Rule6SmallResultSetRule = class {
5898
+ constructor() {
5899
+ this.name = "Rule6SmallResultSet";
5900
+ this.priority = 110;
5901
+ }
5902
+ evaluate(context, _intent) {
5903
+ const docs = context.retrievedDocuments || [];
5904
+ if (docs.length > 0 && docs.length < 3) {
5905
+ const q = (context.userQuery || "").toLowerCase().trim();
5906
+ const isExplicitComparison = /\b(compare|versus|vs\.?)\b/i.test(q);
5907
+ return {
5908
+ renderType: isExplicitComparison ? "table" : "text",
5909
+ confidence: 0.98,
5910
+ reason: `Rule 6: Small result set (${docs.length} rows < 3). Charts avoided to prevent sparse or uninformative visualizations.`
5911
+ };
5912
+ }
5913
+ return null;
5914
+ }
5915
+ };
5916
+
5917
+ // src/rendering/rules/Rule7LargeTableRule.ts
5918
+ var Rule7LargeTableRule = class {
5919
+ constructor() {
5920
+ this.name = "Rule7LargeTable";
5921
+ this.priority = 75;
5922
+ }
5923
+ evaluate(context, intent) {
5924
+ const docs = context.retrievedDocuments || [];
5925
+ if (docs.length > 5) {
5926
+ const q = (context.userQuery || "").toLowerCase().trim();
5927
+ const hasNumericAgg = intent === "analytics" || intent === "ranking" || /\b(sales|revenue|growth|count|total|average)\b/i.test(q);
5928
+ if (hasNumericAgg) {
5929
+ return {
5930
+ renderType: "mixed",
5931
+ confidence: 0.88,
5932
+ reason: `Rule 7: Large result set (${docs.length} rows > 5) with numeric aggregation rendered as Table + Chart.`,
5933
+ sections: [
5934
+ { type: "table", title: "Detailed Data Table" },
5935
+ { type: "chart", chartType: "bar", title: "Numerical Breakdown" }
5936
+ ]
5937
+ };
5938
+ }
5939
+ return {
5940
+ renderType: "table",
5941
+ confidence: 0.85,
5942
+ reason: `Rule 7: Large result set (${docs.length} rows > 5) rendered as structured table for readability.`
5943
+ };
5944
+ }
5945
+ return null;
5946
+ }
5947
+ };
5948
+
5949
+ // src/rendering/RuleEngine.ts
5950
+ var RuleEngine = class {
5951
+ constructor() {
5952
+ this.rules = [];
5953
+ this.registerDefaultRules();
5954
+ }
5955
+ /**
5956
+ * Register standard rules in priority order.
5957
+ */
5958
+ registerDefaultRules() {
5959
+ this.registerRule(new Rule6SmallResultSetRule());
5960
+ this.registerRule(new Rule1SpecificInfoRule());
5961
+ this.registerRule(new Rule5MixedResponseRule());
5962
+ this.registerRule(new Rule2ComparisonRule());
5963
+ this.registerRule(new Rule3ProductDiscoveryRule());
5964
+ this.registerRule(new Rule4AnalyticalRule());
5965
+ this.registerRule(new Rule7LargeTableRule());
5966
+ }
5967
+ /**
5968
+ * Add or replace a rule in the engine.
5969
+ * Automatically keeps rules sorted by descending priority.
5970
+ */
5971
+ registerRule(rule) {
5972
+ this.rules = this.rules.filter((r) => r.name !== rule.name);
5973
+ this.rules.push(rule);
5974
+ this.rules.sort((a, b) => b.priority - a.priority);
5975
+ }
5976
+ /**
5977
+ * Evaluate context against rules in descending priority order.
5978
+ * Returns the first non-null RenderDecision.
5979
+ */
5980
+ evaluate(context, intent) {
5981
+ for (const rule of this.rules) {
5982
+ const decision = rule.evaluate(context, intent);
5983
+ if (decision) {
5984
+ return decision;
5985
+ }
5986
+ }
5987
+ return {
5988
+ renderType: "text",
5989
+ confidence: 0.7,
5990
+ reason: "Default fallback: No specific visualization rule matched context."
5991
+ };
5992
+ }
5993
+ /**
5994
+ * List all registered rules.
5995
+ */
5996
+ getRegisteredRules() {
5997
+ return this.rules.map((r) => ({ name: r.name, priority: r.priority }));
5998
+ }
5999
+ };
6000
+
6001
+ // src/rendering/RendererRegistry.ts
6002
+ var TextRendererStrategy = class {
6003
+ constructor() {
6004
+ this.type = "text";
6005
+ }
6006
+ render(data, options) {
6007
+ const title = (options == null ? void 0 : options.title) || "Information";
6008
+ if (typeof data === "string") {
6009
+ return { type: "text", title, data: { content: data } };
6010
+ }
6011
+ const docs = Array.isArray(data) ? data : [];
6012
+ const text = docs.map((d) => d.content).join("\n\n");
6013
+ return { type: "text", title, data: { content: text || "No detailed content available." } };
6014
+ }
6015
+ };
6016
+ var TableRendererStrategy = class {
6017
+ constructor() {
6018
+ this.type = "table";
6019
+ }
6020
+ render(data, options) {
6021
+ const docs = Array.isArray(data) ? data : [];
6022
+ const query = (options == null ? void 0 : options.query) || "";
6023
+ return UITransformer.transform(query, docs, void 0, void 0, {
6024
+ visualizationHint: "table",
6025
+ recommendedChart: "table",
6026
+ filterInStockOnly: false,
6027
+ wantsExplicitTable: true,
6028
+ isTemporal: false,
6029
+ isComparison: true,
6030
+ language: "en"
6031
+ });
6032
+ }
6033
+ };
6034
+ var CarouselRendererStrategy = class {
6035
+ constructor() {
6036
+ this.type = "carousel";
6037
+ }
6038
+ render(data, options) {
6039
+ const docs = Array.isArray(data) ? data : [];
6040
+ const query = (options == null ? void 0 : options.query) || "";
6041
+ return UITransformer.transform(query, docs, void 0, void 0, {
6042
+ visualizationHint: "product_browse",
6043
+ recommendedChart: "text",
6044
+ filterInStockOnly: false,
6045
+ wantsExplicitTable: false,
6046
+ isTemporal: false,
6047
+ isComparison: false,
6048
+ language: "en"
6049
+ });
6050
+ }
6051
+ };
6052
+ var ChartRendererStrategy = class {
6053
+ constructor() {
6054
+ this.type = "chart";
6055
+ }
6056
+ render(data, options) {
6057
+ const docs = Array.isArray(data) ? data : [];
6058
+ const query = (options == null ? void 0 : options.query) || "";
6059
+ const chartType = (options == null ? void 0 : options.chartType) || "bar";
6060
+ let hint = "comparison";
6061
+ let rec = "bar_chart";
6062
+ if (chartType === "line") {
6063
+ hint = "trend";
6064
+ rec = "line_chart";
6065
+ } else if (chartType === "pie") {
6066
+ hint = "composition";
6067
+ rec = "pie_chart";
6068
+ } else if (chartType === "scatter") {
6069
+ hint = "correlation";
6070
+ rec = "scatter_plot";
6071
+ }
6072
+ return UITransformer.transform(query, docs, void 0, void 0, {
6073
+ visualizationHint: hint,
6074
+ recommendedChart: rec,
6075
+ filterInStockOnly: false,
6076
+ wantsExplicitTable: false,
6077
+ isTemporal: chartType === "line",
6078
+ isComparison: chartType === "bar",
6079
+ language: "en"
6080
+ });
6081
+ }
6082
+ };
6083
+ var MixedRendererStrategy = class {
6084
+ constructor() {
6085
+ this.type = "mixed";
6086
+ }
6087
+ render(data, options) {
6088
+ const docs = Array.isArray(data) ? data : [];
6089
+ const sections = (options == null ? void 0 : options.sections) || [];
6090
+ const sectionPayloads = sections.map((s) => {
6091
+ const strategy = RendererRegistry.getStrategy(s.type);
6092
+ return {
6093
+ type: s.type,
6094
+ title: s.title,
6095
+ payload: strategy.render(data, __spreadProps(__spreadValues({}, options), { chartType: s.chartType, title: s.title }))
6096
+ };
6097
+ });
6098
+ return {
6099
+ type: "table",
6100
+ // Fallback container type for compatibility
6101
+ title: (options == null ? void 0 : options.title) || "Composite Response",
6102
+ description: `Mixed payload containing ${sectionPayloads.length} visual sections`,
6103
+ data: { sections: sectionPayloads }
6104
+ };
6105
+ }
6106
+ };
6107
+ var _RendererRegistry = class _RendererRegistry {
6108
+ /**
6109
+ * Register a new renderer strategy.
6110
+ * Enables seamless addition of custom visualizers (Timeline, Maps, Flow Diagrams, etc.).
6111
+ */
6112
+ static registerStrategy(strategy) {
6113
+ this.strategies.set(String(strategy.type).toLowerCase(), strategy);
6114
+ }
6115
+ /**
6116
+ * Retrieve a registered renderer strategy by type string.
6117
+ */
6118
+ static getStrategy(type) {
6119
+ const key = type.toLowerCase();
6120
+ const strategy = this.strategies.get(key);
6121
+ if (!strategy) {
6122
+ return this.strategies.get("text");
6123
+ }
6124
+ return strategy;
6125
+ }
6126
+ /**
6127
+ * Check if a renderer strategy is registered.
6128
+ */
6129
+ static hasStrategy(type) {
6130
+ return this.strategies.has(type.toLowerCase());
6131
+ }
6132
+ /**
6133
+ * List all registered strategy types.
6134
+ */
6135
+ static getRegisteredTypes() {
6136
+ return Array.from(this.strategies.keys());
6137
+ }
6138
+ };
6139
+ _RendererRegistry.strategies = /* @__PURE__ */ new Map();
6140
+ _RendererRegistry.registerStrategy(new TextRendererStrategy());
6141
+ _RendererRegistry.registerStrategy(new TableRendererStrategy());
6142
+ _RendererRegistry.registerStrategy(new CarouselRendererStrategy());
6143
+ _RendererRegistry.registerStrategy(new ChartRendererStrategy());
6144
+ _RendererRegistry.registerStrategy(new MixedRendererStrategy());
6145
+ var RendererRegistry = _RendererRegistry;
6146
+
6147
+ // src/rendering/VisualizationDecisionEngine.ts
6148
+ var LRUDecisionCache = class {
6149
+ constructor(maxSize = 200) {
6150
+ this.cache = /* @__PURE__ */ new Map();
6151
+ this.maxSize = maxSize;
6152
+ }
6153
+ generateKey(context) {
6154
+ var _a2, _b;
6155
+ const q = (context.userQuery || "").toLowerCase().trim();
6156
+ const docCount = (_b = (_a2 = context.retrievedDocuments) == null ? void 0 : _a2.length) != null ? _b : 0;
6157
+ return `${q}::docs:${docCount}`;
6158
+ }
6159
+ get(context) {
6160
+ const key = this.generateKey(context);
6161
+ const item = this.cache.get(key);
6162
+ if (item) {
6163
+ this.cache.delete(key);
6164
+ this.cache.set(key, item);
6165
+ return item.decision;
6166
+ }
6167
+ return void 0;
6168
+ }
6169
+ set(context, decision) {
6170
+ const key = this.generateKey(context);
6171
+ if (this.cache.has(key)) {
6172
+ this.cache.delete(key);
6173
+ } else if (this.cache.size >= this.maxSize) {
6174
+ const oldest = this.cache.keys().next().value;
6175
+ if (oldest !== void 0) this.cache.delete(oldest);
6176
+ }
6177
+ this.cache.set(key, { decision, timestamp: Date.now() });
6178
+ }
6179
+ clear() {
6180
+ this.cache.clear();
6181
+ }
6182
+ get size() {
6183
+ return this.cache.size;
6184
+ }
6185
+ };
6186
+ var VisualizationDecisionEngine = class {
6187
+ /**
6188
+ * Evaluate user query, documents, and context to produce a RenderDecision.
6189
+ * Execution time is < 1ms for cached or rule-matched queries.
6190
+ */
6191
+ static decideVisualization(userQuery, retrievedDocuments, llmResponse, metadata) {
6192
+ const context = {
6193
+ userQuery,
6194
+ retrievedDocuments,
6195
+ llmResponse,
6196
+ metadata
6197
+ };
6198
+ const cached = this.cache.get(context);
6199
+ if (cached) {
6200
+ return cached;
6201
+ }
6202
+ const { intent } = IntentClassifier.classify(context);
6203
+ const decision = this.ruleEngine.evaluate(context, intent);
6204
+ this.cache.set(context, decision);
6205
+ return decision;
6206
+ }
6207
+ /**
6208
+ * Convenience helper to decide AND render the data into a UITransformationResponse.
6209
+ */
6210
+ static render(userQuery, retrievedDocuments, llmResponse, metadata) {
6211
+ const decision = this.decideVisualization(userQuery, retrievedDocuments, llmResponse, metadata);
6212
+ const strategy = RendererRegistry.getStrategy(decision.renderType);
6213
+ return strategy.render(retrievedDocuments || [], __spreadValues({
6214
+ query: userQuery,
6215
+ chartType: decision.chartType,
6216
+ sections: decision.sections
6217
+ }, metadata));
6218
+ }
6219
+ /**
6220
+ * Register a custom rule in the rule engine.
6221
+ */
6222
+ static registerRule(rule) {
6223
+ this.ruleEngine.registerRule(rule);
6224
+ this.cache.clear();
6225
+ }
6226
+ /**
6227
+ * Register a custom renderer strategy.
6228
+ */
6229
+ static registerRendererStrategy(strategy) {
6230
+ RendererRegistry.registerStrategy(strategy);
6231
+ }
6232
+ /**
6233
+ * Clear the decision cache.
6234
+ */
6235
+ static clearCache() {
6236
+ this.cache.clear();
6237
+ }
6238
+ /**
6239
+ * Get size of current decision cache.
6240
+ */
6241
+ static getCacheSize() {
6242
+ return this.cache.size;
6243
+ }
6244
+ };
6245
+ VisualizationDecisionEngine.ruleEngine = new RuleEngine();
6246
+ VisualizationDecisionEngine.cache = new LRUDecisionCache(200);
6247
+
6248
+ // src/utils/UITransformer.ts
5657
6249
  var UITransformer = class _UITransformer {
5658
6250
  // ─── Public Entry Points ─────────────────────────────────────────────────
5659
6251
  /**
@@ -5700,6 +6292,9 @@ var UITransformer = class _UITransformer {
5700
6292
  return this.hasMultipleFields(filteredData) ? this.transformToTable(filteredData, userQuery) : this.transformToText(filteredData);
5701
6293
  }
5702
6294
  if ((hasProducts || this.isProductQuery(userQuery)) && resolvedIntent.visualizationHint === "product_browse") {
6295
+ if (!this.isProductQuery(userQuery)) {
6296
+ return this.transformToText(filteredData);
6297
+ }
5703
6298
  return this.transformToProductCarousel(filteredData, config, trainedSchema);
5704
6299
  }
5705
6300
  const automatic = this.chooseAutomaticVisualization(filteredData, profile, userQuery);
@@ -5714,6 +6309,11 @@ var UITransformer = class _UITransformer {
5714
6309
  * Step 3 — Fall back to the heuristic `transform()` if either LLM call fails.
5715
6310
  */
5716
6311
  static async analyzeAndDecide(query, sources, llm) {
6312
+ const decision = VisualizationDecisionEngine.decideVisualization(query, sources);
6313
+ if (decision.renderType === "text") {
6314
+ console.debug("[UITransformer] Decision Engine selected plain text rendering \u2014 skipping visual LLM calls.");
6315
+ return this.transformToText(sources);
6316
+ }
5717
6317
  let intent;
5718
6318
  try {
5719
6319
  intent = await this.detectIntent(query, llm, sources);
@@ -6163,7 +6763,7 @@ ${schemaProfileText}` : ""}`;
6163
6763
  }
6164
6764
  static transformToText(data) {
6165
6765
  return this.createTextResponse(
6166
- "Search Results",
6766
+ "Retrieved Context",
6167
6767
  data.map((item) => item.content).join("\n\n"),
6168
6768
  `Found ${data.length} relevant results`
6169
6769
  );
@@ -6330,16 +6930,13 @@ ${schemaProfileText}` : ""}`;
6330
6930
  "product",
6331
6931
  "price",
6332
6932
  "stock",
6333
- "item",
6334
6933
  "sku",
6335
6934
  "brand",
6336
- "model",
6337
6935
  "msrp",
6338
6936
  "inventory",
6339
6937
  "buy",
6340
6938
  "shop",
6341
6939
  "beauty",
6342
- "care",
6343
6940
  "cosmetic",
6344
6941
  "facial",
6345
6942
  "cream",
@@ -6352,7 +6949,7 @@ ${schemaProfileText}` : ""}`;
6352
6949
  const metadata = item.metadata || {};
6353
6950
  const hasMetadataKey = Object.keys(metadata).some((k) => {
6354
6951
  const val = metadata[k];
6355
- return ["price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase()) && val !== null && val !== void 0 && val !== "";
6952
+ return ["price", "product", "sku", "brand", "cost"].includes(k.toLowerCase()) && val !== null && val !== void 0 && val !== "";
6356
6953
  });
6357
6954
  const hasPricePattern = /\$\s*\d+\.\d{2}/.test(content);
6358
6955
  return hasKeywords || hasMetadataKey || hasPricePattern;
@@ -7041,6 +7638,13 @@ SchemaMapper.TARGET_PROPERTIES = {
7041
7638
  };
7042
7639
 
7043
7640
  // src/core/Pipeline.ts
7641
+ function formatNamespace(raw) {
7642
+ if (!raw || !raw.trim()) return "retrivora-default";
7643
+ const trimmed = raw.trim();
7644
+ if (trimmed.startsWith("retrivora-")) return trimmed;
7645
+ const sanitized = trimmed.toLowerCase().replace(/[^a-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
7646
+ return `retrivora-${sanitized}`;
7647
+ }
7044
7648
  var LRUEmbeddingCache = class {
7045
7649
  constructor(maxSize = 500) {
7046
7650
  this.cache = /* @__PURE__ */ new Map();
@@ -7232,7 +7836,7 @@ ${m.content}`).join("\n\n---\n\n");
7232
7836
  */
7233
7837
  async ingest(documents, namespace) {
7234
7838
  await this.initialize();
7235
- const ns = namespace != null ? namespace : this.config.projectId;
7839
+ const ns = formatNamespace(namespace != null ? namespace : this.config.projectId);
7236
7840
  const results = [];
7237
7841
  for (const doc of documents) {
7238
7842
  try {
@@ -7451,7 +8055,7 @@ ${m.content}`).join("\n\n---\n\n");
7451
8055
  return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
7452
8056
  var _a2, _b, _c, _d, _e, _f, _g2, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C;
7453
8057
  yield new __await(this.initialize());
7454
- const ns = namespace != null ? namespace : this.config.projectId;
8058
+ const ns = formatNamespace(namespace != null ? namespace : this.config.projectId);
7455
8059
  const topK = (_b = (_a2 = this.config.rag) == null ? void 0 : _a2.topK) != null ? _b : 5;
7456
8060
  const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0.3;
7457
8061
  const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7589,7 +8193,7 @@ ${context}`;
7589
8193
  const isNativeThinking = isClaude37 && (((_p = this.config.llm.options) == null ? void 0 : _p.thinking) === true || ((_q = this.config.llm.options) == null ? void 0 : _q.thinking) === "enabled" || ((_r = this.config.llm.options) == null ? void 0 : _r.thinking) !== false);
7590
8194
  const modelNameLower = this.config.llm.model.toLowerCase();
7591
8195
  const isReasoningModel = modelNameLower.includes("-r1") || modelNameLower.includes("deepseek-r1") || modelNameLower.includes("reasoning") || modelNameLower.includes("thinking");
7592
- const isSimulatedThinking = !isNativeThinking && (((_s = this.config.llm.options) == null ? void 0 : _s.thinking) === true || ((_t = this.config.llm.options) == null ? void 0 : _t.thinking) === "enabled" || isReasoningModel && ((_u = this.config.llm.options) == null ? void 0 : _u.thinking) !== false);
8196
+ const isSimulatedThinking = !isNativeThinking && (((_s = this.config.llm.options) == null ? void 0 : _s.thinking) === true || ((_t = this.config.llm.options) == null ? void 0 : _t.thinking) === "enabled" || ((_u = this.config.llm.options) == null ? void 0 : _u.thinking) !== false);
7593
8197
  let finalRestrictionSuffix = restrictionSuffix;
7594
8198
  if (isSimulatedThinking) {
7595
8199
  finalRestrictionSuffix += "\n\n(IMPORTANT: You must think step-by-step before answering. Write your thinking process inside a `<think>...</think>` block. Write the `<think>` block first, then follow it with your final response. Keep the thinking process thorough and detailed. Example: `<think>Thinking details here...</think>Final answer text here.`)";
@@ -7915,7 +8519,7 @@ ${context}`;
7915
8519
  }
7916
8520
  async retrieve(query, options) {
7917
8521
  var _a2, _b, _c, _d, _e, _f, _g2;
7918
- const ns = (_a2 = options.namespace) != null ? _a2 : this.config.projectId;
8522
+ const ns = formatNamespace((_a2 = options.namespace) != null ? _a2 : this.config.projectId);
7919
8523
  const topK = (_b = options.topK) != null ? _b : 5;
7920
8524
  const cacheKey = `${ns}::${query}`;
7921
8525
  let queryVector = this.embeddingCache.get(cacheKey);
@@ -7984,7 +8588,7 @@ Optimized Search Query:`;
7984
8588
  async getSuggestions(query, namespace) {
7985
8589
  if (!query || query.trim().length < 3) return [];
7986
8590
  await this.initialize();
7987
- const ns = namespace != null ? namespace : this.config.projectId;
8591
+ const ns = formatNamespace(namespace != null ? namespace : this.config.projectId);
7988
8592
  try {
7989
8593
  const { sources } = await this.retrieve(query, { namespace: ns, topK: 5 });
7990
8594
  if (sources.length === 0) return [];
@@ -8217,7 +8821,7 @@ var LicenseVerifier = class {
8217
8821
  `The database provider "${provider}" is not allowed on the Hobby tier. Hobby tier is restricted to PostgreSQL and Supabase. Please upgrade to a Developer Pro or Enterprise plan.`
8218
8822
  );
8219
8823
  }
8220
- } else if (tier === "pro" || tier === "developer_pro" || tier === "premium" || tier === "growth") {
8824
+ } else if (tier === "pro" || tier === "developer_pro" || tier === "premium" || tier === "growth" || tier === "free_trial" || tier === "free_tier" || tier === "free") {
8221
8825
  const allowedPro = [
8222
8826
  "postgresql",
8223
8827
  "pgvector",
@@ -8476,7 +9080,7 @@ var DocumentParser = class {
8476
9080
  */
8477
9081
  static async parse(file, fileName, mimeType) {
8478
9082
  var _a2;
8479
- const extension = (_a2 = fileName.split(".").pop()) == null ? void 0 : _a2.toLowerCase();
9083
+ const extension = ((_a2 = fileName.split(".").pop()) == null ? void 0 : _a2.toLowerCase()) || "";
8480
9084
  if (extension === "txt" || extension === "md" || mimeType === "text/plain" || mimeType === "text/markdown") {
8481
9085
  return this.readAsText(file);
8482
9086
  }
@@ -8492,6 +9096,29 @@ var DocumentParser = class {
8492
9096
  if (extension === "csv" || mimeType === "text/csv") {
8493
9097
  return this.readAsText(file);
8494
9098
  }
9099
+ if (extension === "xlsx" || extension === "xls" || mimeType.includes("spreadsheet") || mimeType.includes("excel")) {
9100
+ try {
9101
+ const xlsx = await import("xlsx");
9102
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
9103
+ const workbook = xlsx.read(buffer, { type: "buffer" });
9104
+ const sheetsText = [];
9105
+ for (const sheetName of workbook.SheetNames) {
9106
+ const sheet = workbook.Sheets[sheetName];
9107
+ const csv = xlsx.utils.sheet_to_csv(sheet);
9108
+ if (csv && csv.trim()) {
9109
+ sheetsText.push(`--- Sheet: ${sheetName} ---
9110
+ ${csv.trim()}`);
9111
+ }
9112
+ }
9113
+ return sheetsText.join("\n\n") || `[Empty Excel Workbook: ${fileName}]`;
9114
+ } catch (e) {
9115
+ try {
9116
+ return await this.readAsText(file);
9117
+ } catch (e2) {
9118
+ return `[Excel File Content: ${fileName}]`;
9119
+ }
9120
+ }
9121
+ }
8495
9122
  if (extension === "pdf" || mimeType === "application/pdf") {
8496
9123
  try {
8497
9124
  const pdf = await import("pdf-parse");
@@ -8503,15 +9130,15 @@ var DocumentParser = class {
8503
9130
  return `[PDF Parsing Error for ${fileName}]`;
8504
9131
  }
8505
9132
  }
8506
- if (extension === "docx" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
9133
+ if (extension === "docx" || extension === "doc" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || mimeType === "application/msword") {
8507
9134
  try {
8508
9135
  const mammoth = await import("mammoth");
8509
9136
  const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
8510
9137
  const result = await mammoth.extractRawText({ buffer });
8511
9138
  return result.value;
8512
9139
  } catch (err) {
8513
- console.warn('[DocumentParser] DOCX parsing failed. Make sure "mammoth" is installed.', err);
8514
- return `[DOCX Parsing Error for ${fileName}]`;
9140
+ console.warn('[DocumentParser] Word parsing failed. Make sure "mammoth" is installed.', err);
9141
+ return `[Word Parsing Error for ${fileName}]`;
8515
9142
  }
8516
9143
  }
8517
9144
  try {
@@ -8567,7 +9194,7 @@ var DatabaseStorage = class {
8567
9194
  try {
8568
9195
  const payload = LicenseVerifier.verify(this.config.licenseKey, this.config.projectId);
8569
9196
  const tier = (payload.tier || "").toLowerCase();
8570
- if (tier !== "premium" && tier !== "enterprise" && tier !== "growth" && tier !== "pro" && tier !== "developer_pro") {
9197
+ if (tier !== "premium" && tier !== "enterprise" && tier !== "growth" && tier !== "pro" && tier !== "developer_pro" && tier !== "free_trial" && tier !== "free_tier" && tier !== "free" && tier !== "hobby") {
8571
9198
  throw new Error(
8572
9199
  `[Retrivora SDK] Chat History and Feedback features are not available on the "${tier}" tier. Please upgrade to a Developer Pro or Enterprise plan.`
8573
9200
  );
@@ -9335,6 +9962,7 @@ function createUploadHandler(configOrPlugin, options) {
9335
9962
  }
9336
9963
  const documents = [];
9337
9964
  for (const file of files) {
9965
+ const isExcel = file.name.toLowerCase().endsWith(".xlsx") || file.name.toLowerCase().endsWith(".xls") || file.type.includes("spreadsheet") || file.type.includes("excel");
9338
9966
  if (file.name.toLowerCase().endsWith(".csv") || file.type === "text/csv") {
9339
9967
  const text = await file.text();
9340
9968
  const Papa = await import("papaparse");
@@ -9367,7 +9995,7 @@ function createUploadHandler(configOrPlugin, options) {
9367
9995
  metadata: __spreadValues(__spreadProps(__spreadValues({
9368
9996
  fileName: file.name,
9369
9997
  fileSize: file.size,
9370
- fileType: file.type,
9998
+ fileType: file.type || "text/csv",
9371
9999
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
9372
10000
  }, dimension ? { dimension } : {}), {
9373
10001
  csvHeaders: csvCols
@@ -9375,6 +10003,42 @@ function createUploadHandler(configOrPlugin, options) {
9375
10003
  });
9376
10004
  }
9377
10005
  }
10006
+ } else if (isExcel) {
10007
+ const xlsx = await import("xlsx");
10008
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
10009
+ const workbook = xlsx.read(buffer, { type: "buffer" });
10010
+ for (const sheetName of workbook.SheetNames) {
10011
+ const sheet = workbook.Sheets[sheetName];
10012
+ const jsonRows = xlsx.utils.sheet_to_json(sheet, { defval: "" });
10013
+ if (jsonRows && jsonRows.length > 0) {
10014
+ let i = 0;
10015
+ const headers = Object.keys(jsonRows[0] || {});
10016
+ for (const rowData of jsonRows) {
10017
+ i++;
10018
+ const contentParts = [];
10019
+ for (const [key, val] of Object.entries(rowData)) {
10020
+ if (key && val !== void 0 && val !== null && String(val).trim()) {
10021
+ contentParts.push(`${key}: ${val}`);
10022
+ }
10023
+ }
10024
+ if (contentParts.length > 0) {
10025
+ documents.push({
10026
+ docId: `${file.name}-${sheetName}-row-${i}`,
10027
+ content: contentParts.join(", "),
10028
+ metadata: __spreadValues(__spreadProps(__spreadValues({
10029
+ fileName: file.name,
10030
+ sheetName,
10031
+ fileSize: file.size,
10032
+ fileType: file.type || "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
10033
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
10034
+ }, dimension ? { dimension } : {}), {
10035
+ headers
10036
+ }), rowData)
10037
+ });
10038
+ }
10039
+ }
10040
+ }
10041
+ }
9378
10042
  } else {
9379
10043
  const content = await DocumentParser.parse(file, file.name, file.type);
9380
10044
  documents.push({