recursive-llm-ts 4.9.0 → 5.0.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,239 @@
1
+ package rlm
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ )
7
+
8
+ // ─── Five-Level Summarization Escalation ────────────────────────────────────
9
+ // Implements the guaranteed-convergence summarization protocol from the LCM paper.
10
+ // Level 1 (Normal): LLM-Summarize with preserve_details mode
11
+ // Level 2 (Aggressive): LLM-Summarize with bullet_points mode, half target tokens
12
+ // Level 3 (TF-IDF): Extractive compression, no LLM, preserves actual sentences
13
+ // Level 4 (TextRank): Graph-based extractive compression, no LLM, better coherence
14
+ // Level 5 (Deterministic): DeterministicTruncate, no LLM, guaranteed reduction
15
+
16
+ // LCMSummarizer handles the five-level escalation for context compaction.
17
+ type LCMSummarizer struct {
18
+ model string
19
+ apiBase string
20
+ apiKey string
21
+ timeout int
22
+ extraParams map[string]interface{}
23
+ observer *Observer
24
+ }
25
+
26
+ // NewLCMSummarizer creates a summarizer with the given LLM configuration.
27
+ func NewLCMSummarizer(model, apiBase, apiKey string, timeout int, extraParams map[string]interface{}, observer *Observer) *LCMSummarizer {
28
+ return &LCMSummarizer{
29
+ model: model,
30
+ apiBase: apiBase,
31
+ apiKey: apiKey,
32
+ timeout: timeout,
33
+ extraParams: extraParams,
34
+ observer: observer,
35
+ }
36
+ }
37
+
38
+ // SummarizeResult contains the output of a summarization attempt.
39
+ type SummarizeResult struct {
40
+ Content string // The summary text
41
+ Tokens int // Token count of the summary
42
+ Level int // Escalation level used (1-5)
43
+ }
44
+
45
+ // Summarize applies the five-level escalation to compress text to targetTokens.
46
+ // Guaranteed to converge: Level 5 is deterministic truncation.
47
+ func (ls *LCMSummarizer) Summarize(input string, targetTokens int) (*SummarizeResult, error) {
48
+ inputTokens := EstimateTokens(input)
49
+
50
+ // If already within budget, no summarization needed
51
+ if inputTokens <= targetTokens {
52
+ return &SummarizeResult{
53
+ Content: input,
54
+ Tokens: inputTokens,
55
+ Level: 0,
56
+ }, nil
57
+ }
58
+
59
+ ls.observer.Debug("lcm.summarizer", "Starting escalation: %d tokens → target %d", inputTokens, targetTokens)
60
+
61
+ // Level 1: Normal - preserve details
62
+ result, err := ls.summarizeLevel1(input, targetTokens)
63
+ if err == nil && result.Tokens < inputTokens {
64
+ ls.observer.Debug("lcm.summarizer", "Level 1 succeeded: %d → %d tokens", inputTokens, result.Tokens)
65
+ return result, nil
66
+ }
67
+ if err != nil {
68
+ ls.observer.Debug("lcm.summarizer", "Level 1 failed: %v, escalating", err)
69
+ } else {
70
+ ls.observer.Debug("lcm.summarizer", "Level 1 did not reduce (%d >= %d), escalating", result.Tokens, inputTokens)
71
+ }
72
+
73
+ // Level 2: Aggressive - bullet points at half target
74
+ result, err = ls.summarizeLevel2(input, targetTokens/2)
75
+ if err == nil && result.Tokens < inputTokens {
76
+ ls.observer.Debug("lcm.summarizer", "Level 2 succeeded: %d → %d tokens", inputTokens, result.Tokens)
77
+ return result, nil
78
+ }
79
+ if err != nil {
80
+ ls.observer.Debug("lcm.summarizer", "Level 2 failed: %v, escalating", err)
81
+ } else {
82
+ ls.observer.Debug("lcm.summarizer", "Level 2 did not reduce (%d >= %d), escalating", result.Tokens, inputTokens)
83
+ }
84
+
85
+ // Level 3: TF-IDF extractive compression
86
+ result = ls.summarizeLevel3TFIDF(input, targetTokens)
87
+ if result.Tokens < inputTokens {
88
+ ls.observer.Debug("lcm.summarizer", "Level 3 (TF-IDF) succeeded: %d → %d tokens", inputTokens, result.Tokens)
89
+ return result, nil
90
+ }
91
+ ls.observer.Debug("lcm.summarizer", "Level 3 (TF-IDF) did not reduce enough (%d >= %d), escalating", result.Tokens, inputTokens)
92
+
93
+ // Level 4: TextRank graph-based compression
94
+ result = ls.summarizeLevel4TextRank(input, targetTokens)
95
+ if result.Tokens < inputTokens {
96
+ ls.observer.Debug("lcm.summarizer", "Level 4 (TextRank) succeeded: %d → %d tokens", inputTokens, result.Tokens)
97
+ return result, nil
98
+ }
99
+ ls.observer.Debug("lcm.summarizer", "Level 4 (TextRank) did not reduce enough (%d >= %d), escalating to deterministic", result.Tokens, inputTokens)
100
+
101
+ // Level 5: Deterministic truncation - guaranteed convergence
102
+ result = ls.deterministicTruncate(input, targetTokens)
103
+ ls.observer.Debug("lcm.summarizer", "Level 5 (deterministic): %d → %d tokens", inputTokens, result.Tokens)
104
+ return result, nil
105
+ }
106
+
107
+ // SummarizeMessages applies three-level escalation to a slice of messages.
108
+ func (ls *LCMSummarizer) SummarizeMessages(messages []*StoreMessage, targetTokens int) (*SummarizeResult, error) {
109
+ // Build a formatted input from messages
110
+ var sb strings.Builder
111
+ for _, msg := range messages {
112
+ sb.WriteString(fmt.Sprintf("[%s] %s\n", msg.Role, msg.Content))
113
+ }
114
+ return ls.Summarize(sb.String(), targetTokens)
115
+ }
116
+
117
+ // ─── Level 1: Normal LLM Summarization ──────────────────────────────────────
118
+
119
+ func (ls *LCMSummarizer) summarizeLevel1(input string, targetTokens int) (*SummarizeResult, error) {
120
+ prompt := fmt.Sprintf(`Summarize the following content in approximately %d tokens.
121
+ Preserve all key details, decisions, and specific information.
122
+ Maintain the logical flow and any action items or conclusions.
123
+
124
+ Content:
125
+ %s
126
+
127
+ Provide a comprehensive summary that retains the most important information:`, targetTokens, input)
128
+
129
+ content, err := ls.callLLM(prompt, targetTokens)
130
+ if err != nil {
131
+ return nil, err
132
+ }
133
+
134
+ return &SummarizeResult{
135
+ Content: content,
136
+ Tokens: EstimateTokens(content),
137
+ Level: 1,
138
+ }, nil
139
+ }
140
+
141
+ // ─── Level 2: Aggressive LLM Summarization ──────────────────────────────────
142
+
143
+ func (ls *LCMSummarizer) summarizeLevel2(input string, targetTokens int) (*SummarizeResult, error) {
144
+ prompt := fmt.Sprintf(`Aggressively summarize the following content as bullet points.
145
+ Target: approximately %d tokens. Be extremely concise.
146
+ Keep only: key decisions, critical facts, action items, and conclusions.
147
+ Drop: examples, explanations, context, and elaboration.
148
+
149
+ Content:
150
+ %s
151
+
152
+ Bullet-point summary:`, targetTokens, input)
153
+
154
+ content, err := ls.callLLM(prompt, targetTokens)
155
+ if err != nil {
156
+ return nil, err
157
+ }
158
+
159
+ return &SummarizeResult{
160
+ Content: content,
161
+ Tokens: EstimateTokens(content),
162
+ Level: 2,
163
+ }, nil
164
+ }
165
+
166
+ // ─── Level 3: TF-IDF Extractive Compression ─────────────────────────────────
167
+
168
+ func (ls *LCMSummarizer) summarizeLevel3TFIDF(input string, targetTokens int) *SummarizeResult {
169
+ compressed := CompressContextTFIDF(input, targetTokens)
170
+ return &SummarizeResult{
171
+ Content: compressed,
172
+ Tokens: EstimateTokens(compressed),
173
+ Level: 3,
174
+ }
175
+ }
176
+
177
+ // ─── Level 4: TextRank Graph-Based Compression ─────────────────────────────
178
+
179
+ func (ls *LCMSummarizer) summarizeLevel4TextRank(input string, targetTokens int) *SummarizeResult {
180
+ compressed := CompressContextTextRank(input, targetTokens)
181
+ return &SummarizeResult{
182
+ Content: compressed,
183
+ Tokens: EstimateTokens(compressed),
184
+ Level: 4,
185
+ }
186
+ }
187
+
188
+ // ─── Level 5: Deterministic Truncation ──────────────────────────────────────
189
+
190
+ // deterministicTruncate is the guaranteed-convergence fallback.
191
+ // No LLM call involved — uses the shared TruncateText utility (compression.go).
192
+ func (ls *LCMSummarizer) deterministicTruncate(input string, maxTokens int) *SummarizeResult {
193
+ truncated := TruncateText(input, TruncateTextParams{
194
+ MaxTokens: maxTokens,
195
+ MarkerText: "\n[... content truncated for context management ...]\n",
196
+ })
197
+
198
+ return &SummarizeResult{
199
+ Content: truncated,
200
+ Tokens: EstimateTokens(truncated),
201
+ Level: 5,
202
+ }
203
+ }
204
+
205
+ // ─── LLM Helper ─────────────────────────────────────────────────────────────
206
+
207
+ func (ls *LCMSummarizer) callLLM(prompt string, maxTokens int) (string, error) {
208
+ // Cap max_tokens for summarization
209
+ if maxTokens > 4096 {
210
+ maxTokens = 4096
211
+ }
212
+ if maxTokens < 256 {
213
+ maxTokens = 256
214
+ }
215
+
216
+ params := make(map[string]interface{})
217
+ for k, v := range ls.extraParams {
218
+ params[k] = v
219
+ }
220
+ params["max_tokens"] = maxTokens
221
+
222
+ request := ChatRequest{
223
+ Model: ls.model,
224
+ Messages: []Message{
225
+ {Role: "user", Content: prompt},
226
+ },
227
+ APIBase: ls.apiBase,
228
+ APIKey: ls.apiKey,
229
+ Timeout: ls.timeout,
230
+ ExtraParams: params,
231
+ }
232
+
233
+ result, err := CallChatCompletion(request)
234
+ if err != nil {
235
+ return "", fmt.Errorf("summarization LLM call failed: %w", err)
236
+ }
237
+
238
+ return result.Content, nil
239
+ }