recursive-llm-ts 2.0.12 → 3.0.2
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,168 @@
|
|
|
1
|
+
package rlm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// Benchmark parser performance
|
|
9
|
+
func BenchmarkIsFinal(b *testing.B) {
|
|
10
|
+
responses := []string{
|
|
11
|
+
`FINAL("answer")`,
|
|
12
|
+
`FINAL_VAR(result)`,
|
|
13
|
+
`x = 1`,
|
|
14
|
+
`console.log("test")`,
|
|
15
|
+
}
|
|
16
|
+
b.ResetTimer()
|
|
17
|
+
for i := 0; i < b.N; i++ {
|
|
18
|
+
IsFinal(responses[i%len(responses)])
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func BenchmarkExtractFinal(b *testing.B) {
|
|
23
|
+
response := `FINAL("This is a test answer with some content")`
|
|
24
|
+
b.ResetTimer()
|
|
25
|
+
for i := 0; i < b.N; i++ {
|
|
26
|
+
extractFinal(response)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func BenchmarkParseResponse(b *testing.B) {
|
|
31
|
+
response := `FINAL("Test answer")`
|
|
32
|
+
env := map[string]interface{}{
|
|
33
|
+
"result": "test",
|
|
34
|
+
}
|
|
35
|
+
b.ResetTimer()
|
|
36
|
+
for i := 0; i < b.N; i++ {
|
|
37
|
+
ParseResponse(response, env)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Benchmark REPL performance
|
|
42
|
+
func BenchmarkREPLSimpleExecution(b *testing.B) {
|
|
43
|
+
repl := NewREPLExecutor()
|
|
44
|
+
code := `console.log("Hello World")`
|
|
45
|
+
env := map[string]interface{}{}
|
|
46
|
+
|
|
47
|
+
b.ResetTimer()
|
|
48
|
+
for i := 0; i < b.N; i++ {
|
|
49
|
+
_, _ = repl.Execute(code, env)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func BenchmarkREPLContextAccess(b *testing.B) {
|
|
54
|
+
repl := NewREPLExecutor()
|
|
55
|
+
code := `console.log(context.slice(0, 10))`
|
|
56
|
+
env := map[string]interface{}{
|
|
57
|
+
"context": strings.Repeat("Lorem ipsum dolor sit amet. ", 1000),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
b.ResetTimer()
|
|
61
|
+
for i := 0; i < b.N; i++ {
|
|
62
|
+
_, _ = repl.Execute(code, env)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func BenchmarkREPLRegex(b *testing.B) {
|
|
67
|
+
repl := NewREPLExecutor()
|
|
68
|
+
code := `const matches = re.findall("ERROR", context); console.log(matches.length)`
|
|
69
|
+
context := strings.Repeat("INFO ERROR WARNING ", 100)
|
|
70
|
+
env := map[string]interface{}{
|
|
71
|
+
"context": context,
|
|
72
|
+
"re": NewRegexHelper(),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
b.ResetTimer()
|
|
76
|
+
for i := 0; i < b.N; i++ {
|
|
77
|
+
_, _ = repl.Execute(code, env)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func BenchmarkREPLJSBootstrap(b *testing.B) {
|
|
82
|
+
repl := NewREPLExecutor()
|
|
83
|
+
code := `const arr = range(100); const s = sum(arr); console.log(s)`
|
|
84
|
+
env := map[string]interface{}{}
|
|
85
|
+
|
|
86
|
+
b.ResetTimer()
|
|
87
|
+
for i := 0; i < b.N; i++ {
|
|
88
|
+
_, _ = repl.Execute(code, env)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Benchmark regex helper
|
|
93
|
+
func BenchmarkRegexFindall(b *testing.B) {
|
|
94
|
+
re := NewRegexHelper()
|
|
95
|
+
text := strings.Repeat("ERROR INFO WARNING ERROR ", 100)
|
|
96
|
+
|
|
97
|
+
b.ResetTimer()
|
|
98
|
+
for i := 0; i < b.N; i++ {
|
|
99
|
+
re["findall"]("ERROR", text)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func BenchmarkRegexSearch(b *testing.B) {
|
|
104
|
+
re := NewRegexHelper()
|
|
105
|
+
text := strings.Repeat("INFO WARNING ", 50) + "ERROR" + strings.Repeat(" INFO WARNING", 50)
|
|
106
|
+
|
|
107
|
+
b.ResetTimer()
|
|
108
|
+
for i := 0; i < b.N; i++ {
|
|
109
|
+
re["search"]("ERROR", text)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Benchmark config parsing
|
|
114
|
+
func BenchmarkConfigFromMap(b *testing.B) {
|
|
115
|
+
config := map[string]interface{}{
|
|
116
|
+
"recursive_model": "gpt-4o-mini",
|
|
117
|
+
"api_base": "https://api.openai.com/v1",
|
|
118
|
+
"api_key": "sk-test",
|
|
119
|
+
"max_depth": 5,
|
|
120
|
+
"max_iterations": 30,
|
|
121
|
+
"temperature": 0.7,
|
|
122
|
+
"extra_param": "value",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
b.ResetTimer()
|
|
126
|
+
for i := 0; i < b.N; i++ {
|
|
127
|
+
ConfigFromMap(config)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Benchmark code extraction
|
|
132
|
+
func BenchmarkExtractCode(b *testing.B) {
|
|
133
|
+
code := "```javascript\nconsole.log('test')\nconst x = 42\n```"
|
|
134
|
+
|
|
135
|
+
b.ResetTimer()
|
|
136
|
+
for i := 0; i < b.N; i++ {
|
|
137
|
+
extractCode(code)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Memory allocation benchmarks
|
|
142
|
+
func BenchmarkREPLMemoryAllocation(b *testing.B) {
|
|
143
|
+
repl := NewREPLExecutor()
|
|
144
|
+
code := `const arr = []; for (let i = 0; i < 1000; i++) arr.push(i); console.log(arr.length)`
|
|
145
|
+
env := map[string]interface{}{}
|
|
146
|
+
|
|
147
|
+
b.ReportAllocs()
|
|
148
|
+
b.ResetTimer()
|
|
149
|
+
for i := 0; i < b.N; i++ {
|
|
150
|
+
_, _ = repl.Execute(code, env)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func BenchmarkLargeContextAccess(b *testing.B) {
|
|
155
|
+
repl := NewREPLExecutor()
|
|
156
|
+
// Simulate 100KB context
|
|
157
|
+
largeContext := strings.Repeat("Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 2000)
|
|
158
|
+
code := `const first = context.slice(0, 100); const last = context.slice(-100); console.log(first.length + last.length)`
|
|
159
|
+
env := map[string]interface{}{
|
|
160
|
+
"context": largeContext,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
b.ReportAllocs()
|
|
164
|
+
b.ResetTimer()
|
|
165
|
+
for i := 0; i < b.N; i++ {
|
|
166
|
+
_, _ = repl.Execute(code, env)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package rlm
|
|
2
|
+
|
|
3
|
+
import "fmt"
|
|
4
|
+
|
|
5
|
+
// RLMError is the base error type for all RLM errors
|
|
6
|
+
type RLMError struct {
|
|
7
|
+
Message string
|
|
8
|
+
Cause error
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func (e *RLMError) Error() string {
|
|
12
|
+
if e.Cause != nil {
|
|
13
|
+
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
|
|
14
|
+
}
|
|
15
|
+
return e.Message
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func (e *RLMError) Unwrap() error {
|
|
19
|
+
return e.Cause
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MaxIterationsError is returned when max iterations are exceeded
|
|
23
|
+
type MaxIterationsError struct {
|
|
24
|
+
MaxIterations int
|
|
25
|
+
*RLMError
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func NewMaxIterationsError(maxIterations int) *MaxIterationsError {
|
|
29
|
+
return &MaxIterationsError{
|
|
30
|
+
MaxIterations: maxIterations,
|
|
31
|
+
RLMError: &RLMError{
|
|
32
|
+
Message: fmt.Sprintf("max iterations (%d) exceeded without FINAL()", maxIterations),
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// MaxDepthError is returned when max recursion depth is exceeded
|
|
38
|
+
type MaxDepthError struct {
|
|
39
|
+
MaxDepth int
|
|
40
|
+
*RLMError
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func NewMaxDepthError(maxDepth int) *MaxDepthError {
|
|
44
|
+
return &MaxDepthError{
|
|
45
|
+
MaxDepth: maxDepth,
|
|
46
|
+
RLMError: &RLMError{
|
|
47
|
+
Message: fmt.Sprintf("max recursion depth (%d) exceeded", maxDepth),
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// REPLError is returned when REPL execution fails
|
|
53
|
+
type REPLError struct {
|
|
54
|
+
Code string
|
|
55
|
+
*RLMError
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func NewREPLError(message string, code string, cause error) *REPLError {
|
|
59
|
+
return &REPLError{
|
|
60
|
+
Code: code,
|
|
61
|
+
RLMError: &RLMError{
|
|
62
|
+
Message: message,
|
|
63
|
+
Cause: cause,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// APIError is returned when LLM API calls fail
|
|
69
|
+
type APIError struct {
|
|
70
|
+
StatusCode int
|
|
71
|
+
Response string
|
|
72
|
+
*RLMError
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func NewAPIError(statusCode int, response string) *APIError {
|
|
76
|
+
return &APIError{
|
|
77
|
+
StatusCode: statusCode,
|
|
78
|
+
Response: response,
|
|
79
|
+
RLMError: &RLMError{
|
|
80
|
+
Message: fmt.Sprintf("LLM request failed (%d): %s", statusCode, response),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
package rlm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"io"
|
|
9
|
+
"net/http"
|
|
10
|
+
"strings"
|
|
11
|
+
"time"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type Message struct {
|
|
15
|
+
Role string `json:"role"`
|
|
16
|
+
Content string `json:"content"`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ChatRequest struct {
|
|
20
|
+
Model string
|
|
21
|
+
Messages []Message
|
|
22
|
+
APIBase string
|
|
23
|
+
APIKey string
|
|
24
|
+
Timeout int
|
|
25
|
+
ExtraParams map[string]interface{}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type chatResponse struct {
|
|
29
|
+
Choices []struct {
|
|
30
|
+
Message struct {
|
|
31
|
+
Content string `json:"content"`
|
|
32
|
+
} `json:"message"`
|
|
33
|
+
} `json:"choices"`
|
|
34
|
+
Error *struct {
|
|
35
|
+
Message string `json:"message"`
|
|
36
|
+
} `json:"error"`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var (
|
|
40
|
+
// defaultHTTPClient is a shared HTTP client with connection pooling
|
|
41
|
+
defaultHTTPClient = &http.Client{
|
|
42
|
+
Timeout: 60 * time.Second,
|
|
43
|
+
Transport: &http.Transport{
|
|
44
|
+
MaxIdleConns: 100,
|
|
45
|
+
MaxIdleConnsPerHost: 10,
|
|
46
|
+
IdleConnTimeout: 90 * time.Second,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
func CallChatCompletion(request ChatRequest) (string, error) {
|
|
52
|
+
endpoint := buildEndpoint(request.APIBase)
|
|
53
|
+
payload := map[string]interface{}{
|
|
54
|
+
"model": request.Model,
|
|
55
|
+
"messages": request.Messages,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for key, value := range request.ExtraParams {
|
|
59
|
+
payload[key] = value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
body, err := json.Marshal(payload)
|
|
63
|
+
if err != nil {
|
|
64
|
+
return "", err
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Use shared client with connection pooling
|
|
68
|
+
client := defaultHTTPClient
|
|
69
|
+
if request.Timeout > 0 {
|
|
70
|
+
// Create custom client for non-default timeout
|
|
71
|
+
client = &http.Client{
|
|
72
|
+
Timeout: time.Duration(request.Timeout) * time.Second,
|
|
73
|
+
Transport: defaultHTTPClient.Transport,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
|
|
78
|
+
if err != nil {
|
|
79
|
+
return "", err
|
|
80
|
+
}
|
|
81
|
+
req.Header.Set("Content-Type", "application/json")
|
|
82
|
+
if request.APIKey != "" {
|
|
83
|
+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", request.APIKey))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resp, err := client.Do(req)
|
|
87
|
+
if err != nil {
|
|
88
|
+
return "", err
|
|
89
|
+
}
|
|
90
|
+
defer resp.Body.Close()
|
|
91
|
+
|
|
92
|
+
responseBody, err := io.ReadAll(resp.Body)
|
|
93
|
+
if err != nil {
|
|
94
|
+
return "", err
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if resp.StatusCode >= http.StatusBadRequest {
|
|
98
|
+
return "", NewAPIError(resp.StatusCode, strings.TrimSpace(string(responseBody)))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
var parsed chatResponse
|
|
102
|
+
if err := json.Unmarshal(responseBody, &parsed); err != nil {
|
|
103
|
+
return "", err
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if parsed.Error != nil && parsed.Error.Message != "" {
|
|
107
|
+
return "", errors.New(parsed.Error.Message)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if len(parsed.Choices) == 0 {
|
|
111
|
+
return "", errors.New("no choices returned by LLM")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return parsed.Choices[0].Message.Content, nil
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func buildEndpoint(apiBase string) string {
|
|
118
|
+
base := strings.TrimSpace(apiBase)
|
|
119
|
+
if base == "" {
|
|
120
|
+
base = "https://api.openai.com/v1"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if strings.Contains(base, "/chat/completions") {
|
|
124
|
+
return base
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return strings.TrimRight(base, "/") + "/chat/completions"
|
|
128
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package rlm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"regexp"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
var (
|
|
10
|
+
finalTripleDouble = regexp.MustCompile(`(?s)FINAL\s*\(\s*"""(.*)"""`)
|
|
11
|
+
finalTripleSingle = regexp.MustCompile(`(?s)FINAL\s*\(\s*'''(.*)'''`)
|
|
12
|
+
finalDouble = regexp.MustCompile(`(?s)FINAL\s*\(\s*"([^"]*)"`)
|
|
13
|
+
finalSingle = regexp.MustCompile(`(?s)FINAL\s*\(\s*'([^']*)'`)
|
|
14
|
+
finalVar = regexp.MustCompile(`FINAL_VAR\s*\(\s*(\w+)\s*\)`)
|
|
15
|
+
finalAny = regexp.MustCompile(`FINAL\(|FINAL_VAR\(`)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func IsFinal(response string) bool {
|
|
19
|
+
return finalAny.MatchString(response)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func ParseResponse(response string, env map[string]interface{}) (string, bool) {
|
|
23
|
+
answer, ok := extractFinal(response)
|
|
24
|
+
if ok {
|
|
25
|
+
return answer, true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return extractFinalVar(response, env)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func extractFinal(response string) (string, bool) {
|
|
32
|
+
matchers := []*regexp.Regexp{finalTripleDouble, finalTripleSingle, finalDouble, finalSingle}
|
|
33
|
+
for _, matcher := range matchers {
|
|
34
|
+
match := matcher.FindStringSubmatch(response)
|
|
35
|
+
if len(match) > 1 {
|
|
36
|
+
return strings.TrimSpace(match[1]), true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return "", false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func extractFinalVar(response string, env map[string]interface{}) (string, bool) {
|
|
43
|
+
match := finalVar.FindStringSubmatch(response)
|
|
44
|
+
if len(match) < 2 {
|
|
45
|
+
return "", false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
value, ok := env[match[1]]
|
|
49
|
+
if !ok {
|
|
50
|
+
return "", false
|
|
51
|
+
}
|
|
52
|
+
return fmt.Sprint(value), true
|
|
53
|
+
}
|
|
@@ -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
|
+
}
|