adaptive-memory-multi-model-router 1.3.1 → 1.4.1
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 +33 -87
- package/dist/integrations/oauth.js +26 -0
- package/dist/memory/autoFetch.js +59 -0
- package/dist/memory/autoFetch.ts +109 -0
- package/dist/memory/memoryTree.js +81 -0
- package/dist/memory/obsidianVault.js +26 -0
- package/dist/utils/enhancedCompression.js +180 -0
- package/package.json +81 -147
- package/src/integrations/oauth.ts +280 -0
- package/src/memory/autoFetch.ts +109 -0
- package/src/memory/memoryTree.ts +242 -0
- package/src/memory/obsidianVault.ts +224 -0
- package/package.json.tmp +0 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ You're paying **too much** for LLM inference. Running GPT-4 on simple queries. U
|
|
|
21
21
|
|
|
22
22
|
## The Solution
|
|
23
23
|
|
|
24
|
-
**A3M Router** learns your usage patterns and routes each request to the optimal model—automatically. Save 40% on costs. Get 5-10x speedups.
|
|
24
|
+
**A3M Router** learns your usage patterns and routes each request to the optimal model—automatically. Save 40% on costs. Get 5-10x speedups. Built on research from RouteLLM, RadixAttention, and Medusa.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
npm install adaptive-memory-multi-model-router
|
|
@@ -29,16 +29,18 @@ npm install adaptive-memory-multi-model-router
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
## Features
|
|
32
|
+
## Features (v1.4.0)
|
|
33
33
|
|
|
34
34
|
| Capability | How It Works | Result |
|
|
35
35
|
|------------|-------------|--------|
|
|
36
36
|
| **Learned Routing** | RouteLLM cost-quality tradeoff | 40% cost reduction |
|
|
37
|
-
| **Adaptive Memory** |
|
|
37
|
+
| **Adaptive Memory** | Memory Tree + Episodic | 20x more accurate routing |
|
|
38
|
+
| **Auto-Fetch** | 20-min sync loop | Context-aware decisions |
|
|
38
39
|
| **Prefix Caching** | RadixAttention shared prompts | 5-10x speedup |
|
|
39
40
|
| **Speculative Decoding** | Medusa tree verification | 2-3x faster generation |
|
|
40
|
-
| **Token Compression** |
|
|
41
|
+
| **Token Compression** | TokenJuice-style (80% reduction) | 20-80% fewer tokens |
|
|
41
42
|
| **Circuit Breaker** | Exponential backoff | 99.9% uptime |
|
|
43
|
+
| **Obsidian Vault** | Markdown export | Human-readable logs |
|
|
42
44
|
|
|
43
45
|
---
|
|
44
46
|
|
|
@@ -50,8 +52,8 @@ npm install adaptive-memory-multi-model-router
|
|
|
50
52
|
import { createA3MRouter } from 'adaptive-memory-multi-model-router';
|
|
51
53
|
|
|
52
54
|
const router = createA3MRouter({
|
|
53
|
-
memory: true,
|
|
54
|
-
costBudget: 0.05
|
|
55
|
+
memory: true,
|
|
56
|
+
costBudget: 0.05
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
const result = await router.route({
|
|
@@ -67,10 +69,7 @@ console.log(result.output);
|
|
|
67
69
|
from adaptive_memory_multi_model_router import A3MRouter
|
|
68
70
|
|
|
69
71
|
router = A3MRouter()
|
|
70
|
-
result = router.route(
|
|
71
|
-
prompt="Analyze this dataset",
|
|
72
|
-
budget=0.02
|
|
73
|
-
)
|
|
72
|
+
result = router.route(prompt="Analyze this dataset", budget=0.02)
|
|
74
73
|
print(result.output)
|
|
75
74
|
```
|
|
76
75
|
|
|
@@ -79,105 +78,59 @@ print(result.output)
|
|
|
79
78
|
```bash
|
|
80
79
|
npx a3m-router route "Explain quantum computing"
|
|
81
80
|
npx a3m-router parallel "task1" "task2" "task3"
|
|
82
|
-
npx a3m-router cost
|
|
83
81
|
```
|
|
84
82
|
|
|
85
83
|
---
|
|
86
84
|
|
|
87
|
-
##
|
|
85
|
+
## What's New in v1.4.0
|
|
88
86
|
|
|
89
|
-
**
|
|
87
|
+
- **Enhanced Compression** - TokenJuice-style, up to 80% reduction
|
|
88
|
+
- **Auto-Fetch Sync** - 20-minute interval context sync
|
|
89
|
+
- **Memory Tree** - Hierarchical scoring and chunking
|
|
90
|
+
- **Obsidian Vault** - Markdown export for human review
|
|
91
|
+
- **OAuth Manager** - One-click GitHub, Slack, Gmail, Notion
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
from langchain import LLMChain
|
|
93
|
-
from adaptive_memory_multi_model_router import A3MRouter
|
|
93
|
+
---
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
result = chain.run("your query")
|
|
99
|
-
```
|
|
95
|
+
## LLM Providers (14)
|
|
96
|
+
|
|
97
|
+
OpenAI, OpenRouter, Groq, Cerebras, Anthropic, Google, DeepSeek, Fireworks, Perplexity, Cohere, Mistral, AWS Bedrock, xAI, Ollama
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
---
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| Anthropic | claude-3.5, claude-3 | ✅ Production ready |
|
|
107
|
-
| Ollama | llama3, mistral | ✅ Local, zero cost |
|
|
108
|
-
| vLLM | Any HuggingFace | ✅ Self-hosted |
|
|
109
|
-
| LM Studio | Any GGUF | ✅ Local privacy |
|
|
101
|
+
## Agent & Tool Integrations (10)
|
|
102
|
+
|
|
103
|
+
GitHub, Slack, Telegram, Notion, Linear, Jira, Gmail, Discord, Airtable, Google Calendar
|
|
110
104
|
|
|
111
105
|
---
|
|
112
106
|
|
|
113
107
|
## Research-Backed
|
|
114
108
|
|
|
115
|
-
A3M Router implements techniques from peer-reviewed research—not experiments:
|
|
116
|
-
|
|
117
109
|
| Paper | Technique | Impact |
|
|
118
110
|
|-------|-----------|--------|
|
|
119
|
-
| [RouteLLM](https://arxiv.org/abs/2404.06035) | Learned
|
|
111
|
+
| [RouteLLM](https://arxiv.org/abs/2404.06035) | Learned routing | 40% cost reduction |
|
|
120
112
|
| [RadixAttention](https://arxiv.org/abs/2312.07104) | Prefix caching | 5-10x speedup |
|
|
121
113
|
| [Medusa](https://arxiv.org/abs/2401.10774) | Speculative decoding | 2-3x faster |
|
|
122
|
-
| [LLMLingua](https://arxiv.org/abs/2403.12968) | Token compression | 20-
|
|
114
|
+
| [LLMLingua](https://arxiv.org/abs/2403.12968) | Token compression | 20-80% fewer tokens |
|
|
123
115
|
|
|
124
116
|
---
|
|
125
117
|
|
|
126
118
|
## CLI Reference
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
| `a3m-router compress "text"` | ISON token compression |
|
|
136
|
-
| `a3m-router local "prompt"` | Local Ollama execution |
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
## Architecture
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
144
|
-
│ Your Request │
|
|
145
|
-
│ "Analyze this code" │
|
|
146
|
-
└─────────────────────────┬───────────────────────────────────┘
|
|
147
|
-
│
|
|
148
|
-
▼
|
|
149
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
150
|
-
│ A3M Router │
|
|
151
|
-
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
|
|
152
|
-
│ │ Task │ │ Memory │ │ RouteLLM │ │
|
|
153
|
-
│ │ Classifier │→│ Store │→│ Cost-Quality │ │
|
|
154
|
-
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
|
|
155
|
-
│ │ │
|
|
156
|
-
│ ▼ │
|
|
157
|
-
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
|
|
158
|
-
│ │ Circuit │ │ Prefix │ │ Speculative │ │
|
|
159
|
-
│ │ Breaker │→│ Cache │→│ Decoder │ │
|
|
160
|
-
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
|
|
161
|
-
└─────────────────────────┬───────────────────────────────────┘
|
|
162
|
-
│
|
|
163
|
-
▼
|
|
164
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
165
|
-
│ Optimal Model Response │
|
|
166
|
-
│ (cheapest + fastest + highest quality) │
|
|
167
|
-
└─────────────────────────────────────────────────────────────┘
|
|
120
|
+
```bash
|
|
121
|
+
a3m-router route "prompt" # Smart routing
|
|
122
|
+
a3m-router parallel "t1" "t2" # Parallel execution
|
|
123
|
+
a3m-router compare "prompt" # Compare models
|
|
124
|
+
a3m-router cost # Show costs
|
|
125
|
+
a3m-router compress "text" # Token compression
|
|
126
|
+
a3m-router local "prompt" # Local Ollama
|
|
168
127
|
```
|
|
169
128
|
|
|
170
129
|
---
|
|
171
130
|
|
|
172
131
|
## Contributing
|
|
173
132
|
|
|
174
|
-
Issues and PRs welcome!
|
|
175
|
-
|
|
176
|
-
1. Fork the repo
|
|
177
|
-
2. Create your branch (`git checkout -b feature/amazing`)
|
|
178
|
-
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
179
|
-
4. Push to the branch (`git push origin feature/amazing`)
|
|
180
|
-
5. Open a Pull Request
|
|
133
|
+
Issues and PRs welcome!
|
|
181
134
|
|
|
182
135
|
---
|
|
183
136
|
|
|
@@ -185,10 +138,3 @@ Issues and PRs welcome!
|
|
|
185
138
|
|
|
186
139
|
MIT © Das-rebel
|
|
187
140
|
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
<div align="center">
|
|
191
|
-
|
|
192
|
-
**A3M Router** — Built for developers who care about cost, speed, and quality.
|
|
193
|
-
|
|
194
|
-
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Integration Manager (Compiled)
|
|
3
|
+
*/
|
|
4
|
+
const OAUTH_PROVIDERS = {
|
|
5
|
+
github: { name: 'GitHub', authUrl: 'https://github.com/login/oauth/authorize', tokenUrl: 'https://github.com/login/oauth/access_token', scopes: ['repo'], baseUrl: 'https://api.github.com' },
|
|
6
|
+
slack: { name: 'Slack', authUrl: 'https://slack.com/oauth/v2/authorize', tokenUrl: 'https://slack.com/api/oauth.v2.access', scopes: ['chat:write'], baseUrl: 'https://slack.com/api' },
|
|
7
|
+
gmail: { name: 'Gmail', authUrl: 'https://accounts.google.com/o/oauth2/v2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', scopes: ['https://www.googleapis.com/auth/gmail.send'], baseUrl: 'https://gmail.googleapis.com/gmail/v1' },
|
|
8
|
+
notion: { name: 'Notion', authUrl: 'https://api.notion.com/v1/oauth/authorize', tokenUrl: 'https://api.notion.com/v1/oauth/token', scopes: ['read_content'], baseUrl: 'https://api.notion.com/v1' }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class OAuthManager {
|
|
12
|
+
constructor() { this.configs = new Map(); this.tokens = new Map(); this.state = new Map(); }
|
|
13
|
+
configure(provider, config) { this.configs.set(provider, config); }
|
|
14
|
+
getAuthUrl(provider) {
|
|
15
|
+
const config = this.configs.get(provider), info = OAUTH_PROVIDERS[provider];
|
|
16
|
+
if (!config || !info) throw new Error(`Unknown provider: ${provider}`);
|
|
17
|
+
const state = `${provider}_${Date.now()}`;
|
|
18
|
+
this.state.set(provider, state);
|
|
19
|
+
return `${info.authUrl}?client_id=${config.clientId}&redirect_uri=${config.redirectUri}&scope=${info.scopes.join(' ')}&state=${state}`;
|
|
20
|
+
}
|
|
21
|
+
isConnected(provider) { const t = this.tokens.get(provider); return !(!t || (t.expiresAt && Date.now() >= t.expiresAt)); }
|
|
22
|
+
getConnectedProviders() { return Array.from(this.tokens.keys()).filter(p => this.isConnected(p)); }
|
|
23
|
+
disconnect(provider) { this.tokens.delete(provider); this.state.delete(provider); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { OAuthManager, OAUTH_PROVIDERS };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Fetch Sync Loop (Compiled)
|
|
3
|
+
*/
|
|
4
|
+
class AutoFetch {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.intervalMs = config.intervalMs || 20 * 60 * 1000;
|
|
7
|
+
this.enabled = config.enabled !== false;
|
|
8
|
+
this.targets = new Set(config.targets || ['github', 'notion', 'slack']);
|
|
9
|
+
this.lastSync = new Map();
|
|
10
|
+
this.syncHandlers = new Map();
|
|
11
|
+
this.timer = null;
|
|
12
|
+
this.setupDefaultHandlers();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setupDefaultHandlers() {
|
|
16
|
+
this.syncHandlers.set('github', async () => ({ target: 'github', success: true, items: 0, timestamp: Date.now() }));
|
|
17
|
+
this.syncHandlers.set('notion', async () => ({ target: 'notion', success: true, items: 0, timestamp: Date.now() }));
|
|
18
|
+
this.syncHandlers.set('slack', async () => ({ target: 'slack', success: true, items: 0, timestamp: Date.now() }));
|
|
19
|
+
this.syncHandlers.set('gmail', async () => ({ target: 'gmail', success: true, items: 0, timestamp: Date.now() }));
|
|
20
|
+
this.syncHandlers.set('calendar', async () => ({ target: 'calendar', success: true, items: 0, timestamp: Date.now() }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
start() {
|
|
24
|
+
if (!this.enabled) return;
|
|
25
|
+
this.syncAll();
|
|
26
|
+
this.timer = setInterval(() => this.syncAll(), this.intervalMs);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
stop() {
|
|
30
|
+
if (this.timer) {
|
|
31
|
+
clearInterval(this.timer);
|
|
32
|
+
this.timer = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async syncAll() {
|
|
37
|
+
const results = new Map();
|
|
38
|
+
for (const target of this.targets) {
|
|
39
|
+
const handler = this.syncHandlers.get(target);
|
|
40
|
+
if (handler) {
|
|
41
|
+
try {
|
|
42
|
+
const result = await handler();
|
|
43
|
+
this.lastSync.set(target, result);
|
|
44
|
+
results.set(target, result);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const result = { target, success: false, items: 0, timestamp: Date.now(), error: error.message };
|
|
47
|
+
this.lastSync.set(target, result);
|
|
48
|
+
results.set(target, result);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getLastSync(target) { return this.lastSync.get(target); }
|
|
56
|
+
addHandler(target, handler) { this.syncHandlers.set(target, handler); this.targets.add(target); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { AutoFetch };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Fetch Sync Loop
|
|
3
|
+
*
|
|
4
|
+
* Periodically syncs data from connected tools to provide
|
|
5
|
+
* context-aware routing decisions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SyncConfig {
|
|
9
|
+
intervalMs: number;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
targets: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SyncResult {
|
|
15
|
+
target: string;
|
|
16
|
+
success: boolean;
|
|
17
|
+
items: number;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class AutoFetch {
|
|
23
|
+
private intervalMs: number;
|
|
24
|
+
private enabled: boolean;
|
|
25
|
+
private targets: Set<string>;
|
|
26
|
+
private lastSync: Map<string, SyncResult>;
|
|
27
|
+
private timer: NodeJS.Timeout | null = null;
|
|
28
|
+
private syncHandlers: Map<string, () => Promise<SyncResult>>;
|
|
29
|
+
|
|
30
|
+
constructor(config: Partial<SyncConfig> = {}) {
|
|
31
|
+
this.intervalMs = config.intervalMs || 20 * 60 * 1000;
|
|
32
|
+
this.enabled = config.enabled !== false;
|
|
33
|
+
this.targets = new Set(config.targets || ['github', 'notion', 'slack']);
|
|
34
|
+
this.lastSync = new Map();
|
|
35
|
+
this.syncHandlers = new Map();
|
|
36
|
+
this.setupDefaultHandlers();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private setupDefaultHandlers() {
|
|
40
|
+
this.syncHandlers.set('github', async () => this.syncGitHub());
|
|
41
|
+
this.syncHandlers.set('notion', async () => this.syncNotion());
|
|
42
|
+
this.syncHandlers.set('slack', async () => this.syncSlack());
|
|
43
|
+
this.syncHandlers.set('gmail', async () => this.syncGmail());
|
|
44
|
+
this.syncHandlers.set('calendar', async () => this.syncCalendar());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
start() {
|
|
48
|
+
if (!this.enabled) return;
|
|
49
|
+
this.syncAll();
|
|
50
|
+
this.timer = setInterval(() => this.syncAll(), this.intervalMs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
stop() {
|
|
54
|
+
if (this.timer) {
|
|
55
|
+
clearInterval(this.timer);
|
|
56
|
+
this.timer = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async syncAll(): Promise<Map<string, SyncResult>> {
|
|
61
|
+
const results = new Map<string, SyncResult>();
|
|
62
|
+
for (const target of this.targets) {
|
|
63
|
+
const handler = this.syncHandlers.get(target);
|
|
64
|
+
if (handler) {
|
|
65
|
+
try {
|
|
66
|
+
const result = await handler();
|
|
67
|
+
this.lastSync.set(target, result);
|
|
68
|
+
results.set(target, result);
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
const result: SyncResult = { target, success: false, items: 0, timestamp: Date.now(), error: error.message };
|
|
71
|
+
this.lastSync.set(target, result);
|
|
72
|
+
results.set(target, result);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getLastSync(target: string): SyncResult | undefined {
|
|
80
|
+
return this.lastSync.get(target);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addHandler(target: string, handler: () => Promise<SyncResult>) {
|
|
84
|
+
this.syncHandlers.set(target, handler);
|
|
85
|
+
this.targets.add(target);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async syncGitHub(): Promise<SyncResult> {
|
|
89
|
+
return { target: 'github', success: true, items: 0, timestamp: Date.now() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async syncNotion(): Promise<SyncResult> {
|
|
93
|
+
return { target: 'notion', success: true, items: 0, timestamp: Date.now() };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async syncSlack(): Promise<SyncResult> {
|
|
97
|
+
return { target: 'slack', success: true, items: 0, timestamp: Date.now() };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async syncGmail(): Promise<SyncResult> {
|
|
101
|
+
return { target: 'gmail', success: true, items: 0, timestamp: Date.now() };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async syncCalendar(): Promise<SyncResult> {
|
|
105
|
+
return { target: 'calendar', success: true, items: 0, timestamp: Date.now() };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default AutoFetch;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Tree Hierarchy (Compiled)
|
|
3
|
+
*/
|
|
4
|
+
class MemoryTree {
|
|
5
|
+
constructor(maxChunkSize = 3000) {
|
|
6
|
+
this.maxChunkSize = maxChunkSize;
|
|
7
|
+
this.root = { id: 'root', chunks: [], summary: '', children: [], depth: 0 };
|
|
8
|
+
this.chunks = new Map();
|
|
9
|
+
this.idCounter = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
generateId() { return `chunk_${Date.now()}_${this.idCounter++}`; }
|
|
13
|
+
|
|
14
|
+
async add(data) {
|
|
15
|
+
const texts = this.chunk(data);
|
|
16
|
+
const added = [];
|
|
17
|
+
for (const text of texts) {
|
|
18
|
+
const chunk = {
|
|
19
|
+
id: this.generateId(),
|
|
20
|
+
content: text,
|
|
21
|
+
score: 0.5,
|
|
22
|
+
depth: 0,
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
accessCount: 0
|
|
25
|
+
};
|
|
26
|
+
this.chunks.set(chunk.id, chunk);
|
|
27
|
+
this.root.chunks.push(chunk);
|
|
28
|
+
added.push(chunk);
|
|
29
|
+
}
|
|
30
|
+
return added;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
chunk(text) {
|
|
34
|
+
const chunks = [], words = text.split(/\s+/);
|
|
35
|
+
let current = [], size = 0;
|
|
36
|
+
for (const word of words) {
|
|
37
|
+
size += word.length + 1;
|
|
38
|
+
if (size > this.maxChunkSize) {
|
|
39
|
+
chunks.push(current.join(' '));
|
|
40
|
+
current = [word];
|
|
41
|
+
size = word.length + 1;
|
|
42
|
+
} else {
|
|
43
|
+
current.push(word);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (current.length) chunks.push(current.join(' '));
|
|
47
|
+
return chunks;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
search(query) {
|
|
51
|
+
return Array.from(this.chunks.values()).filter(c => c.content.includes(query));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getContext(maxTokens = 3000) {
|
|
55
|
+
return Array.from(this.chunks.values())
|
|
56
|
+
.map(c => c.content)
|
|
57
|
+
.join('\n\n')
|
|
58
|
+
.slice(0, maxTokens);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
toMarkdown() {
|
|
62
|
+
return '# Memory Tree\n' + Array.from(this.chunks.values())
|
|
63
|
+
.map(c => `## ${c.id}\n${c.content}`)
|
|
64
|
+
.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getStats() {
|
|
68
|
+
return {
|
|
69
|
+
totalChunks: this.chunks.size,
|
|
70
|
+
maxDepth: this.getMaxDepth(this.root),
|
|
71
|
+
rootChunks: this.root.chunks.length
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getMaxDepth(node) {
|
|
76
|
+
if (node.children.length === 0) return node.depth;
|
|
77
|
+
return Math.max(...node.children.map(c => this.getMaxDepth(c)));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { MemoryTree };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Obsidian Vault Integration (Compiled)
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
class ObsidianVault {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.config = { path: config.path || './vault', autoSave: config.autoSave !== false, maxFileAge: 30 };
|
|
10
|
+
this.decisions = [];
|
|
11
|
+
if (!fs.existsSync(this.config.path)) fs.mkdirSync(this.config.path, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async saveDecision(decision) {
|
|
15
|
+
this.decisions.push(decision);
|
|
16
|
+
const filepath = path.join(this.config.path, `routing-decision-${decision.id}.md`);
|
|
17
|
+
const content = `# Routing Decision ${decision.id}\n\nDate: ${new Date(decision.timestamp).toISOString()}\n\nProvider: ${decision.selectedProvider}\nModel: ${decision.selectedModel}\n\nReasoning: ${decision.reasoning}\nCost: $${decision.cost}\n`;
|
|
18
|
+
fs.writeFileSync(filepath, content);
|
|
19
|
+
return filepath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getRecentDecisions(count = 10) { return this.decisions.slice(-count).reverse(); }
|
|
23
|
+
searchDecisions(query) { return this.decisions.filter(d => d.prompt.includes(query)); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { ObsidianVault };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Compression - TokenJuice-style
|
|
3
|
+
*
|
|
4
|
+
* Achieves 80% token reduction through multiple techniques:
|
|
5
|
+
* - HTML to Markdown conversion
|
|
6
|
+
* - URL shortening
|
|
7
|
+
* - Non-ASCII removal
|
|
8
|
+
* - Repeated phrase deduplication
|
|
9
|
+
* - Code block optimization
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
class EnhancedCompression {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.maxUrlLength = 50;
|
|
15
|
+
this.maxChunkSize = 3000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compress text to ~80% original size
|
|
20
|
+
*/
|
|
21
|
+
compress(text) {
|
|
22
|
+
if (!text || text.length === 0) return '';
|
|
23
|
+
|
|
24
|
+
let result = text;
|
|
25
|
+
|
|
26
|
+
// 1. HTML → Markdown
|
|
27
|
+
result = this.htmlToMarkdown(result);
|
|
28
|
+
|
|
29
|
+
// 2. Shorten URLs
|
|
30
|
+
result = this.shortenUrls(result);
|
|
31
|
+
|
|
32
|
+
// 3. Remove non-ASCII
|
|
33
|
+
result = this.removeNonASCII(result);
|
|
34
|
+
|
|
35
|
+
// 4. Deduplicate phrases
|
|
36
|
+
result = this.deduplicatePhrases(result);
|
|
37
|
+
|
|
38
|
+
// 5. Compress whitespace
|
|
39
|
+
result = this.compressWhitespace(result);
|
|
40
|
+
|
|
41
|
+
// 6. Optimize code blocks
|
|
42
|
+
result = this.optimizeCodeBlocks(result);
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* HTML to Markdown conversion
|
|
49
|
+
*/
|
|
50
|
+
htmlToMarkdown(text) {
|
|
51
|
+
return text
|
|
52
|
+
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n')
|
|
53
|
+
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n')
|
|
54
|
+
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n')
|
|
55
|
+
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n')
|
|
56
|
+
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
|
57
|
+
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
|
58
|
+
.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
|
|
59
|
+
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
|
60
|
+
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
|
|
61
|
+
.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
|
|
62
|
+
.replace(/<pre[^>]*>(.*?)<\/pre>/gi, '```\n$1\n```')
|
|
63
|
+
.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n')
|
|
64
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
65
|
+
.replace(/<\/div>/gi, '\n')
|
|
66
|
+
.replace(/<[^>]+>/g, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Shorten long URLs
|
|
71
|
+
*/
|
|
72
|
+
shortenUrls(text) {
|
|
73
|
+
return text.replace(/(https?:\/\/[^\s]{50,})/g, (match) => {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(match);
|
|
76
|
+
return `${url.protocol}//${url.host}/...${url.pathname.slice(-10)}`;
|
|
77
|
+
} catch {
|
|
78
|
+
return match.slice(0, this.maxUrlLength) + '...';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove non-ASCII characters
|
|
85
|
+
*/
|
|
86
|
+
removeNonASCII(text) {
|
|
87
|
+
return text.replace(/[^\x00-\x7F]+/g, (match) => {
|
|
88
|
+
// Keep common symbols like ©, ®, ™
|
|
89
|
+
return match.replace(/[^\x00-\x7F]/g, '');
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Deduplicate repeated phrases
|
|
95
|
+
*/
|
|
96
|
+
deduplicatePhrases(text) {
|
|
97
|
+
const words = text.split(/\s+/);
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const result = [];
|
|
100
|
+
|
|
101
|
+
for (const word of words) {
|
|
102
|
+
const lower = word.toLowerCase();
|
|
103
|
+
if (!seen.has(lower)) {
|
|
104
|
+
seen.add(lower);
|
|
105
|
+
result.push(word);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result.join(' ');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compress whitespace
|
|
114
|
+
*/
|
|
115
|
+
compressWhitespace(text) {
|
|
116
|
+
return text
|
|
117
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
118
|
+
.replace(/[ \t]{2,}/g, ' ')
|
|
119
|
+
.replace(/\n /g, '\n')
|
|
120
|
+
.trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Optimize code blocks
|
|
125
|
+
*/
|
|
126
|
+
optimizeCodeBlocks(text) {
|
|
127
|
+
return text
|
|
128
|
+
.replace(/```(\w+)\n([\s\S]*?)```/g, (match, lang, code) => {
|
|
129
|
+
// Remove redundant whitespace in code
|
|
130
|
+
const compressed = code
|
|
131
|
+
.split('\n')
|
|
132
|
+
.map(line => line.trimEnd())
|
|
133
|
+
.join('\n')
|
|
134
|
+
.trim();
|
|
135
|
+
return `\`\`\`${lang}\n${compressed}\n\`\`\``;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Split into chunks (max 3k tokens each)
|
|
141
|
+
*/
|
|
142
|
+
chunk(text) {
|
|
143
|
+
const chunks = [];
|
|
144
|
+
const words = text.split(/\s+/);
|
|
145
|
+
let current = [];
|
|
146
|
+
let currentSize = 0;
|
|
147
|
+
|
|
148
|
+
for (const word of words) {
|
|
149
|
+
currentSize += word.length + 1;
|
|
150
|
+
if (currentSize > this.maxChunkSize) {
|
|
151
|
+
chunks.push(current.join(' '));
|
|
152
|
+
current = [word];
|
|
153
|
+
currentSize = word.length + 1;
|
|
154
|
+
} else {
|
|
155
|
+
current.push(word);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (current.length > 0) {
|
|
160
|
+
chunks.push(current.join(' '));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return chunks;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get compression stats
|
|
168
|
+
*/
|
|
169
|
+
getStats(original, compressed) {
|
|
170
|
+
const reduction = ((original.length - compressed.length) / original.length * 100).toFixed(1);
|
|
171
|
+
return {
|
|
172
|
+
original: original.length,
|
|
173
|
+
compressed: compressed.length,
|
|
174
|
+
reduction: `${reduction}%`,
|
|
175
|
+
ratio: (compressed.length / original.length).toFixed(2)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { EnhancedCompression };
|