recursive-llm-ts 2.0.11 → 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,202 @@
1
+ package rlm
2
+
3
+ import (
4
+ "testing"
5
+ )
6
+
7
+ func TestIsFinal(t *testing.T) {
8
+ tests := []struct {
9
+ name string
10
+ response string
11
+ want bool
12
+ }{
13
+ {"FINAL with double quotes", `FINAL("answer")`, true},
14
+ {"FINAL with single quotes", `FINAL('answer')`, true},
15
+ {"FINAL with triple double quotes", `FINAL("""answer""")`, true},
16
+ {"FINAL_VAR", `FINAL_VAR(result)`, true},
17
+ {"No FINAL", `x = 1`, false},
18
+ {"Contains FINAL as substring", `This is FINALLY done`, false},
19
+ }
20
+
21
+ for _, tt := range tests {
22
+ t.Run(tt.name, func(t *testing.T) {
23
+ if got := IsFinal(tt.response); got != tt.want {
24
+ t.Errorf("IsFinal() = %v, want %v", got, tt.want)
25
+ }
26
+ })
27
+ }
28
+ }
29
+
30
+ func TestExtractFinal(t *testing.T) {
31
+ tests := []struct {
32
+ name string
33
+ response string
34
+ want string
35
+ wantOk bool
36
+ }{
37
+ {
38
+ name: "Double quotes",
39
+ response: `FINAL("The answer is 42")`,
40
+ want: "The answer is 42",
41
+ wantOk: true,
42
+ },
43
+ {
44
+ name: "Single quotes",
45
+ response: `FINAL('The answer is 42')`,
46
+ want: "The answer is 42",
47
+ wantOk: true,
48
+ },
49
+ {
50
+ name: "Triple double quotes",
51
+ response: `FINAL("""The answer is 42""")`,
52
+ want: "The answer is 42",
53
+ wantOk: true,
54
+ },
55
+ {
56
+ name: "Triple single quotes",
57
+ response: `FINAL('''The answer is 42''')`,
58
+ want: "The answer is 42",
59
+ wantOk: true,
60
+ },
61
+ {
62
+ name: "Multiline with triple quotes",
63
+ response: `FINAL("""Line 1
64
+ Line 2
65
+ Line 3""")`,
66
+ want: "Line 1\nLine 2\nLine 3",
67
+ wantOk: true,
68
+ },
69
+ {
70
+ name: "With whitespace",
71
+ response: `FINAL( "The answer" )`,
72
+ want: "The answer",
73
+ wantOk: true,
74
+ },
75
+ {
76
+ name: "No FINAL",
77
+ response: `x = 1`,
78
+ want: "",
79
+ wantOk: false,
80
+ },
81
+ {
82
+ name: "FINAL_VAR should not match",
83
+ response: `FINAL_VAR(result)`,
84
+ want: "",
85
+ wantOk: false,
86
+ },
87
+ }
88
+
89
+ for _, tt := range tests {
90
+ t.Run(tt.name, func(t *testing.T) {
91
+ got, gotOk := extractFinal(tt.response)
92
+ if gotOk != tt.wantOk {
93
+ t.Errorf("extractFinal() ok = %v, want %v", gotOk, tt.wantOk)
94
+ }
95
+ if got != tt.want {
96
+ t.Errorf("extractFinal() = %q, want %q", got, tt.want)
97
+ }
98
+ })
99
+ }
100
+ }
101
+
102
+ func TestExtractFinalVar(t *testing.T) {
103
+ tests := []struct {
104
+ name string
105
+ response string
106
+ env map[string]interface{}
107
+ want string
108
+ wantOk bool
109
+ }{
110
+ {
111
+ name: "Simple variable",
112
+ response: `FINAL_VAR(result)`,
113
+ env: map[string]interface{}{"result": "The answer"},
114
+ want: "The answer",
115
+ wantOk: true,
116
+ },
117
+ {
118
+ name: "Integer variable",
119
+ response: `FINAL_VAR(count)`,
120
+ env: map[string]interface{}{"count": 42},
121
+ want: "42",
122
+ wantOk: true,
123
+ },
124
+ {
125
+ name: "Variable not found",
126
+ response: `FINAL_VAR(missing)`,
127
+ env: map[string]interface{}{"result": "The answer"},
128
+ want: "",
129
+ wantOk: false,
130
+ },
131
+ {
132
+ name: "With whitespace",
133
+ response: `FINAL_VAR( result )`,
134
+ env: map[string]interface{}{"result": "The answer"},
135
+ want: "The answer",
136
+ wantOk: true,
137
+ },
138
+ {
139
+ name: "No FINAL_VAR",
140
+ response: `x = 1`,
141
+ env: map[string]interface{}{},
142
+ want: "",
143
+ wantOk: false,
144
+ },
145
+ }
146
+
147
+ for _, tt := range tests {
148
+ t.Run(tt.name, func(t *testing.T) {
149
+ got, gotOk := extractFinalVar(tt.response, tt.env)
150
+ if gotOk != tt.wantOk {
151
+ t.Errorf("extractFinalVar() ok = %v, want %v", gotOk, tt.wantOk)
152
+ }
153
+ if got != tt.want {
154
+ t.Errorf("extractFinalVar() = %q, want %q", got, tt.want)
155
+ }
156
+ })
157
+ }
158
+ }
159
+
160
+ func TestParseResponse(t *testing.T) {
161
+ tests := []struct {
162
+ name string
163
+ response string
164
+ env map[string]interface{}
165
+ want string
166
+ wantOk bool
167
+ }{
168
+ {
169
+ name: "FINAL takes precedence",
170
+ response: `FINAL("Direct answer")`,
171
+ env: map[string]interface{}{"result": "Var answer"},
172
+ want: "Direct answer",
173
+ wantOk: true,
174
+ },
175
+ {
176
+ name: "FINAL_VAR fallback",
177
+ response: `FINAL_VAR(result)`,
178
+ env: map[string]interface{}{"result": "Var answer"},
179
+ want: "Var answer",
180
+ wantOk: true,
181
+ },
182
+ {
183
+ name: "Neither",
184
+ response: `x = 1`,
185
+ env: map[string]interface{}{},
186
+ want: "",
187
+ wantOk: false,
188
+ },
189
+ }
190
+
191
+ for _, tt := range tests {
192
+ t.Run(tt.name, func(t *testing.T) {
193
+ got, gotOk := ParseResponse(tt.response, tt.env)
194
+ if gotOk != tt.wantOk {
195
+ t.Errorf("ParseResponse() ok = %v, want %v", gotOk, tt.wantOk)
196
+ }
197
+ if got != tt.want {
198
+ t.Errorf("ParseResponse() = %q, want %q", got, tt.want)
199
+ }
200
+ })
201
+ }
202
+ }
@@ -0,0 +1,68 @@
1
+ package rlm
2
+
3
+ import "fmt"
4
+
5
+ // BuildSystemPrompt creates the system prompt for RLM
6
+ // useMetacognitive enables step-by-step reasoning guidance
7
+ func BuildSystemPrompt(contextSize int, depth int, query string, useMetacognitive bool) string {
8
+ if useMetacognitive {
9
+ return buildMetacognitivePrompt(contextSize, depth, query)
10
+ }
11
+ return buildMinimalPrompt(contextSize, depth, query)
12
+ }
13
+
14
+ func buildMinimalPrompt(contextSize int, depth int, query string) string {
15
+ return fmt.Sprintf(`You are a Recursive Language Model. You interact with context through a JavaScript REPL environment.
16
+
17
+ The context is stored in variable "context" (not in this prompt). Size: %d characters.
18
+
19
+ Available in environment:
20
+ - context: string (the document to analyze)
21
+ - query: string (the question: %q)
22
+ - recursive_llm(sub_query, sub_context) -> string (recursively process sub-context)
23
+ - re: regex helper with findall(pattern, text) and search(pattern, text)
24
+ - print(value, ...) -> output text
25
+ - len(value) -> length of arrays/strings
26
+ - json: helper with loads() and dumps()
27
+ - math: Math helper
28
+ - datetime: Date helper
29
+ - Counter(iterable) -> object of counts
30
+ - defaultdict(defaultFactory) -> object with defaults
31
+
32
+ Write JavaScript code to answer the query. The last expression or console.log output will be shown to you.
33
+
34
+ Examples:
35
+ - console.log(context.slice(0, 100)); // See first 100 chars
36
+ - const errors = re.findall("ERROR", context);
37
+ - const count = errors.length; console.log(count);
38
+
39
+ When you have the answer, use FINAL("answer") - this is NOT a function, just write it as text.
40
+
41
+ Depth: %d`, contextSize, query, depth)
42
+ }
43
+
44
+ func buildMetacognitivePrompt(contextSize int, depth int, query string) string {
45
+ return fmt.Sprintf(`You are a Recursive Language Model. You interact with context through a JavaScript REPL environment.
46
+
47
+ The context is stored in variable "context" (not in this prompt). Size: %d characters.
48
+
49
+ Available in environment:
50
+ - context: string (the document to analyze)
51
+ - query: string (the question: %q)
52
+ - recursive_llm(sub_query, sub_context) -> string (recursively process sub-context)
53
+ - re: regex helper with findall(pattern, text) and search(pattern, text)
54
+ - print(value, ...) -> output text
55
+ - len(value) -> length of arrays/strings
56
+ - json: helper with loads() and dumps()
57
+ - math: Math helper
58
+ - Counter(iterable) -> object of counts
59
+
60
+ STRATEGY TIP: You can peek at context first to understand its structure before processing.
61
+ Example: console.log(context.slice(0, 100))
62
+
63
+ Write JavaScript code to answer the query. The last expression or console.log output will be shown to you.
64
+
65
+ When you have the answer, use FINAL("answer") - this is NOT a function, just write it as text.
66
+
67
+ Depth: %d`, contextSize, query, depth)
68
+ }
@@ -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
+ }