recursive-llm-ts 4.5.0 → 4.7.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.
@@ -0,0 +1,272 @@
1
+ package rlm
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ // ─── SplitSentences Tests ───────────────────────────────────────────────────
9
+
10
+ func TestSplitSentences_Basic(t *testing.T) {
11
+ text := "First sentence. Second sentence. Third sentence."
12
+ sentences := SplitSentences(text)
13
+
14
+ if len(sentences) != 3 {
15
+ t.Fatalf("expected 3 sentences, got %d: %v", len(sentences), sentences)
16
+ }
17
+ if sentences[0] != "First sentence." {
18
+ t.Errorf("sentence 0: %q", sentences[0])
19
+ }
20
+ if sentences[1] != "Second sentence." {
21
+ t.Errorf("sentence 1: %q", sentences[1])
22
+ }
23
+ if sentences[2] != "Third sentence." {
24
+ t.Errorf("sentence 2: %q", sentences[2])
25
+ }
26
+ }
27
+
28
+ func TestSplitSentences_ParagraphBreaks(t *testing.T) {
29
+ text := "First paragraph.\n\nSecond paragraph.\n\nThird paragraph."
30
+ sentences := SplitSentences(text)
31
+
32
+ if len(sentences) < 3 {
33
+ t.Fatalf("expected at least 3 sentences, got %d: %v", len(sentences), sentences)
34
+ }
35
+ }
36
+
37
+ func TestSplitSentences_MixedPunctuation(t *testing.T) {
38
+ text := "Is this a question? Yes it is! And here is a statement."
39
+ sentences := SplitSentences(text)
40
+
41
+ if len(sentences) != 3 {
42
+ t.Fatalf("expected 3 sentences, got %d: %v", len(sentences), sentences)
43
+ }
44
+ if sentences[0] != "Is this a question?" {
45
+ t.Errorf("sentence 0: %q", sentences[0])
46
+ }
47
+ }
48
+
49
+ func TestSplitSentences_Empty(t *testing.T) {
50
+ sentences := SplitSentences("")
51
+ if len(sentences) != 0 {
52
+ t.Errorf("expected 0 sentences for empty string, got %d", len(sentences))
53
+ }
54
+
55
+ sentences = SplitSentences(" ")
56
+ if len(sentences) != 0 {
57
+ t.Errorf("expected 0 sentences for whitespace, got %d", len(sentences))
58
+ }
59
+ }
60
+
61
+ func TestSplitSentences_NoTerminator(t *testing.T) {
62
+ text := "A sentence without punctuation"
63
+ sentences := SplitSentences(text)
64
+
65
+ if len(sentences) != 1 {
66
+ t.Fatalf("expected 1 sentence, got %d: %v", len(sentences), sentences)
67
+ }
68
+ if sentences[0] != "A sentence without punctuation" {
69
+ t.Errorf("sentence: %q", sentences[0])
70
+ }
71
+ }
72
+
73
+ // ─── TokenizeWords Tests ────────────────────────────────────────────────────
74
+
75
+ func TestTokenizeWords_Basic(t *testing.T) {
76
+ words := TokenizeWords("Hello, World! This is a test.")
77
+ expected := []string{"hello", "world", "this", "is", "a", "test"}
78
+
79
+ if len(words) != len(expected) {
80
+ t.Fatalf("expected %d words, got %d: %v", len(expected), len(words), words)
81
+ }
82
+ for i, w := range words {
83
+ if w != expected[i] {
84
+ t.Errorf("word %d: got %q, expected %q", i, w, expected[i])
85
+ }
86
+ }
87
+ }
88
+
89
+ func TestTokenizeWords_Numbers(t *testing.T) {
90
+ words := TokenizeWords("There are 42 cats and 7 dogs.")
91
+ // Should include numbers
92
+ found42 := false
93
+ for _, w := range words {
94
+ if w == "42" {
95
+ found42 = true
96
+ }
97
+ }
98
+ if !found42 {
99
+ t.Error("expected tokenized words to include '42'")
100
+ }
101
+ }
102
+
103
+ func TestTokenizeWords_Empty(t *testing.T) {
104
+ words := TokenizeWords("")
105
+ if len(words) != 0 {
106
+ t.Errorf("expected 0 words for empty string, got %d", len(words))
107
+ }
108
+ }
109
+
110
+ // ─── FilterStopWords Tests ──────────────────────────────────────────────────
111
+
112
+ func TestFilterStopWords(t *testing.T) {
113
+ words := []string{"the", "quick", "brown", "fox", "is", "a", "animal"}
114
+ filtered := FilterStopWords(words)
115
+
116
+ for _, w := range filtered {
117
+ if stopWords[w] {
118
+ t.Errorf("stop word %q was not filtered", w)
119
+ }
120
+ }
121
+
122
+ // "quick", "brown", "fox", "animal" should survive
123
+ if len(filtered) < 3 {
124
+ t.Errorf("expected at least 3 content words, got %d: %v", len(filtered), filtered)
125
+ }
126
+ }
127
+
128
+ // ─── ComputeTFIDF Tests ─────────────────────────────────────────────────────
129
+
130
+ func TestComputeTFIDF_Basic(t *testing.T) {
131
+ sentences := []string{
132
+ "The machine learning algorithm processes data efficiently.",
133
+ "Natural language processing uses deep learning models.",
134
+ "The weather today is sunny and warm.",
135
+ }
136
+
137
+ scored := ComputeTFIDF(sentences)
138
+
139
+ if len(scored) != 3 {
140
+ t.Fatalf("expected 3 scored sentences, got %d", len(scored))
141
+ }
142
+
143
+ // All scores should be non-negative
144
+ for i, s := range scored {
145
+ if s.Score < 0 {
146
+ t.Errorf("sentence %d has negative score: %f", i, s.Score)
147
+ }
148
+ if s.Index != i {
149
+ t.Errorf("sentence %d has wrong index: %d", i, s.Index)
150
+ }
151
+ }
152
+ }
153
+
154
+ func TestComputeTFIDF_UniqueTermsScoreHigher(t *testing.T) {
155
+ // A sentence with unique terms (not appearing in other sentences) should score higher
156
+ sentences := []string{
157
+ "Common words appear everywhere in text.",
158
+ "Common words appear everywhere in documents.",
159
+ "Quantum entanglement revolutionizes cryptographic security protocols.",
160
+ }
161
+
162
+ scored := ComputeTFIDF(sentences)
163
+
164
+ // The third sentence has unique terms not shared with others, so it should have a high score
165
+ // (though IDF will boost unique terms)
166
+ if scored[2].Score <= 0 {
167
+ t.Error("sentence with unique terms should have positive score")
168
+ }
169
+ }
170
+
171
+ func TestComputeTFIDF_Empty(t *testing.T) {
172
+ scored := ComputeTFIDF(nil)
173
+ if scored != nil {
174
+ t.Errorf("expected nil for empty input, got %v", scored)
175
+ }
176
+ }
177
+
178
+ func TestComputeTFIDF_PreservesIndex(t *testing.T) {
179
+ sentences := []string{"First.", "Second.", "Third."}
180
+ scored := ComputeTFIDF(sentences)
181
+
182
+ for i, s := range scored {
183
+ if s.Index != i {
184
+ t.Errorf("expected index %d, got %d", i, s.Index)
185
+ }
186
+ }
187
+ }
188
+
189
+ // ─── CompressContextTFIDF Tests ─────────────────────────────────────────────
190
+
191
+ func TestCompressContextTFIDF_NoCompressionNeeded(t *testing.T) {
192
+ text := "Short text that fits easily."
193
+ result := CompressContextTFIDF(text, 1000)
194
+
195
+ if result != text {
196
+ t.Errorf("expected unchanged text when no compression needed, got %q", result)
197
+ }
198
+ }
199
+
200
+ func TestCompressContextTFIDF_CompressesLargeText(t *testing.T) {
201
+ // Build a large document with many sentences
202
+ var sentences []string
203
+ for i := 0; i < 50; i++ {
204
+ sentences = append(sentences, "This is a test sentence with various content and information.")
205
+ }
206
+ // Add some unique high-value sentences
207
+ sentences = append(sentences, "The quantum computing breakthrough enables 1000x faster processing.")
208
+ sentences = append(sentences, "Revenue grew 47% year-over-year to reach $2.3 billion in Q4.")
209
+ text := strings.Join(sentences, " ")
210
+
211
+ // Request much smaller budget than the full text
212
+ originalTokens := EstimateTokens(text)
213
+ targetTokens := originalTokens / 3
214
+
215
+ result := CompressContextTFIDF(text, targetTokens)
216
+
217
+ resultTokens := EstimateTokens(result)
218
+ if resultTokens > targetTokens+10 { // Allow small slack
219
+ t.Errorf("compressed result (%d tokens) exceeds target (%d tokens)", resultTokens, targetTokens)
220
+ }
221
+
222
+ if len(result) >= len(text) {
223
+ t.Errorf("expected compressed result to be shorter: %d >= %d chars", len(result), len(text))
224
+ }
225
+ }
226
+
227
+ func TestCompressContextTFIDF_PreservesOrder(t *testing.T) {
228
+ text := "Alpha sentence first. Beta sentence second. Gamma sentence third. Delta sentence fourth. Epsilon sentence fifth."
229
+ // Very small budget to force selection of only a few sentences
230
+ result := CompressContextTFIDF(text, 20)
231
+
232
+ // The selected sentences should appear in their original order
233
+ sentences := SplitSentences(result)
234
+ if len(sentences) == 0 {
235
+ t.Fatal("expected at least one sentence in result")
236
+ }
237
+
238
+ // Verify order: if we find multiple sentences, their order should be preserved
239
+ for i := 1; i < len(sentences); i++ {
240
+ posI := strings.Index(text, sentences[i])
241
+ posPrev := strings.Index(text, sentences[i-1])
242
+ if posI < posPrev {
243
+ t.Errorf("sentence order not preserved: %q appears before %q in original but after in result",
244
+ sentences[i-1], sentences[i])
245
+ }
246
+ }
247
+ }
248
+
249
+ func TestCompressContextTFIDF_HighValueSentencesSelected(t *testing.T) {
250
+ // Mix of generic and specific/data-rich sentences
251
+ text := "The weather is nice today. " +
252
+ "It is a good day to go outside. " +
253
+ "The GDP of Japan reached $4.2 trillion in 2024 with 2.3% growth. " +
254
+ "Trees are green and the sky is blue. " +
255
+ "CRISPR-Cas9 gene editing achieved 99.7% accuracy in clinical trials at Johns Hopkins."
256
+
257
+ // Budget enough for ~2 sentences
258
+ result := CompressContextTFIDF(text, 40)
259
+
260
+ // The data-rich sentences should be selected over generic ones
261
+ hasSpecific := strings.Contains(result, "trillion") || strings.Contains(result, "CRISPR") || strings.Contains(result, "accuracy")
262
+ if !hasSpecific {
263
+ t.Errorf("expected high-value sentences to be selected, got: %q", result)
264
+ }
265
+ }
266
+
267
+ func TestCompressContextTFIDF_EmptyText(t *testing.T) {
268
+ result := CompressContextTFIDF("", 100)
269
+ if result != "" {
270
+ t.Errorf("expected empty result for empty input, got %q", result)
271
+ }
272
+ }
package/go/rlm/types.go CHANGED
@@ -66,6 +66,7 @@ type Config struct {
66
66
  ExtraParams map[string]interface{}
67
67
  MetaAgent *MetaAgentConfig
68
68
  Observability *ObservabilityConfig
69
+ ContextOverflow *ContextOverflowConfig
69
70
  }
70
71
 
71
72
  func ConfigFromMap(config map[string]interface{}) Config {
@@ -101,6 +102,27 @@ func ConfigFromMap(config map[string]interface{}) Config {
101
102
  parsed.MetaAgent = ma
102
103
  }
103
104
 
105
+ // Extract context_overflow config
106
+ if coConfig, ok := config["context_overflow"].(map[string]interface{}); ok {
107
+ co := DefaultContextOverflowConfig()
108
+ if v, ok := coConfig["enabled"].(bool); ok {
109
+ co.Enabled = v
110
+ }
111
+ if v, ok := toInt(coConfig["max_model_tokens"]); ok {
112
+ co.MaxModelTokens = v
113
+ }
114
+ if v, ok := coConfig["strategy"].(string); ok {
115
+ co.Strategy = v
116
+ }
117
+ if v, ok := coConfig["safety_margin"].(float64); ok {
118
+ co.SafetyMargin = v
119
+ }
120
+ if v, ok := toInt(coConfig["max_reduction_attempts"]); ok {
121
+ co.MaxReductionAttempts = v
122
+ }
123
+ parsed.ContextOverflow = &co
124
+ }
125
+
104
126
  for key, value := range config {
105
127
  switch key {
106
128
  case "recursive_model":
@@ -133,8 +155,9 @@ func ConfigFromMap(config map[string]interface{}) Config {
133
155
  "meta_agent", "observability", "debug", "trace_enabled",
134
156
  "trace_endpoint", "service_name", "log_output",
135
157
  "langfuse_enabled", "langfuse_public_key",
136
- "langfuse_secret_key", "langfuse_host":
137
- // ignore bridge-only config, meta_agent, observability (handled above/separately)
158
+ "langfuse_secret_key", "langfuse_host",
159
+ "context_overflow":
160
+ // ignore bridge-only config, meta_agent, observability, context_overflow (handled above/separately)
138
161
  default:
139
162
  parsed.ExtraParams[key] = value
140
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recursive-llm-ts",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "TypeScript bridge for recursive-llm: Recursive Language Models for unbounded context processing with structured outputs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",