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.
- package/README.md +60 -43
- package/bin/rlm-go +0 -0
- package/dist/bridge-factory.d.ts +1 -1
- package/dist/bridge-factory.js +44 -14
- package/dist/bridge-interface.d.ts +1 -0
- package/dist/bunpy-bridge.d.ts +3 -4
- package/dist/bunpy-bridge.js +11 -164
- package/dist/go-bridge.d.ts +5 -0
- package/dist/go-bridge.js +136 -0
- package/dist/rlm-bridge.js +3 -1
- package/go/README.md +347 -0
- package/go/cmd/rlm/main.go +63 -0
- package/go/go.mod +12 -0
- package/go/go.sum +57 -0
- package/go/integration_test.sh +169 -0
- package/go/internal/rlm/benchmark_test.go +168 -0
- package/go/internal/rlm/errors.go +83 -0
- package/go/internal/rlm/openai.go +128 -0
- package/go/internal/rlm/parser.go +53 -0
- package/go/internal/rlm/parser_test.go +202 -0
- package/go/internal/rlm/prompt.go +68 -0
- package/go/internal/rlm/repl.go +260 -0
- package/go/internal/rlm/repl_test.go +291 -0
- package/go/internal/rlm/rlm.go +142 -0
- package/go/internal/rlm/types.go +108 -0
- package/go/test_mock.sh +90 -0
- package/go/test_rlm.sh +41 -0
- package/go/test_simple.sh +78 -0
- package/package.json +6 -9
- package/scripts/build-go-binary.js +41 -0
- package/recursive-llm/pyproject.toml +0 -70
- package/recursive-llm/src/rlm/__init__.py +0 -14
- package/recursive-llm/src/rlm/core.py +0 -322
- package/recursive-llm/src/rlm/parser.py +0 -93
- package/recursive-llm/src/rlm/prompts.py +0 -50
- package/recursive-llm/src/rlm/repl.py +0 -235
- package/recursive-llm/src/rlm/types.py +0 -37
- package/scripts/install-python-deps.js +0 -101
|
@@ -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
|
+
}
|