recursive-llm-ts 2.0.12 → 3.0.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.
@@ -0,0 +1,260 @@
1
+ package rlm
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "regexp"
7
+ "strings"
8
+
9
+ "github.com/dop251/goja"
10
+ )
11
+
12
+ type REPLExecutor struct {
13
+ maxOutputChars int
14
+ }
15
+
16
+ func NewREPLExecutor() *REPLExecutor {
17
+ return &REPLExecutor{
18
+ maxOutputChars: 2000,
19
+ }
20
+ }
21
+
22
+ func (r *REPLExecutor) Execute(code string, env map[string]interface{}) (string, error) {
23
+ code = extractCode(code)
24
+ if strings.TrimSpace(code) == "" {
25
+ return "No code to execute", nil
26
+ }
27
+
28
+ vm := goja.New()
29
+ var output strings.Builder
30
+
31
+ for key, value := range env {
32
+ vm.Set(key, value)
33
+ }
34
+
35
+ writeOutput := func(call goja.FunctionCall) goja.Value {
36
+ parts := make([]string, 0, len(call.Arguments))
37
+ for _, arg := range call.Arguments {
38
+ parts = append(parts, arg.String())
39
+ }
40
+ output.WriteString(strings.Join(parts, " "))
41
+ output.WriteString("\n")
42
+ return goja.Undefined()
43
+ }
44
+
45
+ console := map[string]func(goja.FunctionCall) goja.Value{
46
+ "log": writeOutput,
47
+ }
48
+
49
+ vm.Set("console", console)
50
+ vm.Set("print", writeOutput)
51
+ vm.Set("len", func(value goja.Value) int {
52
+ if value == nil || value == goja.Undefined() || value == goja.Null() {
53
+ return 0
54
+ }
55
+ if exported := value.Export(); exported != nil {
56
+ switch typed := exported.(type) {
57
+ case string:
58
+ return len(typed)
59
+ case []interface{}:
60
+ return len(typed)
61
+ case map[string]interface{}:
62
+ return len(typed)
63
+ }
64
+ }
65
+ return len(value.String())
66
+ })
67
+
68
+ if _, err := vm.RunString(jsBootstrap); err != nil {
69
+ return "", NewREPLError("Bootstrap execution error", jsBootstrap, err)
70
+ }
71
+
72
+ if _, err := vm.RunString(code); err != nil {
73
+ return "", NewREPLError("Code execution error", code, err)
74
+ }
75
+
76
+ if output.Len() == 0 {
77
+ lastLine := getLastLine(code)
78
+ if looksLikeExpression(lastLine) {
79
+ value, err := vm.RunString(lastLine)
80
+ if err == nil && value != nil && value != goja.Undefined() {
81
+ output.WriteString(value.String())
82
+ output.WriteString("\n")
83
+ }
84
+ }
85
+ }
86
+
87
+ if output.Len() == 0 {
88
+ return "Code executed successfully (no output)", nil
89
+ }
90
+
91
+ rawOutput := output.String()
92
+ trimmedOutput := strings.TrimSpace(rawOutput)
93
+ if len(rawOutput) > r.maxOutputChars {
94
+ truncated := rawOutput[:r.maxOutputChars]
95
+ return fmt.Sprintf("%s\n\n[Output truncated: %d chars total, showing first %d]", strings.TrimSpace(truncated), len(rawOutput), r.maxOutputChars), nil
96
+ }
97
+
98
+ return trimmedOutput, nil
99
+ }
100
+
101
+ func extractCode(text string) string {
102
+ if strings.Contains(text, "```python") {
103
+ return extractBlock(text, "```python")
104
+ }
105
+ if strings.Contains(text, "```javascript") {
106
+ return extractBlock(text, "```javascript")
107
+ }
108
+ if strings.Contains(text, "```js") {
109
+ return extractBlock(text, "```js")
110
+ }
111
+ if strings.Contains(text, "```") {
112
+ return extractBlock(text, "```")
113
+ }
114
+ return text
115
+ }
116
+
117
+ const jsBootstrap = `
118
+ const json = {
119
+ loads: (text) => JSON.parse(text),
120
+ dumps: (value, replacer, space) => JSON.stringify(value, replacer, space),
121
+ };
122
+ const math = Math;
123
+ const datetime = Date;
124
+ const Counter = (iterable) => {
125
+ const counts = {};
126
+ if (iterable == null) {
127
+ return counts;
128
+ }
129
+ const items = typeof iterable === "string" ? iterable.split("") : iterable;
130
+ for (const item of items) {
131
+ const key = String(item);
132
+ counts[key] = (counts[key] || 0) + 1;
133
+ }
134
+ return counts;
135
+ };
136
+ const defaultdict = (defaultFactory) => new Proxy({}, {
137
+ get(target, prop) {
138
+ if (!(prop in target)) {
139
+ target[prop] = typeof defaultFactory === "function" ? defaultFactory() : defaultFactory;
140
+ }
141
+ return target[prop];
142
+ },
143
+ });
144
+ const range = (start, stop, step) => {
145
+ if (stop === undefined) {
146
+ stop = start;
147
+ start = 0;
148
+ }
149
+ if (step === undefined) {
150
+ step = 1;
151
+ }
152
+ if (step === 0) {
153
+ return [];
154
+ }
155
+ const result = [];
156
+ if (step > 0) {
157
+ for (let i = start; i < stop; i += step) {
158
+ result.push(i);
159
+ }
160
+ } else {
161
+ for (let i = start; i > stop; i += step) {
162
+ result.push(i);
163
+ }
164
+ }
165
+ return result;
166
+ };
167
+ const sorted = (iterable, compareFn) => [...iterable].sort(compareFn);
168
+ const sum = (iterable) => (iterable || []).reduce((acc, value) => acc + Number(value), 0);
169
+ const min = (iterable) => Math.min(...iterable);
170
+ const max = (iterable) => Math.max(...iterable);
171
+ const enumerate = (iterable) => (iterable || []).map((value, index) => [index, value]);
172
+ const zip = (...iterables) => {
173
+ const length = Math.min(...iterables.map((items) => items.length));
174
+ const result = [];
175
+ for (let i = 0; i < length; i++) {
176
+ result.push(iterables.map((items) => items[i]));
177
+ }
178
+ return result;
179
+ };
180
+ const any = (iterable) => (iterable || []).some(Boolean);
181
+ const all = (iterable) => (iterable || []).every(Boolean);
182
+ `
183
+
184
+ func extractBlock(text string, marker string) string {
185
+ start := strings.Index(text, marker)
186
+ if start == -1 {
187
+ return text
188
+ }
189
+ start += len(marker)
190
+ end := strings.Index(text[start:], "```")
191
+ if end == -1 {
192
+ return text[start:]
193
+ }
194
+ return strings.TrimSpace(text[start : start+end])
195
+ }
196
+
197
+ func getLastLine(code string) string {
198
+ lines := strings.Split(strings.TrimSpace(code), "\n")
199
+ if len(lines) == 0 {
200
+ return ""
201
+ }
202
+ lastLine := strings.TrimSpace(lines[len(lines)-1])
203
+ // Handle multiple statements on same line separated by semicolon
204
+ if strings.Contains(lastLine, ";") {
205
+ parts := strings.Split(lastLine, ";")
206
+ // Get the last non-empty part
207
+ for i := len(parts) - 1; i >= 0; i-- {
208
+ if trimmed := strings.TrimSpace(parts[i]); trimmed != "" {
209
+ return trimmed
210
+ }
211
+ }
212
+ }
213
+ return lastLine
214
+ }
215
+
216
+ func looksLikeExpression(line string) bool {
217
+ if line == "" {
218
+ return false
219
+ }
220
+ // Check for statements that shouldn't be evaluated as expressions
221
+ keywords := []string{"const ", "let ", "var ", "function ", "if ", "for ", "while ", "class ", "return "}
222
+ for _, keyword := range keywords {
223
+ if strings.HasPrefix(strings.TrimSpace(line), keyword) {
224
+ return false
225
+ }
226
+ }
227
+ // Check for assignment (but not == or === comparison)
228
+ if strings.Contains(line, "=") && !strings.Contains(line, "==") && !strings.Contains(line, "===") {
229
+ return false
230
+ }
231
+ return true
232
+ }
233
+
234
+ func NewRegexHelper() map[string]func(string, string) interface{} {
235
+ return map[string]func(string, string) interface{}{
236
+ "findall": func(pattern string, text string) interface{} {
237
+ re, err := regexpFromPattern(pattern)
238
+ if err != nil {
239
+ return []string{}
240
+ }
241
+ return re.FindAllString(text, -1)
242
+ },
243
+ "search": func(pattern string, text string) interface{} {
244
+ re, err := regexpFromPattern(pattern)
245
+ if err != nil {
246
+ return ""
247
+ }
248
+ match := re.FindString(text)
249
+ return match
250
+ },
251
+ }
252
+ }
253
+
254
+ func regexpFromPattern(pattern string) (*regexp.Regexp, error) {
255
+ re, err := regexp.Compile(pattern)
256
+ if err != nil {
257
+ return nil, errors.New("invalid regex pattern")
258
+ }
259
+ return re, nil
260
+ }
@@ -0,0 +1,291 @@
1
+ package rlm
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestREPLExecutor_BasicExecution(t *testing.T) {
9
+ repl := NewREPLExecutor()
10
+
11
+ tests := []struct {
12
+ name string
13
+ code string
14
+ env map[string]interface{}
15
+ want string
16
+ wantErr bool
17
+ }{
18
+ {
19
+ name: "Simple print",
20
+ code: `console.log("Hello World")`,
21
+ env: map[string]interface{}{},
22
+ want: "Hello World",
23
+ },
24
+ {
25
+ name: "Variable access",
26
+ code: `console.log(context)`,
27
+ env: map[string]interface{}{"context": "Test Context"},
28
+ want: "Test Context",
29
+ },
30
+ {
31
+ name: "String slicing",
32
+ code: `console.log(context.slice(0, 5))`,
33
+ env: map[string]interface{}{"context": "Hello World"},
34
+ want: "Hello",
35
+ },
36
+ {
37
+ name: "len function",
38
+ code: `console.log(len(context))`,
39
+ env: map[string]interface{}{"context": "Hello"},
40
+ want: "5",
41
+ },
42
+ {
43
+ name: "regex findall",
44
+ code: `console.log(re.findall("ERROR", context))`,
45
+ env: map[string]interface{}{"context": "ERROR 1 ERROR 2", "re": NewRegexHelper()},
46
+ want: "ERROR,ERROR",
47
+ },
48
+ {
49
+ name: "Last expression evaluation",
50
+ code: `const x = 42; x`,
51
+ env: map[string]interface{}{},
52
+ want: "42",
53
+ },
54
+ {
55
+ name: "Array operations",
56
+ code: `const arr = [1, 2, 3]; console.log(arr.length)`,
57
+ env: map[string]interface{}{},
58
+ want: "3",
59
+ },
60
+ }
61
+
62
+ for _, tt := range tests {
63
+ t.Run(tt.name, func(t *testing.T) {
64
+ got, err := repl.Execute(tt.code, tt.env)
65
+ if (err != nil) != tt.wantErr {
66
+ t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
67
+ return
68
+ }
69
+ if !tt.wantErr && got != tt.want {
70
+ t.Errorf("Execute() = %q, want %q", got, tt.want)
71
+ }
72
+ })
73
+ }
74
+ }
75
+
76
+ func TestREPLExecutor_CodeExtraction(t *testing.T) {
77
+ repl := NewREPLExecutor()
78
+
79
+ tests := []struct {
80
+ name string
81
+ code string
82
+ env map[string]interface{}
83
+ want string
84
+ }{
85
+ {
86
+ name: "JavaScript code block",
87
+ code: "```javascript\nconsole.log('test')\n```",
88
+ env: map[string]interface{}{},
89
+ want: "test",
90
+ },
91
+ {
92
+ name: "Python code block (should still extract)",
93
+ code: "```python\nconsole.log('test')\n```",
94
+ env: map[string]interface{}{},
95
+ want: "test",
96
+ },
97
+ {
98
+ name: "Generic code block",
99
+ code: "```\nconsole.log('test')\n```",
100
+ env: map[string]interface{}{},
101
+ want: "test",
102
+ },
103
+ {
104
+ name: "No code block",
105
+ code: "console.log('test')",
106
+ env: map[string]interface{}{},
107
+ want: "test",
108
+ },
109
+ }
110
+
111
+ for _, tt := range tests {
112
+ t.Run(tt.name, func(t *testing.T) {
113
+ got, err := repl.Execute(tt.code, tt.env)
114
+ if err != nil {
115
+ t.Errorf("Execute() error = %v", err)
116
+ return
117
+ }
118
+ if got != tt.want {
119
+ t.Errorf("Execute() = %q, want %q", got, tt.want)
120
+ }
121
+ })
122
+ }
123
+ }
124
+
125
+ func TestREPLExecutor_JSBootstrap(t *testing.T) {
126
+ repl := NewREPLExecutor()
127
+
128
+ tests := []struct {
129
+ name string
130
+ code string
131
+ want string
132
+ }{
133
+ {
134
+ name: "json.loads",
135
+ code: `const obj = json.loads('{"key":"value"}'); console.log(obj.key)`,
136
+ want: "value",
137
+ },
138
+ {
139
+ name: "json.dumps",
140
+ code: `console.log(json.dumps({key: "value"}))`,
141
+ want: `{"key":"value"}`,
142
+ },
143
+ {
144
+ name: "range",
145
+ code: `console.log(range(5).length)`,
146
+ want: "5",
147
+ },
148
+ {
149
+ name: "sum",
150
+ code: `console.log(sum([1, 2, 3]))`,
151
+ want: "6",
152
+ },
153
+ {
154
+ name: "Counter",
155
+ code: `const c = Counter("hello"); console.log(c.l)`,
156
+ want: "2",
157
+ },
158
+ {
159
+ name: "sorted",
160
+ code: `console.log(sorted([3, 1, 2]).join(','))`,
161
+ want: "1,2,3",
162
+ },
163
+ {
164
+ name: "enumerate",
165
+ code: `console.log(enumerate(['a', 'b']).length)`,
166
+ want: "2",
167
+ },
168
+ {
169
+ name: "zip",
170
+ code: `console.log(zip([1, 2], ['a', 'b']).length)`,
171
+ want: "2",
172
+ },
173
+ {
174
+ name: "any",
175
+ code: `console.log(any([false, true, false]))`,
176
+ want: "true",
177
+ },
178
+ {
179
+ name: "all",
180
+ code: `console.log(all([true, true, true]))`,
181
+ want: "true",
182
+ },
183
+ }
184
+
185
+ for _, tt := range tests {
186
+ t.Run(tt.name, func(t *testing.T) {
187
+ got, err := repl.Execute(tt.code, map[string]interface{}{})
188
+ if err != nil {
189
+ t.Errorf("Execute() error = %v", err)
190
+ return
191
+ }
192
+ if got != tt.want {
193
+ t.Errorf("Execute() = %q, want %q", got, tt.want)
194
+ }
195
+ })
196
+ }
197
+ }
198
+
199
+ func TestREPLExecutor_OutputTruncation(t *testing.T) {
200
+ repl := NewREPLExecutor()
201
+ repl.maxOutputChars = 50
202
+
203
+ longOutput := strings.Repeat("x", 100)
204
+ code := `console.log("` + longOutput + `")`
205
+
206
+ got, err := repl.Execute(code, map[string]interface{}{})
207
+ if err != nil {
208
+ t.Errorf("Execute() error = %v", err)
209
+ return
210
+ }
211
+
212
+ if len(got) <= repl.maxOutputChars {
213
+ t.Errorf("Expected truncation, but got length %d", len(got))
214
+ }
215
+
216
+ if !strings.Contains(got, "[Output truncated") {
217
+ t.Errorf("Expected truncation message in output")
218
+ }
219
+ }
220
+
221
+ func TestREPLExecutor_ErrorHandling(t *testing.T) {
222
+ repl := NewREPLExecutor()
223
+
224
+ tests := []struct {
225
+ name string
226
+ code string
227
+ wantErr bool
228
+ }{
229
+ {
230
+ name: "Syntax error",
231
+ code: `const x = ;`,
232
+ wantErr: true,
233
+ },
234
+ {
235
+ name: "Reference error",
236
+ code: `console.log(undefinedVariable)`,
237
+ wantErr: true,
238
+ },
239
+ {
240
+ name: "Valid code",
241
+ code: `console.log("ok")`,
242
+ wantErr: false,
243
+ },
244
+ }
245
+
246
+ for _, tt := range tests {
247
+ t.Run(tt.name, func(t *testing.T) {
248
+ _, err := repl.Execute(tt.code, map[string]interface{}{})
249
+ if (err != nil) != tt.wantErr {
250
+ t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
251
+ }
252
+ })
253
+ }
254
+ }
255
+
256
+ func TestRegexHelper(t *testing.T) {
257
+ re := NewRegexHelper()
258
+
259
+ t.Run("findall", func(t *testing.T) {
260
+ result := re["findall"]("ERROR", "ERROR 1 ERROR 2 WARNING")
261
+ matches, ok := result.([]string)
262
+ if !ok {
263
+ t.Fatal("Expected []string from findall")
264
+ }
265
+ if len(matches) != 2 {
266
+ t.Errorf("Expected 2 matches, got %d", len(matches))
267
+ }
268
+ })
269
+
270
+ t.Run("search", func(t *testing.T) {
271
+ result := re["search"]("ERROR", "INFO ERROR WARNING")
272
+ match, ok := result.(string)
273
+ if !ok {
274
+ t.Fatal("Expected string from search")
275
+ }
276
+ if match != "ERROR" {
277
+ t.Errorf("Expected 'ERROR', got %q", match)
278
+ }
279
+ })
280
+
281
+ t.Run("no match", func(t *testing.T) {
282
+ result := re["search"]("ERROR", "INFO WARNING")
283
+ match, ok := result.(string)
284
+ if !ok {
285
+ t.Fatal("Expected string from search")
286
+ }
287
+ if match != "" {
288
+ t.Errorf("Expected empty string, got %q", match)
289
+ }
290
+ })
291
+ }
@@ -0,0 +1,142 @@
1
+ package rlm
2
+
3
+ import (
4
+ "fmt"
5
+ )
6
+
7
+ type RLM struct {
8
+ model string
9
+ recursiveModel string
10
+ apiBase string
11
+ apiKey string
12
+ maxDepth int
13
+ maxIterations int
14
+ timeoutSeconds int
15
+ useMetacognitive bool
16
+ extraParams map[string]interface{}
17
+ currentDepth int
18
+ repl *REPLExecutor
19
+ stats RLMStats
20
+ }
21
+
22
+ func New(model string, config Config) *RLM {
23
+ recursiveModel := config.RecursiveModel
24
+ if recursiveModel == "" {
25
+ recursiveModel = model
26
+ }
27
+
28
+ return &RLM{
29
+ model: model,
30
+ recursiveModel: recursiveModel,
31
+ apiBase: config.APIBase,
32
+ apiKey: config.APIKey,
33
+ maxDepth: config.MaxDepth,
34
+ maxIterations: config.MaxIterations,
35
+ timeoutSeconds: config.TimeoutSeconds,
36
+ useMetacognitive: config.UseMetacognitive,
37
+ extraParams: config.ExtraParams,
38
+ currentDepth: 0,
39
+ repl: NewREPLExecutor(),
40
+ stats: RLMStats{},
41
+ }
42
+ }
43
+
44
+ func (r *RLM) Completion(query string, context string) (string, RLMStats, error) {
45
+ if query != "" && context == "" {
46
+ context = query
47
+ query = ""
48
+ }
49
+
50
+ if r.currentDepth >= r.maxDepth {
51
+ return "", r.stats, NewMaxDepthError(r.maxDepth)
52
+ }
53
+
54
+ r.stats.Depth = r.currentDepth
55
+ replEnv := r.buildREPLEnv(query, context)
56
+ systemPrompt := BuildSystemPrompt(len(context), r.currentDepth, query, r.useMetacognitive)
57
+ messages := []Message{
58
+ {Role: "system", Content: systemPrompt},
59
+ {Role: "user", Content: query},
60
+ }
61
+
62
+ for iteration := 0; iteration < r.maxIterations; iteration++ {
63
+ r.stats.Iterations = iteration + 1
64
+
65
+ response, err := r.callLLM(messages)
66
+ if err != nil {
67
+ return "", r.stats, err
68
+ }
69
+
70
+ if IsFinal(response) {
71
+ answer, ok := ParseResponse(response, replEnv)
72
+ if ok {
73
+ return answer, r.stats, nil
74
+ }
75
+ }
76
+
77
+ execResult, err := r.repl.Execute(response, replEnv)
78
+ if err != nil {
79
+ execResult = fmt.Sprintf("Error: %s", err.Error())
80
+ }
81
+
82
+ messages = append(messages, Message{Role: "assistant", Content: response})
83
+ messages = append(messages, Message{Role: "user", Content: execResult})
84
+ }
85
+
86
+ return "", r.stats, NewMaxIterationsError(r.maxIterations)
87
+ }
88
+
89
+ func (r *RLM) callLLM(messages []Message) (string, error) {
90
+ r.stats.LlmCalls++
91
+ defaultModel := r.model
92
+ if r.currentDepth > 0 {
93
+ defaultModel = r.recursiveModel
94
+ }
95
+
96
+ request := ChatRequest{
97
+ Model: defaultModel,
98
+ Messages: messages,
99
+ APIBase: r.apiBase,
100
+ APIKey: r.apiKey,
101
+ Timeout: r.timeoutSeconds,
102
+ ExtraParams: r.extraParams,
103
+ }
104
+
105
+ return CallChatCompletion(request)
106
+ }
107
+
108
+ func (r *RLM) buildREPLEnv(query string, context string) map[string]interface{} {
109
+ env := map[string]interface{}{
110
+ "context": context,
111
+ "query": query,
112
+ }
113
+
114
+ env["re"] = NewRegexHelper()
115
+ env["recursive_llm"] = func(subQuery string, subContext string) string {
116
+ if r.currentDepth+1 >= r.maxDepth {
117
+ return fmt.Sprintf("Max recursion depth (%d) reached", r.maxDepth)
118
+ }
119
+
120
+ subConfig := Config{
121
+ RecursiveModel: r.recursiveModel,
122
+ APIBase: r.apiBase,
123
+ APIKey: r.apiKey,
124
+ MaxDepth: r.maxDepth,
125
+ MaxIterations: r.maxIterations,
126
+ TimeoutSeconds: r.timeoutSeconds,
127
+ UseMetacognitive: r.useMetacognitive,
128
+ ExtraParams: r.extraParams,
129
+ }
130
+
131
+ subRLM := New(r.recursiveModel, subConfig)
132
+ subRLM.currentDepth = r.currentDepth + 1
133
+
134
+ answer, _, err := subRLM.Completion(subQuery, subContext)
135
+ if err != nil {
136
+ return fmt.Sprintf("Error: %s", err.Error())
137
+ }
138
+ return answer
139
+ }
140
+
141
+ return env
142
+ }