adaptive-memory-multi-model-router 2.12.5 → 2.13.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.
@@ -38,6 +38,8 @@ import { BatchProcessor, executeBatch, BatchItem, BatchResult, BatchOptions, Bat
38
38
  import { routeQuery, routeBatch, recommendForTask, extractQueryFeatures, updateModelProfile, MODEL_PROFILES, QueryFeatures, ModelProfile, RouteDecision } from "./routing/advancedRouter";
39
39
  import { PrefixCache, createWarmedCache, PrefixCacheStats } from "./cache/prefixCache";
40
40
  import { SpeculativeDecoder, speculativeBatch, estimateSpeedupPotential, MedusaPredictor, EagleSpeculative, SpeculativeConfig, SpeculativeResult } from "./utils/speculativeDecoding";
41
+ import { executeEnsemble, mergeComplementary, recordFeedback, EnsembleResult, EnsembleConfig } from "./routing/ensembleVoting";
42
+ import { createPresetRouter, getPresetForQuery, DEFAULT_PRESETS, QueryPreset, PresetRouter } from "./routing/queryTypePresets";
41
43
 
42
44
  // Re-exports
43
45
  export { createTMLPD, TMLPDTools, TMLPDConfig, ExecuteResult, ParallelResult, StreamingConfig };
@@ -64,6 +66,12 @@ export { BatchProcessor, executeBatch, BatchItem, BatchResult, BatchOptions, Bat
64
66
  // Advanced routing (RouteLLM-style)
65
67
  export { routeQuery, routeBatch, recommendForTask, extractQueryFeatures, updateModelProfile, MODEL_PROFILES, QueryFeatures, ModelProfile as ModelProfileType, RouteDecision };
66
68
 
69
+ // Ensemble voting (P0) — Core differentiator: parallel multi-LLM execution with confidence merging
70
+ export { executeEnsemble, mergeComplementary, recordFeedback, EnsembleResult, EnsembleConfig };
71
+
72
+ // Query-type presets (P1) — Configurable provider+temp profiles per query type
73
+ export { createPresetRouter, getPresetForQuery, DEFAULT_PRESETS, QueryPreset, PresetRouter };
74
+
67
75
  // Prefix caching (RadixAttention-style)
68
76
  export { PrefixCache, createWarmedCache, PrefixCacheStats };
69
77
 
@@ -44,10 +44,13 @@ export class EpisodicMemoryStore {
44
44
  private entries: EpisodicEntry[] = [];
45
45
  private maxEntries: number;
46
46
  private keywordIndex: Map<string, string[]>;
47
+ private persistencePath: string | null;
47
48
 
48
- constructor(maxEntries = 1000) {
49
+ constructor(maxEntries = 1000, persistencePath?: string) {
49
50
  this.maxEntries = maxEntries;
50
51
  this.keywordIndex = new Map();
52
+ this.persistencePath = persistencePath || null;
53
+ if (this.persistencePath) this.loadFromDisk();
51
54
  }
52
55
 
53
56
  /**
@@ -62,6 +65,7 @@ export class EpisodicMemoryStore {
62
65
  };
63
66
 
64
67
  this.entries.push(fullEntry);
68
+ this.autoPersist();
65
69
 
66
70
  // Index keywords
67
71
  if (entry.task.description) {
@@ -162,11 +166,79 @@ export class EpisodicMemoryStore {
162
166
  }
163
167
 
164
168
  /**
165
- * Clear all memories
169
+ * Persist current memory to disk as JSON
170
+ */
171
+ saveToDisk(): boolean {
172
+ if (!this.persistencePath) return false;
173
+ try {
174
+ const fs = require('fs');
175
+ const data = JSON.stringify({ entries: this.entries, maxEntries: this.maxEntries }, null, 2);
176
+ fs.writeFileSync(this.persistencePath, data, 'utf8');
177
+ return true;
178
+ } catch (e) {
179
+ console.error('❌ Memory persist failed:', (e as Error).message);
180
+ return false;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Load memory from disk JSON
186
+ */
187
+ loadFromDisk(): boolean {
188
+ if (!this.persistencePath) return false;
189
+ try {
190
+ const fs = require('fs');
191
+ if (!fs.existsSync(this.persistencePath)) return false;
192
+ const data = JSON.parse(fs.readFileSync(this.persistencePath, 'utf8'));
193
+ if (data.entries && Array.isArray(data.entries)) {
194
+ this.entries = data.entries;
195
+ this.maxEntries = data.maxEntries || this.maxEntries;
196
+ this.rebuildIndex();
197
+ return true;
198
+ }
199
+ return false;
200
+ } catch (e) {
201
+ console.error('❌ Memory load failed:', (e as Error).message);
202
+ return false;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Rebuild keyword index from loaded entries
208
+ */
209
+ private rebuildIndex(): void {
210
+ this.keywordIndex.clear();
211
+ for (const entry of this.entries) {
212
+ const words = (entry.task.description + ' ' + (entry.result.output || '')).toLowerCase().split(/\s+/);
213
+ for (const word of new Set(words)) {
214
+ if (word.length < 3) continue;
215
+ if (!this.keywordIndex.has(word)) this.keywordIndex.set(word, []);
216
+ this.keywordIndex.get(word)!.push(entry.id);
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Auto-persist after adding new entries (if path configured)
223
+ */
224
+ private autoPersist(): void {
225
+ if (this.persistencePath && this.entries.length % 3 === 0) {
226
+ this.saveToDisk();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Clear all memories (also removes persisted file)
166
232
  */
167
233
  clear() {
168
234
  this.entries = [];
169
235
  this.keywordIndex.clear();
236
+ if (this.persistencePath) {
237
+ try {
238
+ const fs = require('fs');
239
+ if (fs.existsSync(this.persistencePath)) fs.unlinkSync(this.persistencePath);
240
+ } catch {}
241
+ }
170
242
  }
171
243
  }
172
244
 
@@ -0,0 +1,159 @@
1
+ /**
2
+ * TMLPD - Ensemble Voting (P0)
3
+ *
4
+ * Parallel multi-LLM execution with confidence-weighted result merging.
5
+ * This is TMLPD's core differentiator: nobody else does parallel ensemble.
6
+ *
7
+ * Runs N providers simultaneously on the same query, scores each result,
8
+ * and returns the best one with explanation of why it was chosen.
9
+ */
10
+
11
+ export interface EnsembleResult {
12
+ best: string;
13
+ winner: string;
14
+ runnerUp: string | null;
15
+ scores: Record<string, number>;
16
+ allResults: Record<string, string | null>;
17
+ reasoning: string;
18
+ timing: { totalMs: number; perProvider: Record<string, number> };
19
+ }
20
+
21
+ export interface EnsembleConfig {
22
+ providers: string[];
23
+ timeoutMs: number;
24
+ minProviders: number; // minimum to proceed (default 2)
25
+ scoringWeights: {
26
+ lengthPenalty: number; // penalize extremely short/long (0-1)
27
+ recencyBoost: number; // prefer newer provider patterns
28
+ historicalAccuracy: number; // weight from past performance
29
+ };
30
+ }
31
+
32
+ const DEFAULT_CONFIG: EnsembleConfig = {
33
+ providers: ['nvidia', 'groq'],
34
+ timeoutMs: 30000,
35
+ minProviders: 1,
36
+ scoringWeights: { lengthPenalty: 0.3, recencyBoost: 0.2, historicalAccuracy: 0.5 }
37
+ };
38
+
39
+ /**
40
+ * Execute a query across multiple providers IN PARALLEL and score results.
41
+ * Returns the best answer with full provenance.
42
+ */
43
+ export async function executeEnsemble(
44
+ query: string,
45
+ systemPrompt: string,
46
+ context: string,
47
+ providerExecutors: Record<string, (q: string, sys: string, ctx: string) => Promise<string | null>>,
48
+ config: Partial<EnsembleConfig> = {}
49
+ ): Promise<EnsembleResult> {
50
+ const cfg = { ...DEFAULT_CONFIG, ...config };
51
+ const start = Date.now();
52
+ const perProvider: Record<string, number> = {};
53
+
54
+ // Step 1: Fire ALL providers in parallel
55
+ const results = await Promise.allSettled(
56
+ cfg.providers.map(async (name) => {
57
+ const pStart = Date.now();
58
+ try {
59
+ const executor = providerExecutors[name];
60
+ if (!executor) throw new Error(`No executor for ${name}`);
61
+ const result = await executor(query, systemPrompt, context);
62
+ perProvider[name] = Date.now() - pStart;
63
+ return { provider: name, result };
64
+ } catch (e) {
65
+ perProvider[name] = Date.now() - pStart;
66
+ return { provider: name, result: null };
67
+ }
68
+ })
69
+ );
70
+
71
+ const allResults: Record<string, string | null> = {};
72
+ for (const r of results) {
73
+ if (r.status === 'fulfilled') {
74
+ allResults[r.value.provider] = r.value.result;
75
+ }
76
+ }
77
+
78
+ // Step 2: Score each result
79
+ const scores: Record<string, number> = {};
80
+ for (const name of cfg.providers) {
81
+ const text = allResults[name];
82
+ if (!text) { scores[name] = 0; continue; }
83
+
84
+ let score = 50; // baseline
85
+
86
+ // Length score: penalize extremely short (<50 chars) or very long (>5000)
87
+ if (cfg.scoringWeights.lengthPenalty > 0) {
88
+ const len = text.length;
89
+ if (len < 50) score -= 20;
90
+ else if (len < 100) score -= 5;
91
+ else if (len > 5000) score -= 10;
92
+ else if (len > 200) score += cfg.scoringWeights.lengthPenalty * 10;
93
+ }
94
+
95
+ // Specificity score: presence of concrete details
96
+ const hasNumbers = /\d+/.test(text);
97
+ const hasTechNames = /[A-Z][a-z]+\.(js|ts|py|json)/.test(text) || /\b(API|SDK|CLI|npm|docker|redis|gcs|faiss)\b/i.test(text);
98
+ const hasBullets = text.includes('*') || text.includes('-') || text.includes('1.');
99
+ if (hasNumbers) score += 10;
100
+ if (hasTechNames) score += 15;
101
+ if (hasBullets) score += 5;
102
+
103
+ // Structure score: well-formatted responses
104
+ if (text.split('\n').length >= 5) score += 5;
105
+
106
+ scores[name] = score;
107
+ }
108
+
109
+ // Step 3: Determine winner and runner-up
110
+ const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
111
+ const winner = sorted[0]?.[0] || cfg.providers[0];
112
+ const runnerUp = sorted[1]?.[0] || null;
113
+ const best = allResults[winner] || allResults[runnerUp || ''] || '';
114
+
115
+ const reasoning = winner === runnerUp
116
+ ? `Single provider returned results: ${winner}`
117
+ : `Ensemble merged ${cfg.providers.filter(p => !!allResults[p]).length} providers. ` +
118
+ `${winner} scored ${scores[winner].toFixed(0)} (${describeScore(scores[winner])}) vs ` +
119
+ `${runnerUp} at ${runnerUp ? scores[runnerUp]?.toFixed(0) : 'N/A'}.`;
120
+
121
+ return {
122
+ best,
123
+ winner,
124
+ runnerUp,
125
+ scores,
126
+ allResults,
127
+ reasoning,
128
+ timing: { totalMs: Date.now() - start, perProvider }
129
+ };
130
+ }
131
+
132
+ function describeScore(score: number): string {
133
+ if (score >= 80) return 'high confidence';
134
+ if (score >= 60) return 'moderate confidence';
135
+ return 'low confidence';
136
+ }
137
+
138
+ /**
139
+ * Merge multiple text results into a combined response.
140
+ * Used when providers give complementary answers.
141
+ */
142
+ export function mergeComplementary(results: string[], maxLength: number = 4000): string {
143
+ const sections = results.filter(r => !!r).map((r, i) => `### Provider ${i + 1}\n${r.trim()}`);
144
+ return sections.join('\n\n---\n\n').slice(0, maxLength);
145
+ }
146
+
147
+ /**
148
+ * Update historical accuracy for a provider based on user feedback.
149
+ */
150
+ export function recordFeedback(
151
+ winner: string,
152
+ wasHelpful: boolean,
153
+ history: Record<string, { good: number; bad: number }>
154
+ ): Record<string, { good: number; bad: number }> {
155
+ if (!history[winner]) history[winner] = { good: 0, bad: 0 };
156
+ if (wasHelpful) history[winner].good++;
157
+ else history[winner].bad++;
158
+ return history;
159
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * TMLPD - Query-Type Presets (P1)
3
+ *
4
+ * Configurable provider + temperature profiles per query type.
5
+ * Replaces flat regex patterns with named, adjustable presets.
6
+ * This productizes what Reddit users do manually: route different
7
+ * task types to different providers with different settings.
8
+ */
9
+
10
+ export interface QueryPreset {
11
+ name: string;
12
+ description: string;
13
+ provider: string; // primary provider
14
+ fallbackProvider?: string;
15
+ temperature: number;
16
+ maxTokens: number;
17
+ ensemble: boolean; // use ensemble voting?
18
+ ensembleProviders?: string[];
19
+ timeoutMs: number;
20
+ systemPrompt?: string;
21
+ }
22
+
23
+ export interface PresetRouter {
24
+ presets: Record<string, QueryPreset>;
25
+ defaultPreset: string;
26
+ classify: (query: string) => string;
27
+ }
28
+
29
+ // ============================================================
30
+ // DEFAULT PRESETS
31
+ // ============================================================
32
+
33
+ export const DEFAULT_PRESETS: Record<string, QueryPreset> = {
34
+ fast: {
35
+ name: 'Fast Query',
36
+ description: 'Quick lookups, simple questions, status checks',
37
+ provider: 'groq',
38
+ temperature: 0.3,
39
+ maxTokens: 500,
40
+ ensemble: false,
41
+ timeoutMs: 15000,
42
+ systemPrompt: 'Answer concisely in 1-2 sentences.',
43
+ },
44
+ research: {
45
+ name: 'Research / Deep Analysis',
46
+ description: 'Complex multi-step reasoning, comparisons, deep dives',
47
+ provider: 'nvidia',
48
+ fallbackProvider: 'groq',
49
+ temperature: 0.3,
50
+ maxTokens: 3000,
51
+ ensemble: true,
52
+ ensembleProviders: ['nvidia', 'groq'],
53
+ timeoutMs: 60000,
54
+ systemPrompt: 'Provide thorough analysis with specific technical details and examples.',
55
+ },
56
+ creative: {
57
+ name: 'Creative / Writing',
58
+ description: 'Content generation, storytelling, brainstorming',
59
+ provider: 'nvidia',
60
+ temperature: 0.7,
61
+ maxTokens: 2500,
62
+ ensemble: false,
63
+ timeoutMs: 45000,
64
+ systemPrompt: 'Be creative and engaging. Use vivid language.',
65
+ },
66
+ code: {
67
+ name: 'Code / Technical',
68
+ description: 'Code generation, debugging, architecture, implementation',
69
+ provider: 'nvidia',
70
+ fallbackProvider: 'groq',
71
+ temperature: 0.2,
72
+ maxTokens: 3000,
73
+ ensemble: true,
74
+ ensembleProviders: ['nvidia', 'groq'],
75
+ timeoutMs: 45000,
76
+ systemPrompt: 'Be precise. Show code with clear explanations. Prefer working solutions over theoretical ones.',
77
+ },
78
+ factual: {
79
+ name: 'Factual / Q&A',
80
+ description: 'Direct answers with citations, definitions, explanations',
81
+ provider: 'groq',
82
+ temperature: 0.2,
83
+ maxTokens: 1000,
84
+ ensemble: false,
85
+ timeoutMs: 20000,
86
+ systemPrompt: 'Answer factually. If unsure, say so. Be concise.',
87
+ },
88
+ };
89
+
90
+ // ============================================================
91
+ // CLASSIFICATION PATTERNS
92
+ // ============================================================
93
+
94
+ const PATTERNS: Array<{ rx: RegExp; preset: string }> = [
95
+ { rx: /\b(debug|error|bug|fix|crash|exception|fail|broken|compile)\b/i, preset: 'code' },
96
+ { rx: /\b(code|function|api|endpoint|syntax|npm|import|implement|class|algorithm)\b/i, preset: 'code' },
97
+ { rx: /\b(story|poem|write|create|generate|tweet|post|article|content|draft|compose|creative)\b/i, preset: 'creative' },
98
+ { rx: /\b(architecture|design|pattern|database|cache|queue|latency|throughput|scal|deploy|optimize|refactor)\b/i, preset: 'research' },
99
+ { rx: /\b(compare|analyze|evaluate|difference|pros|cons|tradeoff|vs\b|research|deep|comprehensive)\b/i, preset: 'research' },
100
+ { rx: /\b(what is|define|explain|meaning|definition|describe|how does)\b/i, preset: 'factual' },
101
+ ];
102
+
103
+ // ============================================================
104
+ // PRESET ROUTER
105
+ // ============================================================
106
+
107
+ export function createPresetRouter(customPresets?: Record<string, QueryPreset>): PresetRouter {
108
+ const presets = { ...DEFAULT_PRESETS, ...customPresets };
109
+
110
+ return {
111
+ presets,
112
+ defaultPreset: 'fast',
113
+
114
+ classify(query: string): string {
115
+ for (const p of PATTERNS) {
116
+ if (p.rx.test(query)) return p.preset;
117
+ }
118
+ // Length-based classification
119
+ const words = query.split(/\s+/).length;
120
+ if (words > 30) return 'research';
121
+ if (words > 10) return 'factual';
122
+ return 'fast';
123
+ }
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Get preset config for a query, with fallback to default.
129
+ */
130
+ export function getPresetForQuery(
131
+ query: string,
132
+ router: PresetRouter
133
+ ): QueryPreset {
134
+ const presetName = router.classify(query);
135
+ return router.presets[presetName] || router.presets[router.defaultPreset];
136
+ }