adaptive-memory-multi-model-router 2.12.7 → 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.
- package/README.md +256 -753
- package/package.json +2 -2
- package/tmlpd-pi-extension/README.md +52 -22
- package/tmlpd-pi-extension/package-lock.json +5 -1
- package/tmlpd-pi-extension/package.json +2 -2
- package/tmlpd-pi-extension/src/index.ts +8 -0
- package/tmlpd-pi-extension/src/memory/episodicMemory.ts +74 -2
- package/tmlpd-pi-extension/src/routing/ensembleVoting.ts +159 -0
- package/tmlpd-pi-extension/src/routing/queryTypePresets.ts +136 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Parallel Multi-LLM Execution with Intelligent Merge. The central routing brain for PI CLI, WhatsApp Bot & Telegram Bot. 47+ providers, parallel ensemble (nobody else does this), confidence scoring, query-type presets, persistent memory. RouteLLM-style routing with 99.5% accuracy. 62% cost savings. Zero ML, 19.5KB. MIT.",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
9
|
"a3m-router": "dist/cli.js",
|
|
@@ -1,36 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# TMLPD PI Extension — Parallel Multi-LLM for PI Agent
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
> npm: https://npmjs.com/package/adaptive-memory-multi-model-router
|
|
3
|
+
> **Part of the [A3M Router](https://github.com/Das-rebel/adaptive-memory-multi-model-router) ecosystem.**
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
PI agent tools for parallel multi-LLM execution with confidence-weighted ensemble merging. Powers `/tmlpd-parallel`, `/tmlpd-route`, `/tmlpd-compare`, and `/tmlpd-cost` commands in the PI CLI.
|
|
6
|
+
|
|
7
|
+
## What This Is
|
|
8
|
+
|
|
9
|
+
The PI agent integration for A3M Router. These tools let your PI agent:
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
- **Execute prompts across multiple LLMs in parallel** and pick the best result
|
|
12
|
+
- **Smart-route** single queries to the optimal provider based on task type
|
|
13
|
+
- **Track costs** across all providers and sessions
|
|
14
|
+
- **Persist agent memory** across CLI sessions
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
## Core Features
|
|
17
|
+
|
|
18
|
+
| Tool | Description |
|
|
19
|
+
|:-----|:------------|
|
|
20
|
+
| `tmlpd_execute` | Run prompt across multiple providers in parallel, merge results |
|
|
21
|
+
| `tmlpd_execute_single` | Smart-route to optimal single provider |
|
|
22
|
+
| Parallel ensemble | NVIDIA + Groq simultaneously, scored and merged |
|
|
23
|
+
| Cost tracking | Per-query cost display, provider-level breakdown |
|
|
24
|
+
| Persistent memory | Cross-session `.memory.json` with keyword indexing |
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
11
27
|
|
|
12
|
-
### Quick Install
|
|
13
28
|
```bash
|
|
14
|
-
npm install
|
|
15
|
-
npx a3m-router --help
|
|
29
|
+
npm install tmlpd-pi
|
|
16
30
|
```
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **Prefix Caching** (RadixAttention) - 5-10x speedup
|
|
21
|
-
- **Speculative Decoding** (Medusa) - 2-3x faster
|
|
22
|
-
- **Python Bindings** - LangChain, LlamaIndex, AutoGen, CrewAI
|
|
32
|
+
```typescript
|
|
33
|
+
import { createTMLPD } from "tmlpd-pi";
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
const tmlpd = createTMLPD({ cache: { ttl_seconds: 3600 } });
|
|
36
|
+
|
|
37
|
+
// Parallel execution across providers
|
|
38
|
+
const result = await tmlpd.executeParallel(prompt, ["nvidia", "groq"]);
|
|
39
|
+
|
|
40
|
+
// With ensemble scoring
|
|
41
|
+
const { best, winner, scores } = await executeEnsemble(
|
|
42
|
+
prompt, systemPrompt, context,
|
|
43
|
+
{ nvidia: callNvidia, groq: callGroq }
|
|
44
|
+
);
|
|
29
45
|
```
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
## Exports
|
|
48
|
+
|
|
49
|
+
- `createTMLPD`, `TMLPDTools` — Core parallel execution
|
|
50
|
+
- `executeEnsemble`, `mergeComplementary`, `recordFeedback` — P0 Ensemble voting
|
|
51
|
+
- `createPresetRouter`, `getPresetForQuery`, `DEFAULT_PRESETS` — P1 Query presets
|
|
52
|
+
- `EpisodicMemoryStore` — P3 Persistent memory with auto-save
|
|
53
|
+
- `CostTracker`, `BudgetEnforcer` — P2 Cost tracking
|
|
54
|
+
- `ResponseCache`, `PrefixCache` — Caching layers
|
|
55
|
+
- `HALOOrchestrator`, `MCTSWorkflowOptimizer` — Advanced orchestration
|
|
56
|
+
|
|
57
|
+
## Research Backing
|
|
58
|
+
|
|
59
|
+
- **RouteLLM** (arXiv:2404.06035) — Learned cost-quality routing
|
|
60
|
+
- **RadixAttention** (arXiv:2312.07104) — 5-10x speedup via prefix caching
|
|
61
|
+
- **Medusa** (arXiv:2401.10774) — 2-3x faster generation
|
|
62
|
+
- **A-Mem** (arXiv:2502.12110) — Episodic memory patterns
|
|
33
63
|
|
|
34
64
|
---
|
|
35
65
|
|
|
36
|
-
|
|
66
|
+
*Part of the A3M Router ecosystem. "Nobody does parallel multi-LLM execution with result merging. Everyone does sequential fallback."*
|
|
@@ -15,11 +15,15 @@
|
|
|
15
15
|
"tmlpd-pi": "dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@types/node": "^20.
|
|
18
|
+
"@types/node": "^20.19.41",
|
|
19
19
|
"typescript": "^5.0.0"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"funding": {
|
|
25
|
+
"type": "individual",
|
|
26
|
+
"url": "https://github.com/sponsors/Das-rebel"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"node_modules/@types/node": {
|
|
@@ -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
|
-
*
|
|
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
|
+
}
|