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.
- 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 -143
- package/dist/go-bridge.d.ts +5 -0
- package/dist/go-bridge.js +136 -0
- package/dist/rlm-bridge.js +28 -5
- 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 -72
|
@@ -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
|
+
}
|