echo-ai-sdk-ts 2.3.1 → 2.4.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.
package/README.md CHANGED
@@ -113,6 +113,41 @@ bot.use(async ({ sessionId, message }) => {
113
113
  });
114
114
  ```
115
115
 
116
+
117
+ ### 🚀 Tier 2 Pro-Grade Features
118
+
119
+ #### 🌐 Omnichannel Sync
120
+ Connect your bot to Slack and Telegram while maintaining a single user context.
121
+ ```typescript
122
+ import { TelegramAdapter, SlackAdapter } from "echo-ai-sdk";
123
+
124
+ const tg = new TelegramAdapter({ bot, token: "TG_TOKEN" });
125
+ await tg.start();
126
+
127
+ const slack = new SlackAdapter({ bot, signingSecret: "...", token: "..." });
128
+ await slack.start();
129
+ ```
130
+
131
+ #### 💾 Persistent Session Store
132
+ Move beyond memory. Use `FileSessionStore` for local persistence or implement `SessionStore` for Redis.
133
+ ```typescript
134
+ import { FileSessionStore } from "echo-ai-sdk";
135
+ const bot = new CustomerSupportBot({
136
+ sessionStore: new FileSessionStore("./sessions"),
137
+ });
138
+ ```
139
+
140
+ #### 📈 Outcome-Based Billing & ROI
141
+ Track the *real value* of your AI by recording business outcomes.
142
+ ```typescript
143
+ // Inside a tool or middleware
144
+ bot.trackOutcome(sessionId, "lead_captured", 50.0);
145
+
146
+ const stats = bot.analytics.getSnapshot();
147
+ console.log(`ROI: ${stats.roi * 100}%`);
148
+ console.log(`Value Generated: $${stats.totalValueGeneratedUsd}`);
149
+ ```
150
+
116
151
  ---
117
152
 
118
153
  ## Installation
package/dist/index.d.mts CHANGED
@@ -655,6 +655,7 @@ interface ConversationRecord {
655
655
  topQueries: string[];
656
656
  responseTimes: number[];
657
657
  model?: string;
658
+ variantId?: string;
658
659
  }
659
660
  interface AnalyticsSnapshot {
660
661
  totalConversations: number;
@@ -667,6 +668,12 @@ interface AnalyticsSnapshot {
667
668
  totalCostUsd: number;
668
669
  totalValueGeneratedUsd: number;
669
670
  roi: number;
671
+ variants?: Record<string, {
672
+ totalConversations: number;
673
+ totalValueGeneratedUsd: number;
674
+ resolutionRate: number;
675
+ roi: number;
676
+ }>;
670
677
  avgTokensPerConversation: number;
671
678
  avgCostPerConversation: number;
672
679
  topQueries: {
@@ -686,7 +693,7 @@ declare class ConversationAnalytics {
686
693
  constructor(outcomeTracker?: OutcomeTracker);
687
694
  estimateTokens(text: string): number;
688
695
  calculateCost(model: string, tokens: number, type?: "input" | "output"): number;
689
- startConversation(sessionId: string, model?: string): void;
696
+ startConversation(sessionId: string, model?: string, variantId?: string): void;
690
697
  recordQuery(sessionId: string, query: string): void;
691
698
  recordResponse(sessionId: string, reply: string, latencyMs: number): void;
692
699
  markResolved(sessionId: string): void;
@@ -784,6 +791,43 @@ declare class FileSessionStore implements SessionStore {
784
791
  clear(): Promise<void>;
785
792
  }
786
793
 
794
+ interface RedactionRule {
795
+ name: string;
796
+ pattern: RegExp;
797
+ placeholder: string;
798
+ }
799
+ declare const DEFAULT_REDACTION_RULES: RedactionRule[];
800
+ declare class PIIRedactor {
801
+ private rules;
802
+ constructor(rules?: RedactionRule[]);
803
+ /** Scans text and replaces any matched patterns with their placeholders. */
804
+ redact(text: string): string;
805
+ /** Adds custom redaction rules to the engine. */
806
+ addRule(rule: RedactionRule): void;
807
+ }
808
+
809
+ interface ExperimentVariant {
810
+ id: string;
811
+ weight: number;
812
+ config: Record<string, any>;
813
+ }
814
+ interface Experiment {
815
+ name: string;
816
+ variants: ExperimentVariant[];
817
+ }
818
+ declare class ExperimentManager {
819
+ private experiments;
820
+ constructor(experiments?: Experiment[]);
821
+ registerExperiment(exp: Experiment): void;
822
+ /**
823
+ * Assigns a user to a specific variant based on deterministic hashing of their sessionId.
824
+ * This guarantees 'sticky sessions' where the same user always gets the same A/B test variant.
825
+ */
826
+ assignVariant(experimentName: string, sessionId: string): string | null;
827
+ /** Retrieves the variant config object for an assigned variant id. */
828
+ getVariantConfig(experimentName: string, variantId: string): Record<string, any> | null;
829
+ }
830
+
787
831
  interface SupportBotConfig {
788
832
  gateway: AIModelGateway;
789
833
  companyName: string;
@@ -798,6 +842,8 @@ interface SupportBotConfig {
798
842
  greeting?: string;
799
843
  systemPrompt?: string;
800
844
  maxIterations?: number;
845
+ enablePIIRedaction?: boolean;
846
+ experiments?: Experiment[];
801
847
  }
802
848
  type BotMiddleware = (ctx: {
803
849
  sessionId: string;
@@ -817,7 +863,10 @@ declare class CustomerSupportBot {
817
863
  outcomeTracker: OutcomeTracker;
818
864
  sessionStore: SessionStore;
819
865
  handoff?: HandoffManager;
866
+ piiRedactor?: PIIRedactor;
867
+ experimentManager?: ExperimentManager;
820
868
  greeting: string;
869
+ private defaultSystemPrompt;
821
870
  private middlewares;
822
871
  constructor(config: SupportBotConfig);
823
872
  /** Register a middleware function to run before every chat turn. */
@@ -933,4 +982,4 @@ declare class TelegramAdapter extends ChannelAdapter {
933
982
  private sendMessage;
934
983
  }
935
984
 
936
- export { AIModelGateway, APIConnector, type APIConnectorConfig, AgentExecutor, AgentIterationLimitError, AgentPipeline, AgentRouter, type AgentTelemetry, type AnalyticsSnapshot, type BaseMemoryStore, BaseProvider, BaseSTTProvider, BaseSpeakerRecognizer, BaseTTSProvider, type BotMiddleware, CachedGateway, ChannelAdapter, type ChannelConfig, ChatAgent, type ChatMessage, ChatMessageSchema, type ChatRequest, ChatRequestSchema, type ChatResponse, ChatResponseSchema, ChatWidget, type ChatWidgetConfig, type ChatWidgetTheme, type ChunkOptions, ConfigurationError, ConversationAnalytics, type ConversationRecord, CustomerSupportBot, EchoAI, EchoVoice, type EscalationTrigger, type FetchResult, FileSessionStore, type GatewayMiddleware, GatewayRoutingError, type HandoffConfig, type HandoffEvent, HandoffManager, type IdentificationResult, InMemoryStore, KnowledgeBase, type KnowledgeBaseConfig, MemorySessionStore, MemoryVectorStore, OPENAI_PRICING, OpenAITTS, OpenAIWhisperSTT, type OutcomeRecord, OutcomeTracker, PromptRegistry, PromptTemplate, PromptVersionError, ProviderDependencyError, type STTOptions, type SearchResult, type ServerConfig, type SessionStore, SlackAdapter, type SlackConfig, type SpeakerProfile, StructuredOutputError, type SupportBotConfig, type TTSFormat, type TTSOptions, type TTSResult, type TTSVoice, TelegramAdapter, type TelegramConfig, ToolAgent, type ToolContext, ToolExecutionError, type TranscriptionResult, type TranscriptionSegment, type UsageMetrics, UsageMetricsSchema, ValidationError, type VectorEntry, type VerificationResult, VoiceprintStore, applyRequestMiddleware, applyResponseMiddleware, calculatorTool, chunkText, createChatHandler, createTool, dateTimeTool, startChatServer, webSearchTool };
985
+ export { AIModelGateway, APIConnector, type APIConnectorConfig, AgentExecutor, AgentIterationLimitError, AgentPipeline, AgentRouter, type AgentTelemetry, type AnalyticsSnapshot, type BaseMemoryStore, BaseProvider, BaseSTTProvider, BaseSpeakerRecognizer, BaseTTSProvider, type BotMiddleware, CachedGateway, ChannelAdapter, type ChannelConfig, ChatAgent, type ChatMessage, ChatMessageSchema, type ChatRequest, ChatRequestSchema, type ChatResponse, ChatResponseSchema, ChatWidget, type ChatWidgetConfig, type ChatWidgetTheme, type ChunkOptions, ConfigurationError, ConversationAnalytics, type ConversationRecord, CustomerSupportBot, DEFAULT_REDACTION_RULES, EchoAI, EchoVoice, type EscalationTrigger, type Experiment, ExperimentManager, type ExperimentVariant, type FetchResult, FileSessionStore, type GatewayMiddleware, GatewayRoutingError, type HandoffConfig, type HandoffEvent, HandoffManager, type IdentificationResult, InMemoryStore, KnowledgeBase, type KnowledgeBaseConfig, MemorySessionStore, MemoryVectorStore, OPENAI_PRICING, OpenAITTS, OpenAIWhisperSTT, type OutcomeRecord, OutcomeTracker, PIIRedactor, PromptRegistry, PromptTemplate, PromptVersionError, ProviderDependencyError, type RedactionRule, type STTOptions, type SearchResult, type ServerConfig, type SessionStore, SlackAdapter, type SlackConfig, type SpeakerProfile, StructuredOutputError, type SupportBotConfig, type TTSFormat, type TTSOptions, type TTSResult, type TTSVoice, TelegramAdapter, type TelegramConfig, ToolAgent, type ToolContext, ToolExecutionError, type TranscriptionResult, type TranscriptionSegment, type UsageMetrics, UsageMetricsSchema, ValidationError, type VectorEntry, type VerificationResult, VoiceprintStore, applyRequestMiddleware, applyResponseMiddleware, calculatorTool, chunkText, createChatHandler, createTool, dateTimeTool, startChatServer, webSearchTool };
package/dist/index.d.ts CHANGED
@@ -655,6 +655,7 @@ interface ConversationRecord {
655
655
  topQueries: string[];
656
656
  responseTimes: number[];
657
657
  model?: string;
658
+ variantId?: string;
658
659
  }
659
660
  interface AnalyticsSnapshot {
660
661
  totalConversations: number;
@@ -667,6 +668,12 @@ interface AnalyticsSnapshot {
667
668
  totalCostUsd: number;
668
669
  totalValueGeneratedUsd: number;
669
670
  roi: number;
671
+ variants?: Record<string, {
672
+ totalConversations: number;
673
+ totalValueGeneratedUsd: number;
674
+ resolutionRate: number;
675
+ roi: number;
676
+ }>;
670
677
  avgTokensPerConversation: number;
671
678
  avgCostPerConversation: number;
672
679
  topQueries: {
@@ -686,7 +693,7 @@ declare class ConversationAnalytics {
686
693
  constructor(outcomeTracker?: OutcomeTracker);
687
694
  estimateTokens(text: string): number;
688
695
  calculateCost(model: string, tokens: number, type?: "input" | "output"): number;
689
- startConversation(sessionId: string, model?: string): void;
696
+ startConversation(sessionId: string, model?: string, variantId?: string): void;
690
697
  recordQuery(sessionId: string, query: string): void;
691
698
  recordResponse(sessionId: string, reply: string, latencyMs: number): void;
692
699
  markResolved(sessionId: string): void;
@@ -784,6 +791,43 @@ declare class FileSessionStore implements SessionStore {
784
791
  clear(): Promise<void>;
785
792
  }
786
793
 
794
+ interface RedactionRule {
795
+ name: string;
796
+ pattern: RegExp;
797
+ placeholder: string;
798
+ }
799
+ declare const DEFAULT_REDACTION_RULES: RedactionRule[];
800
+ declare class PIIRedactor {
801
+ private rules;
802
+ constructor(rules?: RedactionRule[]);
803
+ /** Scans text and replaces any matched patterns with their placeholders. */
804
+ redact(text: string): string;
805
+ /** Adds custom redaction rules to the engine. */
806
+ addRule(rule: RedactionRule): void;
807
+ }
808
+
809
+ interface ExperimentVariant {
810
+ id: string;
811
+ weight: number;
812
+ config: Record<string, any>;
813
+ }
814
+ interface Experiment {
815
+ name: string;
816
+ variants: ExperimentVariant[];
817
+ }
818
+ declare class ExperimentManager {
819
+ private experiments;
820
+ constructor(experiments?: Experiment[]);
821
+ registerExperiment(exp: Experiment): void;
822
+ /**
823
+ * Assigns a user to a specific variant based on deterministic hashing of their sessionId.
824
+ * This guarantees 'sticky sessions' where the same user always gets the same A/B test variant.
825
+ */
826
+ assignVariant(experimentName: string, sessionId: string): string | null;
827
+ /** Retrieves the variant config object for an assigned variant id. */
828
+ getVariantConfig(experimentName: string, variantId: string): Record<string, any> | null;
829
+ }
830
+
787
831
  interface SupportBotConfig {
788
832
  gateway: AIModelGateway;
789
833
  companyName: string;
@@ -798,6 +842,8 @@ interface SupportBotConfig {
798
842
  greeting?: string;
799
843
  systemPrompt?: string;
800
844
  maxIterations?: number;
845
+ enablePIIRedaction?: boolean;
846
+ experiments?: Experiment[];
801
847
  }
802
848
  type BotMiddleware = (ctx: {
803
849
  sessionId: string;
@@ -817,7 +863,10 @@ declare class CustomerSupportBot {
817
863
  outcomeTracker: OutcomeTracker;
818
864
  sessionStore: SessionStore;
819
865
  handoff?: HandoffManager;
866
+ piiRedactor?: PIIRedactor;
867
+ experimentManager?: ExperimentManager;
820
868
  greeting: string;
869
+ private defaultSystemPrompt;
821
870
  private middlewares;
822
871
  constructor(config: SupportBotConfig);
823
872
  /** Register a middleware function to run before every chat turn. */
@@ -933,4 +982,4 @@ declare class TelegramAdapter extends ChannelAdapter {
933
982
  private sendMessage;
934
983
  }
935
984
 
936
- export { AIModelGateway, APIConnector, type APIConnectorConfig, AgentExecutor, AgentIterationLimitError, AgentPipeline, AgentRouter, type AgentTelemetry, type AnalyticsSnapshot, type BaseMemoryStore, BaseProvider, BaseSTTProvider, BaseSpeakerRecognizer, BaseTTSProvider, type BotMiddleware, CachedGateway, ChannelAdapter, type ChannelConfig, ChatAgent, type ChatMessage, ChatMessageSchema, type ChatRequest, ChatRequestSchema, type ChatResponse, ChatResponseSchema, ChatWidget, type ChatWidgetConfig, type ChatWidgetTheme, type ChunkOptions, ConfigurationError, ConversationAnalytics, type ConversationRecord, CustomerSupportBot, EchoAI, EchoVoice, type EscalationTrigger, type FetchResult, FileSessionStore, type GatewayMiddleware, GatewayRoutingError, type HandoffConfig, type HandoffEvent, HandoffManager, type IdentificationResult, InMemoryStore, KnowledgeBase, type KnowledgeBaseConfig, MemorySessionStore, MemoryVectorStore, OPENAI_PRICING, OpenAITTS, OpenAIWhisperSTT, type OutcomeRecord, OutcomeTracker, PromptRegistry, PromptTemplate, PromptVersionError, ProviderDependencyError, type STTOptions, type SearchResult, type ServerConfig, type SessionStore, SlackAdapter, type SlackConfig, type SpeakerProfile, StructuredOutputError, type SupportBotConfig, type TTSFormat, type TTSOptions, type TTSResult, type TTSVoice, TelegramAdapter, type TelegramConfig, ToolAgent, type ToolContext, ToolExecutionError, type TranscriptionResult, type TranscriptionSegment, type UsageMetrics, UsageMetricsSchema, ValidationError, type VectorEntry, type VerificationResult, VoiceprintStore, applyRequestMiddleware, applyResponseMiddleware, calculatorTool, chunkText, createChatHandler, createTool, dateTimeTool, startChatServer, webSearchTool };
985
+ export { AIModelGateway, APIConnector, type APIConnectorConfig, AgentExecutor, AgentIterationLimitError, AgentPipeline, AgentRouter, type AgentTelemetry, type AnalyticsSnapshot, type BaseMemoryStore, BaseProvider, BaseSTTProvider, BaseSpeakerRecognizer, BaseTTSProvider, type BotMiddleware, CachedGateway, ChannelAdapter, type ChannelConfig, ChatAgent, type ChatMessage, ChatMessageSchema, type ChatRequest, ChatRequestSchema, type ChatResponse, ChatResponseSchema, ChatWidget, type ChatWidgetConfig, type ChatWidgetTheme, type ChunkOptions, ConfigurationError, ConversationAnalytics, type ConversationRecord, CustomerSupportBot, DEFAULT_REDACTION_RULES, EchoAI, EchoVoice, type EscalationTrigger, type Experiment, ExperimentManager, type ExperimentVariant, type FetchResult, FileSessionStore, type GatewayMiddleware, GatewayRoutingError, type HandoffConfig, type HandoffEvent, HandoffManager, type IdentificationResult, InMemoryStore, KnowledgeBase, type KnowledgeBaseConfig, MemorySessionStore, MemoryVectorStore, OPENAI_PRICING, OpenAITTS, OpenAIWhisperSTT, type OutcomeRecord, OutcomeTracker, PIIRedactor, PromptRegistry, PromptTemplate, PromptVersionError, ProviderDependencyError, type RedactionRule, type STTOptions, type SearchResult, type ServerConfig, type SessionStore, SlackAdapter, type SlackConfig, type SpeakerProfile, StructuredOutputError, type SupportBotConfig, type TTSFormat, type TTSOptions, type TTSResult, type TTSVoice, TelegramAdapter, type TelegramConfig, ToolAgent, type ToolContext, ToolExecutionError, type TranscriptionResult, type TranscriptionSegment, type UsageMetrics, UsageMetricsSchema, ValidationError, type VectorEntry, type VerificationResult, VoiceprintStore, applyRequestMiddleware, applyResponseMiddleware, calculatorTool, chunkText, createChatHandler, createTool, dateTimeTool, startChatServer, webSearchTool };
package/dist/index.js CHANGED
@@ -9503,8 +9503,10 @@ __export(index_exports, {
9503
9503
  ConfigurationError: () => ConfigurationError,
9504
9504
  ConversationAnalytics: () => ConversationAnalytics,
9505
9505
  CustomerSupportBot: () => CustomerSupportBot,
9506
+ DEFAULT_REDACTION_RULES: () => DEFAULT_REDACTION_RULES,
9506
9507
  EchoAI: () => EchoAI,
9507
9508
  EchoVoice: () => EchoVoice,
9509
+ ExperimentManager: () => ExperimentManager,
9508
9510
  FileSessionStore: () => FileSessionStore,
9509
9511
  GatewayRoutingError: () => GatewayRoutingError,
9510
9512
  HandoffManager: () => HandoffManager,
@@ -9516,6 +9518,7 @@ __export(index_exports, {
9516
9518
  OpenAITTS: () => OpenAITTS,
9517
9519
  OpenAIWhisperSTT: () => OpenAIWhisperSTT,
9518
9520
  OutcomeTracker: () => OutcomeTracker,
9521
+ PIIRedactor: () => PIIRedactor,
9519
9522
  PromptRegistry: () => PromptRegistry,
9520
9523
  PromptTemplate: () => PromptTemplate,
9521
9524
  PromptVersionError: () => PromptVersionError,
@@ -15559,7 +15562,7 @@ var ConversationAnalytics = class {
15559
15562
  const rate = type === "input" ? pricing.input : pricing.output;
15560
15563
  return tokens / 1e6 * rate;
15561
15564
  }
15562
- startConversation(sessionId, model = "gpt-4o-mini") {
15565
+ startConversation(sessionId, model = "gpt-4o-mini", variantId) {
15563
15566
  if (!this.records.has(sessionId)) {
15564
15567
  this.records.set(sessionId, {
15565
15568
  sessionId,
@@ -15571,7 +15574,8 @@ var ConversationAnalytics = class {
15571
15574
  totalCostUsd: 0,
15572
15575
  topQueries: [],
15573
15576
  responseTimes: [],
15574
- model
15577
+ model,
15578
+ variantId
15575
15579
  });
15576
15580
  }
15577
15581
  }
@@ -15636,6 +15640,31 @@ var ConversationAnalytics = class {
15636
15640
  const firstConvo = all.length > 0 ? Math.min(...all.map((r2) => r2.startTime)) : now;
15637
15641
  const hourSpan = Math.max(1, (now - firstConvo) / 36e5);
15638
15642
  const topQueries = [...this.queryFrequency.entries()].sort((a2, b2) => b2[1] - a2[1]).slice(0, 20).map(([query, count]) => ({ query, count }));
15643
+ const variants = {};
15644
+ const groupedByVariant = all.reduce((acc, r2) => {
15645
+ const v2 = r2.variantId || "default";
15646
+ if (!acc[v2]) acc[v2] = [];
15647
+ acc[v2].push(r2);
15648
+ return acc;
15649
+ }, {});
15650
+ for (const [variantId, vRecords] of Object.entries(groupedByVariant)) {
15651
+ const vTotal = vRecords.length;
15652
+ const vResolved = vRecords.filter((r2) => r2.resolved).length;
15653
+ let vValue = 0;
15654
+ if (this.outcomeTracker) {
15655
+ vRecords.forEach((r2) => {
15656
+ const outRecords = this.outcomeTracker.outcomes?.get(r2.sessionId) || [];
15657
+ vValue += outRecords.reduce((s2, o2) => s2 + o2.valueUsd, 0);
15658
+ });
15659
+ }
15660
+ const vCost = vRecords.reduce((s2, r2) => s2 + r2.totalCostUsd, 0);
15661
+ variants[variantId] = {
15662
+ totalConversations: vTotal,
15663
+ totalValueGeneratedUsd: vValue,
15664
+ resolutionRate: vTotal > 0 ? vResolved / vTotal : 0,
15665
+ roi: vCost > 0 ? (vValue - vCost) / vCost : 0
15666
+ };
15667
+ }
15639
15668
  return {
15640
15669
  totalConversations: total,
15641
15670
  resolvedCount: resolved,
@@ -15905,6 +15934,93 @@ var OutcomeTracker = class {
15905
15934
  }
15906
15935
  };
15907
15936
 
15937
+ // src/analytics/redact.ts
15938
+ var DEFAULT_REDACTION_RULES = [
15939
+ {
15940
+ name: "email",
15941
+ pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
15942
+ placeholder: "[REDACTED_EMAIL]"
15943
+ },
15944
+ {
15945
+ name: "phone",
15946
+ pattern: /\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
15947
+ placeholder: "[REDACTED_PHONE]"
15948
+ },
15949
+ {
15950
+ name: "credit_card",
15951
+ // Standard 16 digit match with optional dashes or spaces
15952
+ pattern: /\b(?:\d[ -]*?){13,16}\b/g,
15953
+ placeholder: "[REDACTED_CC]"
15954
+ },
15955
+ {
15956
+ name: "ssn",
15957
+ // Standard US SSN
15958
+ pattern: /\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g,
15959
+ placeholder: "[REDACTED_SSN]"
15960
+ }
15961
+ ];
15962
+ var PIIRedactor = class {
15963
+ rules;
15964
+ constructor(rules) {
15965
+ this.rules = rules || DEFAULT_REDACTION_RULES;
15966
+ }
15967
+ /** Scans text and replaces any matched patterns with their placeholders. */
15968
+ redact(text) {
15969
+ if (!text) return text;
15970
+ let redacted = text;
15971
+ for (const rule of this.rules) {
15972
+ redacted = redacted.replace(rule.pattern, rule.placeholder);
15973
+ }
15974
+ return redacted;
15975
+ }
15976
+ /** Adds custom redaction rules to the engine. */
15977
+ addRule(rule) {
15978
+ this.rules.push(rule);
15979
+ }
15980
+ };
15981
+
15982
+ // src/analytics/experiment.ts
15983
+ var crypto2 = __toESM(require("crypto"));
15984
+ var ExperimentManager = class {
15985
+ experiments = /* @__PURE__ */ new Map();
15986
+ constructor(experiments) {
15987
+ if (experiments) {
15988
+ experiments.forEach((e2) => this.registerExperiment(e2));
15989
+ }
15990
+ }
15991
+ registerExperiment(exp) {
15992
+ this.experiments.set(exp.name, exp);
15993
+ }
15994
+ /**
15995
+ * Assigns a user to a specific variant based on deterministic hashing of their sessionId.
15996
+ * This guarantees 'sticky sessions' where the same user always gets the same A/B test variant.
15997
+ */
15998
+ assignVariant(experimentName, sessionId) {
15999
+ const exp = this.experiments.get(experimentName);
16000
+ if (!exp || exp.variants.length === 0) return null;
16001
+ if (exp.variants.length === 1) return exp.variants[0].id;
16002
+ const totalWeight = exp.variants.reduce((sum, v2) => sum + v2.weight, 0);
16003
+ const hash = crypto2.createHash("md5").update(`${experimentName}_${sessionId}`).digest("hex");
16004
+ const hashInt = parseInt(hash.slice(0, 8), 16);
16005
+ const bucket = hashInt % totalWeight;
16006
+ let cumulative = 0;
16007
+ for (const variant of exp.variants) {
16008
+ cumulative += variant.weight;
16009
+ if (bucket < cumulative) {
16010
+ return variant.id;
16011
+ }
16012
+ }
16013
+ return exp.variants[0].id;
16014
+ }
16015
+ /** Retrieves the variant config object for an assigned variant id. */
16016
+ getVariantConfig(experimentName, variantId) {
16017
+ const exp = this.experiments.get(experimentName);
16018
+ if (!exp) return null;
16019
+ const v2 = exp.variants.find((v3) => v3.id === variantId);
16020
+ return v2 ? v2.config : null;
16021
+ }
16022
+ };
16023
+
15908
16024
  // src/widget/bot.ts
15909
16025
  var CustomerSupportBot = class {
15910
16026
  executor;
@@ -15914,7 +16030,10 @@ var CustomerSupportBot = class {
15914
16030
  outcomeTracker;
15915
16031
  sessionStore;
15916
16032
  handoff;
16033
+ piiRedactor;
16034
+ experimentManager;
15917
16035
  greeting;
16036
+ defaultSystemPrompt;
15918
16037
  middlewares = [];
15919
16038
  constructor(config) {
15920
16039
  this.outcomeTracker = new OutcomeTracker();
@@ -15926,6 +16045,12 @@ var CustomerSupportBot = class {
15926
16045
  if (config.handoff) {
15927
16046
  this.handoff = new HandoffManager(config.handoff);
15928
16047
  }
16048
+ if (config.enablePIIRedaction) {
16049
+ this.piiRedactor = new PIIRedactor();
16050
+ }
16051
+ if (config.experiments && config.experiments.length > 0) {
16052
+ this.experimentManager = new ExperimentManager(config.experiments);
16053
+ }
15929
16054
  const tools = [];
15930
16055
  if (config.apiConnector) {
15931
16056
  this.connector = new APIConnector(config.apiConnector);
@@ -15957,11 +16082,12 @@ Your role:
15957
16082
  - If you can't find an answer, politely suggest contacting human support
15958
16083
  - Always maintain a warm, professional tone
15959
16084
  - Never make up data \u2014 always verify through the API or context when available`;
16085
+ this.defaultSystemPrompt = config.systemPrompt || defaultSystemPrompt;
15960
16086
  this.executor = new AgentExecutor({
15961
16087
  gateway: config.gateway,
15962
16088
  memory: config.memory || new InMemoryStore(100),
15963
16089
  tools,
15964
- systemPrompt: config.systemPrompt || defaultSystemPrompt,
16090
+ systemPrompt: this.defaultSystemPrompt,
15965
16091
  telemetry: config.telemetry
15966
16092
  });
15967
16093
  this.greeting = config.greeting || `Hello! \u{1F44B} Welcome to ${config.companyName}. How can I help you today?`;
@@ -15976,15 +16102,28 @@ Your role:
15976
16102
  }
15977
16103
  /** Process a customer message and return the bot's response. */
15978
16104
  async chat(sessionId, message) {
15979
- this.analytics.startConversation(sessionId);
15980
- this.analytics.recordQuery(sessionId, message);
16105
+ const activeMessage = this.piiRedactor ? this.piiRedactor.redact(message) : message;
16106
+ let variantId;
16107
+ if (this.experimentManager) {
16108
+ variantId = this.experimentManager.assignVariant("default_experiment", sessionId) || void 0;
16109
+ if (variantId) {
16110
+ const config = this.experimentManager.getVariantConfig("default_experiment", variantId);
16111
+ if (config && config.systemPrompt) {
16112
+ this.executor.systemPrompt = config.systemPrompt;
16113
+ } else {
16114
+ this.executor.systemPrompt = this.defaultSystemPrompt;
16115
+ }
16116
+ }
16117
+ }
16118
+ this.analytics.startConversation(sessionId, "gpt-4o-mini", variantId);
16119
+ this.analytics.recordQuery(sessionId, activeMessage);
15981
16120
  const startTime = Date.now();
15982
16121
  for (const mw of this.middlewares) {
15983
- const result2 = await mw({ sessionId, message, bot: this });
16122
+ const result2 = await mw({ sessionId, message: activeMessage, bot: this });
15984
16123
  if (typeof result2 === "string") return result2;
15985
16124
  }
15986
16125
  if (this.handoff) {
15987
- const trigger = this.handoff.shouldEscalate(sessionId, message);
16126
+ const trigger = this.handoff.shouldEscalate(sessionId, activeMessage);
15988
16127
  if (trigger) {
15989
16128
  const history = await this.executor.memory.getMessages(sessionId);
15990
16129
  let summary = "Customer needs assistance.";
@@ -15992,23 +16131,23 @@ Your role:
15992
16131
  summary = await this.executor.gateway.chat([
15993
16132
  { role: "system", content: "Summarize the customer's problem in one concise sentence for a human support agent context." },
15994
16133
  ...history,
15995
- { role: "user", content: message }
16134
+ { role: "user", content: activeMessage }
15996
16135
  ]);
15997
16136
  } catch {
15998
16137
  }
15999
- const reply = await this.handoff.escalate(sessionId, trigger, history, message, summary);
16138
+ const reply = await this.handoff.escalate(sessionId, trigger, history, activeMessage, summary);
16000
16139
  this.analytics.markHandedOff(sessionId);
16001
16140
  return reply;
16002
16141
  }
16003
16142
  }
16004
- let enhancedMessage = message;
16143
+ let enhancedMessage = activeMessage;
16005
16144
  if (this.knowledgeBase) {
16006
- const context = await this.knowledgeBase.query(message);
16145
+ const context = await this.knowledgeBase.query(activeMessage);
16007
16146
  if (context) {
16008
16147
  enhancedMessage = `Context from Knowledge Base:
16009
16148
  ${context}
16010
16149
 
16011
- User Question: ${message}`;
16150
+ User Question: ${activeMessage}`;
16012
16151
  }
16013
16152
  }
16014
16153
  try {
@@ -16019,7 +16158,7 @@ User Question: ${message}`;
16019
16158
  return reply;
16020
16159
  } catch (e2) {
16021
16160
  if (this.handoff) {
16022
- return this.handoff.escalate(sessionId, "low_confidence", [], message, `Error: ${e2.message}`);
16161
+ return this.handoff.escalate(sessionId, "low_confidence", [], activeMessage, `Error: ${e2.message}`);
16023
16162
  }
16024
16163
  throw e2;
16025
16164
  }
@@ -16326,8 +16465,10 @@ var TelegramAdapter = class extends ChannelAdapter {
16326
16465
  ConfigurationError,
16327
16466
  ConversationAnalytics,
16328
16467
  CustomerSupportBot,
16468
+ DEFAULT_REDACTION_RULES,
16329
16469
  EchoAI,
16330
16470
  EchoVoice,
16471
+ ExperimentManager,
16331
16472
  FileSessionStore,
16332
16473
  GatewayRoutingError,
16333
16474
  HandoffManager,
@@ -16339,6 +16480,7 @@ var TelegramAdapter = class extends ChannelAdapter {
16339
16480
  OpenAITTS,
16340
16481
  OpenAIWhisperSTT,
16341
16482
  OutcomeTracker,
16483
+ PIIRedactor,
16342
16484
  PromptRegistry,
16343
16485
  PromptTemplate,
16344
16486
  PromptVersionError,
package/dist/index.mjs CHANGED
@@ -13270,7 +13270,7 @@ var ConversationAnalytics = class {
13270
13270
  const rate = type === "input" ? pricing.input : pricing.output;
13271
13271
  return tokens / 1e6 * rate;
13272
13272
  }
13273
- startConversation(sessionId, model = "gpt-4o-mini") {
13273
+ startConversation(sessionId, model = "gpt-4o-mini", variantId) {
13274
13274
  if (!this.records.has(sessionId)) {
13275
13275
  this.records.set(sessionId, {
13276
13276
  sessionId,
@@ -13282,7 +13282,8 @@ var ConversationAnalytics = class {
13282
13282
  totalCostUsd: 0,
13283
13283
  topQueries: [],
13284
13284
  responseTimes: [],
13285
- model
13285
+ model,
13286
+ variantId
13286
13287
  });
13287
13288
  }
13288
13289
  }
@@ -13347,6 +13348,31 @@ var ConversationAnalytics = class {
13347
13348
  const firstConvo = all.length > 0 ? Math.min(...all.map((r) => r.startTime)) : now;
13348
13349
  const hourSpan = Math.max(1, (now - firstConvo) / 36e5);
13349
13350
  const topQueries = [...this.queryFrequency.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([query, count]) => ({ query, count }));
13351
+ const variants = {};
13352
+ const groupedByVariant = all.reduce((acc, r) => {
13353
+ const v = r.variantId || "default";
13354
+ if (!acc[v]) acc[v] = [];
13355
+ acc[v].push(r);
13356
+ return acc;
13357
+ }, {});
13358
+ for (const [variantId, vRecords] of Object.entries(groupedByVariant)) {
13359
+ const vTotal = vRecords.length;
13360
+ const vResolved = vRecords.filter((r) => r.resolved).length;
13361
+ let vValue = 0;
13362
+ if (this.outcomeTracker) {
13363
+ vRecords.forEach((r) => {
13364
+ const outRecords = this.outcomeTracker.outcomes?.get(r.sessionId) || [];
13365
+ vValue += outRecords.reduce((s, o) => s + o.valueUsd, 0);
13366
+ });
13367
+ }
13368
+ const vCost = vRecords.reduce((s, r) => s + r.totalCostUsd, 0);
13369
+ variants[variantId] = {
13370
+ totalConversations: vTotal,
13371
+ totalValueGeneratedUsd: vValue,
13372
+ resolutionRate: vTotal > 0 ? vResolved / vTotal : 0,
13373
+ roi: vCost > 0 ? (vValue - vCost) / vCost : 0
13374
+ };
13375
+ }
13350
13376
  return {
13351
13377
  totalConversations: total,
13352
13378
  resolvedCount: resolved,
@@ -13616,6 +13642,93 @@ var OutcomeTracker = class {
13616
13642
  }
13617
13643
  };
13618
13644
 
13645
+ // src/analytics/redact.ts
13646
+ var DEFAULT_REDACTION_RULES = [
13647
+ {
13648
+ name: "email",
13649
+ pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
13650
+ placeholder: "[REDACTED_EMAIL]"
13651
+ },
13652
+ {
13653
+ name: "phone",
13654
+ pattern: /\b(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
13655
+ placeholder: "[REDACTED_PHONE]"
13656
+ },
13657
+ {
13658
+ name: "credit_card",
13659
+ // Standard 16 digit match with optional dashes or spaces
13660
+ pattern: /\b(?:\d[ -]*?){13,16}\b/g,
13661
+ placeholder: "[REDACTED_CC]"
13662
+ },
13663
+ {
13664
+ name: "ssn",
13665
+ // Standard US SSN
13666
+ pattern: /\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g,
13667
+ placeholder: "[REDACTED_SSN]"
13668
+ }
13669
+ ];
13670
+ var PIIRedactor = class {
13671
+ rules;
13672
+ constructor(rules) {
13673
+ this.rules = rules || DEFAULT_REDACTION_RULES;
13674
+ }
13675
+ /** Scans text and replaces any matched patterns with their placeholders. */
13676
+ redact(text) {
13677
+ if (!text) return text;
13678
+ let redacted = text;
13679
+ for (const rule of this.rules) {
13680
+ redacted = redacted.replace(rule.pattern, rule.placeholder);
13681
+ }
13682
+ return redacted;
13683
+ }
13684
+ /** Adds custom redaction rules to the engine. */
13685
+ addRule(rule) {
13686
+ this.rules.push(rule);
13687
+ }
13688
+ };
13689
+
13690
+ // src/analytics/experiment.ts
13691
+ import * as crypto2 from "crypto";
13692
+ var ExperimentManager = class {
13693
+ experiments = /* @__PURE__ */ new Map();
13694
+ constructor(experiments) {
13695
+ if (experiments) {
13696
+ experiments.forEach((e) => this.registerExperiment(e));
13697
+ }
13698
+ }
13699
+ registerExperiment(exp) {
13700
+ this.experiments.set(exp.name, exp);
13701
+ }
13702
+ /**
13703
+ * Assigns a user to a specific variant based on deterministic hashing of their sessionId.
13704
+ * This guarantees 'sticky sessions' where the same user always gets the same A/B test variant.
13705
+ */
13706
+ assignVariant(experimentName, sessionId) {
13707
+ const exp = this.experiments.get(experimentName);
13708
+ if (!exp || exp.variants.length === 0) return null;
13709
+ if (exp.variants.length === 1) return exp.variants[0].id;
13710
+ const totalWeight = exp.variants.reduce((sum, v) => sum + v.weight, 0);
13711
+ const hash = crypto2.createHash("md5").update(`${experimentName}_${sessionId}`).digest("hex");
13712
+ const hashInt = parseInt(hash.slice(0, 8), 16);
13713
+ const bucket = hashInt % totalWeight;
13714
+ let cumulative = 0;
13715
+ for (const variant of exp.variants) {
13716
+ cumulative += variant.weight;
13717
+ if (bucket < cumulative) {
13718
+ return variant.id;
13719
+ }
13720
+ }
13721
+ return exp.variants[0].id;
13722
+ }
13723
+ /** Retrieves the variant config object for an assigned variant id. */
13724
+ getVariantConfig(experimentName, variantId) {
13725
+ const exp = this.experiments.get(experimentName);
13726
+ if (!exp) return null;
13727
+ const v = exp.variants.find((v2) => v2.id === variantId);
13728
+ return v ? v.config : null;
13729
+ }
13730
+ };
13731
+
13619
13732
  // src/widget/bot.ts
13620
13733
  var CustomerSupportBot = class {
13621
13734
  executor;
@@ -13625,7 +13738,10 @@ var CustomerSupportBot = class {
13625
13738
  outcomeTracker;
13626
13739
  sessionStore;
13627
13740
  handoff;
13741
+ piiRedactor;
13742
+ experimentManager;
13628
13743
  greeting;
13744
+ defaultSystemPrompt;
13629
13745
  middlewares = [];
13630
13746
  constructor(config) {
13631
13747
  this.outcomeTracker = new OutcomeTracker();
@@ -13637,6 +13753,12 @@ var CustomerSupportBot = class {
13637
13753
  if (config.handoff) {
13638
13754
  this.handoff = new HandoffManager(config.handoff);
13639
13755
  }
13756
+ if (config.enablePIIRedaction) {
13757
+ this.piiRedactor = new PIIRedactor();
13758
+ }
13759
+ if (config.experiments && config.experiments.length > 0) {
13760
+ this.experimentManager = new ExperimentManager(config.experiments);
13761
+ }
13640
13762
  const tools = [];
13641
13763
  if (config.apiConnector) {
13642
13764
  this.connector = new APIConnector(config.apiConnector);
@@ -13668,11 +13790,12 @@ Your role:
13668
13790
  - If you can't find an answer, politely suggest contacting human support
13669
13791
  - Always maintain a warm, professional tone
13670
13792
  - Never make up data \u2014 always verify through the API or context when available`;
13793
+ this.defaultSystemPrompt = config.systemPrompt || defaultSystemPrompt;
13671
13794
  this.executor = new AgentExecutor({
13672
13795
  gateway: config.gateway,
13673
13796
  memory: config.memory || new InMemoryStore(100),
13674
13797
  tools,
13675
- systemPrompt: config.systemPrompt || defaultSystemPrompt,
13798
+ systemPrompt: this.defaultSystemPrompt,
13676
13799
  telemetry: config.telemetry
13677
13800
  });
13678
13801
  this.greeting = config.greeting || `Hello! \u{1F44B} Welcome to ${config.companyName}. How can I help you today?`;
@@ -13687,15 +13810,28 @@ Your role:
13687
13810
  }
13688
13811
  /** Process a customer message and return the bot's response. */
13689
13812
  async chat(sessionId, message) {
13690
- this.analytics.startConversation(sessionId);
13691
- this.analytics.recordQuery(sessionId, message);
13813
+ const activeMessage = this.piiRedactor ? this.piiRedactor.redact(message) : message;
13814
+ let variantId;
13815
+ if (this.experimentManager) {
13816
+ variantId = this.experimentManager.assignVariant("default_experiment", sessionId) || void 0;
13817
+ if (variantId) {
13818
+ const config = this.experimentManager.getVariantConfig("default_experiment", variantId);
13819
+ if (config && config.systemPrompt) {
13820
+ this.executor.systemPrompt = config.systemPrompt;
13821
+ } else {
13822
+ this.executor.systemPrompt = this.defaultSystemPrompt;
13823
+ }
13824
+ }
13825
+ }
13826
+ this.analytics.startConversation(sessionId, "gpt-4o-mini", variantId);
13827
+ this.analytics.recordQuery(sessionId, activeMessage);
13692
13828
  const startTime = Date.now();
13693
13829
  for (const mw of this.middlewares) {
13694
- const result2 = await mw({ sessionId, message, bot: this });
13830
+ const result2 = await mw({ sessionId, message: activeMessage, bot: this });
13695
13831
  if (typeof result2 === "string") return result2;
13696
13832
  }
13697
13833
  if (this.handoff) {
13698
- const trigger = this.handoff.shouldEscalate(sessionId, message);
13834
+ const trigger = this.handoff.shouldEscalate(sessionId, activeMessage);
13699
13835
  if (trigger) {
13700
13836
  const history = await this.executor.memory.getMessages(sessionId);
13701
13837
  let summary = "Customer needs assistance.";
@@ -13703,23 +13839,23 @@ Your role:
13703
13839
  summary = await this.executor.gateway.chat([
13704
13840
  { role: "system", content: "Summarize the customer's problem in one concise sentence for a human support agent context." },
13705
13841
  ...history,
13706
- { role: "user", content: message }
13842
+ { role: "user", content: activeMessage }
13707
13843
  ]);
13708
13844
  } catch {
13709
13845
  }
13710
- const reply = await this.handoff.escalate(sessionId, trigger, history, message, summary);
13846
+ const reply = await this.handoff.escalate(sessionId, trigger, history, activeMessage, summary);
13711
13847
  this.analytics.markHandedOff(sessionId);
13712
13848
  return reply;
13713
13849
  }
13714
13850
  }
13715
- let enhancedMessage = message;
13851
+ let enhancedMessage = activeMessage;
13716
13852
  if (this.knowledgeBase) {
13717
- const context = await this.knowledgeBase.query(message);
13853
+ const context = await this.knowledgeBase.query(activeMessage);
13718
13854
  if (context) {
13719
13855
  enhancedMessage = `Context from Knowledge Base:
13720
13856
  ${context}
13721
13857
 
13722
- User Question: ${message}`;
13858
+ User Question: ${activeMessage}`;
13723
13859
  }
13724
13860
  }
13725
13861
  try {
@@ -13730,7 +13866,7 @@ User Question: ${message}`;
13730
13866
  return reply;
13731
13867
  } catch (e) {
13732
13868
  if (this.handoff) {
13733
- return this.handoff.escalate(sessionId, "low_confidence", [], message, `Error: ${e.message}`);
13869
+ return this.handoff.escalate(sessionId, "low_confidence", [], activeMessage, `Error: ${e.message}`);
13734
13870
  }
13735
13871
  throw e;
13736
13872
  }
@@ -14036,8 +14172,10 @@ export {
14036
14172
  ConfigurationError,
14037
14173
  ConversationAnalytics,
14038
14174
  CustomerSupportBot,
14175
+ DEFAULT_REDACTION_RULES,
14039
14176
  EchoAI,
14040
14177
  EchoVoice,
14178
+ ExperimentManager,
14041
14179
  FileSessionStore,
14042
14180
  GatewayRoutingError,
14043
14181
  HandoffManager,
@@ -14049,6 +14187,7 @@ export {
14049
14187
  OpenAITTS,
14050
14188
  OpenAIWhisperSTT,
14051
14189
  OutcomeTracker,
14190
+ PIIRedactor,
14052
14191
  PromptRegistry,
14053
14192
  PromptTemplate,
14054
14193
  PromptVersionError,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "echo-ai-sdk-ts",
3
- "version": "2.3.1",
4
- "description": "Echo AI SDK: Tier 2 Pro-Grade (Omnichannel, Outcome Billing, Session Persistence)",
3
+ "version": "2.4.0",
4
+ "description": "Echo AI SDK: Tier 3 Enterprise Premium (PII Redaction, A/B Testing, Omnichannel, RAG)",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",