recursive-llm-ts 5.0.1 → 5.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/package.json +6 -3
- package/bin/rlm-go +0 -0
- package/go/README.md +0 -426
- package/go/integration_test.sh +0 -169
- package/go/rlm/benchmark_test.go +0 -168
- package/go/rlm/context_overflow_test.go +0 -1271
- package/go/rlm/context_savings_test.go +0 -387
- package/go/rlm/lcm_episodes_test.go +0 -384
- package/go/rlm/lcm_test.go +0 -1407
- package/go/rlm/meta_agent_test.go +0 -270
- package/go/rlm/observability_test.go +0 -252
- package/go/rlm/parser_test.go +0 -202
- package/go/rlm/repl_test.go +0 -291
- package/go/rlm/schema_test.go +0 -343
- package/go/rlm/store_backend_test.go +0 -428
- package/go/rlm/structured_test.go +0 -895
- package/go/rlm/textrank_test.go +0 -335
- package/go/rlm/tfidf_test.go +0 -272
- package/go/rlm/token_tracking_test.go +0 -859
- package/go/rlm/tokenizer_test.go +0 -305
- package/go/rlm.test +0 -0
- package/go/test_mock.sh +0 -90
- package/go/test_rlm.sh +0 -41
- package/go/test_simple.sh +0 -78
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
package rlm
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"strings"
|
|
5
|
-
"testing"
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
func TestMetaAgentCreation(t *testing.T) {
|
|
9
|
-
config := Config{
|
|
10
|
-
APIKey: "test-key",
|
|
11
|
-
MaxDepth: 5,
|
|
12
|
-
MaxIterations: 30,
|
|
13
|
-
}
|
|
14
|
-
engine := New("gpt-4o-mini", config)
|
|
15
|
-
|
|
16
|
-
maConfig := MetaAgentConfig{
|
|
17
|
-
Enabled: true,
|
|
18
|
-
Model: "gpt-4o",
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
obs := NewNoopObserver()
|
|
22
|
-
ma := NewMetaAgent(engine, maConfig, obs)
|
|
23
|
-
|
|
24
|
-
if ma == nil {
|
|
25
|
-
t.Fatal("expected non-nil meta agent")
|
|
26
|
-
}
|
|
27
|
-
if ma.config.Model != "gpt-4o" {
|
|
28
|
-
t.Errorf("expected model 'gpt-4o', got '%s'", ma.config.Model)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
func TestMetaAgentDefaultModel(t *testing.T) {
|
|
33
|
-
config := Config{
|
|
34
|
-
APIKey: "test-key",
|
|
35
|
-
MaxDepth: 5,
|
|
36
|
-
MaxIterations: 30,
|
|
37
|
-
}
|
|
38
|
-
engine := New("gpt-4o-mini", config)
|
|
39
|
-
|
|
40
|
-
maConfig := MetaAgentConfig{
|
|
41
|
-
Enabled: true,
|
|
42
|
-
// No model specified - should default to engine model
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
obs := NewNoopObserver()
|
|
46
|
-
ma := NewMetaAgent(engine, maConfig, obs)
|
|
47
|
-
|
|
48
|
-
if ma.config.Model != "gpt-4o-mini" {
|
|
49
|
-
t.Errorf("expected default model 'gpt-4o-mini', got '%s'", ma.config.Model)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
func TestMetaAgentNeedsOptimization(t *testing.T) {
|
|
54
|
-
config := Config{
|
|
55
|
-
APIKey: "test-key",
|
|
56
|
-
MaxDepth: 5,
|
|
57
|
-
MaxIterations: 30,
|
|
58
|
-
}
|
|
59
|
-
engine := New("gpt-4o-mini", config)
|
|
60
|
-
|
|
61
|
-
tests := []struct {
|
|
62
|
-
name string
|
|
63
|
-
maConfig MetaAgentConfig
|
|
64
|
-
query string
|
|
65
|
-
context string
|
|
66
|
-
shouldNeed bool
|
|
67
|
-
}{
|
|
68
|
-
{
|
|
69
|
-
name: "short vague query should need optimization",
|
|
70
|
-
maConfig: MetaAgentConfig{
|
|
71
|
-
Enabled: true,
|
|
72
|
-
},
|
|
73
|
-
query: "what?",
|
|
74
|
-
context: "some context",
|
|
75
|
-
shouldNeed: true,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: "specific query with length limit should not need optimization",
|
|
79
|
-
maConfig: MetaAgentConfig{
|
|
80
|
-
Enabled: true,
|
|
81
|
-
MaxOptimizeLen: 10000, // Non-zero enables specificity check
|
|
82
|
-
},
|
|
83
|
-
query: "Extract all the email addresses from the document and list them",
|
|
84
|
-
context: "some context",
|
|
85
|
-
shouldNeed: false,
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: "long context triggers optimization",
|
|
89
|
-
maConfig: MetaAgentConfig{
|
|
90
|
-
Enabled: true,
|
|
91
|
-
MaxOptimizeLen: 100,
|
|
92
|
-
},
|
|
93
|
-
query: "Find all errors and summarize the root causes from the log file",
|
|
94
|
-
context: string(make([]byte, 200)), // 200 bytes > 100 threshold
|
|
95
|
-
shouldNeed: true,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: "always optimize when MaxOptimizeLen is 0",
|
|
99
|
-
maConfig: MetaAgentConfig{
|
|
100
|
-
Enabled: true,
|
|
101
|
-
MaxOptimizeLen: 0,
|
|
102
|
-
},
|
|
103
|
-
query: "Extract the key takeaways and provide a comprehensive analysis of the conversation",
|
|
104
|
-
context: "short",
|
|
105
|
-
shouldNeed: true,
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for _, tt := range tests {
|
|
110
|
-
t.Run(tt.name, func(t *testing.T) {
|
|
111
|
-
obs := NewNoopObserver()
|
|
112
|
-
ma := NewMetaAgent(engine, tt.maConfig, obs)
|
|
113
|
-
|
|
114
|
-
result := ma.needsOptimization(tt.query, tt.context)
|
|
115
|
-
if result != tt.shouldNeed {
|
|
116
|
-
t.Errorf("needsOptimization = %v, want %v", result, tt.shouldNeed)
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
func TestMetaAgentBuildOptimizePrompt(t *testing.T) {
|
|
123
|
-
config := Config{
|
|
124
|
-
APIKey: "test-key",
|
|
125
|
-
MaxDepth: 5,
|
|
126
|
-
MaxIterations: 30,
|
|
127
|
-
}
|
|
128
|
-
engine := New("gpt-4o-mini", config)
|
|
129
|
-
|
|
130
|
-
obs := NewNoopObserver()
|
|
131
|
-
ma := NewMetaAgent(engine, MetaAgentConfig{Enabled: true}, obs)
|
|
132
|
-
|
|
133
|
-
prompt := ma.buildOptimizePrompt("what are the key points?", "some long context here")
|
|
134
|
-
|
|
135
|
-
if prompt == "" {
|
|
136
|
-
t.Error("expected non-empty prompt")
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Should contain the original query
|
|
140
|
-
if !strings.Contains(prompt, "what are the key points?") {
|
|
141
|
-
t.Error("prompt should contain the original query")
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
func TestTruncateStr(t *testing.T) {
|
|
146
|
-
tests := []struct {
|
|
147
|
-
input string
|
|
148
|
-
maxLen int
|
|
149
|
-
expected string
|
|
150
|
-
}{
|
|
151
|
-
{"hello", 10, "hello"},
|
|
152
|
-
{"hello world", 5, "hello..."},
|
|
153
|
-
{"", 5, ""},
|
|
154
|
-
{"abc", 3, "abc"},
|
|
155
|
-
{"abcd", 3, "abc..."},
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for _, tt := range tests {
|
|
159
|
-
result := truncateStr(tt.input, tt.maxLen)
|
|
160
|
-
if result != tt.expected {
|
|
161
|
-
t.Errorf("truncateStr(%q, %d) = %q, want %q", tt.input, tt.maxLen, result, tt.expected)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
func TestMetaAgentConfigParsing(t *testing.T) {
|
|
167
|
-
config := map[string]interface{}{
|
|
168
|
-
"api_key": "test-key",
|
|
169
|
-
"meta_agent": map[string]interface{}{
|
|
170
|
-
"enabled": true,
|
|
171
|
-
"model": "gpt-4o",
|
|
172
|
-
"max_optimize_len": 5000,
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
parsed := ConfigFromMap(config)
|
|
177
|
-
|
|
178
|
-
if parsed.MetaAgent == nil {
|
|
179
|
-
t.Fatal("expected non-nil MetaAgent config")
|
|
180
|
-
}
|
|
181
|
-
if !parsed.MetaAgent.Enabled {
|
|
182
|
-
t.Error("expected meta_agent.enabled to be true")
|
|
183
|
-
}
|
|
184
|
-
if parsed.MetaAgent.Model != "gpt-4o" {
|
|
185
|
-
t.Errorf("expected model 'gpt-4o', got '%s'", parsed.MetaAgent.Model)
|
|
186
|
-
}
|
|
187
|
-
if parsed.MetaAgent.MaxOptimizeLen != 5000 {
|
|
188
|
-
t.Errorf("expected max_optimize_len 5000, got %d", parsed.MetaAgent.MaxOptimizeLen)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
func TestConfigWithObservability(t *testing.T) {
|
|
193
|
-
config := map[string]interface{}{
|
|
194
|
-
"api_key": "test-key",
|
|
195
|
-
"debug": true,
|
|
196
|
-
"observability": map[string]interface{}{
|
|
197
|
-
"trace_enabled": true,
|
|
198
|
-
"service_name": "test-rlm",
|
|
199
|
-
},
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
parsed := ConfigFromMap(config)
|
|
203
|
-
|
|
204
|
-
if parsed.Observability == nil {
|
|
205
|
-
t.Fatal("expected non-nil Observability config")
|
|
206
|
-
}
|
|
207
|
-
if !parsed.Observability.Debug {
|
|
208
|
-
t.Error("expected debug to be true")
|
|
209
|
-
}
|
|
210
|
-
if !parsed.Observability.TraceEnabled {
|
|
211
|
-
t.Error("expected trace_enabled to be true")
|
|
212
|
-
}
|
|
213
|
-
if parsed.Observability.ServiceName != "test-rlm" {
|
|
214
|
-
t.Errorf("expected service_name 'test-rlm', got '%s'", parsed.Observability.ServiceName)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
func TestEngineWithMetaAgent(t *testing.T) {
|
|
219
|
-
config := Config{
|
|
220
|
-
APIKey: "test-key",
|
|
221
|
-
MaxDepth: 5,
|
|
222
|
-
MaxIterations: 30,
|
|
223
|
-
MetaAgent: &MetaAgentConfig{
|
|
224
|
-
Enabled: true,
|
|
225
|
-
Model: "gpt-4o",
|
|
226
|
-
},
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
engine := New("gpt-4o-mini", config)
|
|
230
|
-
|
|
231
|
-
if engine.metaAgent == nil {
|
|
232
|
-
t.Error("expected meta agent to be initialized")
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
func TestEngineWithObservability(t *testing.T) {
|
|
237
|
-
config := Config{
|
|
238
|
-
APIKey: "test-key",
|
|
239
|
-
MaxDepth: 5,
|
|
240
|
-
MaxIterations: 30,
|
|
241
|
-
Observability: &ObservabilityConfig{
|
|
242
|
-
Debug: true,
|
|
243
|
-
},
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
engine := New("gpt-4o-mini", config)
|
|
247
|
-
defer engine.Shutdown()
|
|
248
|
-
|
|
249
|
-
obs := engine.GetObserver()
|
|
250
|
-
if obs == nil {
|
|
251
|
-
t.Fatal("expected non-nil observer")
|
|
252
|
-
}
|
|
253
|
-
if !obs.config.Debug {
|
|
254
|
-
t.Error("expected debug mode enabled")
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
func TestEngineWithoutMetaAgent(t *testing.T) {
|
|
259
|
-
config := Config{
|
|
260
|
-
APIKey: "test-key",
|
|
261
|
-
MaxDepth: 5,
|
|
262
|
-
MaxIterations: 30,
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
engine := New("gpt-4o-mini", config)
|
|
266
|
-
|
|
267
|
-
if engine.metaAgent != nil {
|
|
268
|
-
t.Error("expected meta agent to be nil when not configured")
|
|
269
|
-
}
|
|
270
|
-
}
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
package rlm
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"strings"
|
|
5
|
-
"testing"
|
|
6
|
-
"time"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
func TestNewObserver(t *testing.T) {
|
|
10
|
-
t.Run("with debug enabled", func(t *testing.T) {
|
|
11
|
-
obs := NewObserver(ObservabilityConfig{Debug: true})
|
|
12
|
-
if obs == nil {
|
|
13
|
-
t.Fatal("expected non-nil observer")
|
|
14
|
-
}
|
|
15
|
-
if !obs.config.Debug {
|
|
16
|
-
t.Error("expected debug to be enabled")
|
|
17
|
-
}
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
t.Run("with tracing enabled", func(t *testing.T) {
|
|
21
|
-
obs := NewObserver(ObservabilityConfig{
|
|
22
|
-
TraceEnabled: true,
|
|
23
|
-
ServiceName: "test-rlm",
|
|
24
|
-
})
|
|
25
|
-
if obs == nil {
|
|
26
|
-
t.Fatal("expected non-nil observer")
|
|
27
|
-
}
|
|
28
|
-
if obs.tracer == nil {
|
|
29
|
-
t.Error("expected tracer to be initialized")
|
|
30
|
-
}
|
|
31
|
-
obs.Shutdown()
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func TestNewNoopObserver(t *testing.T) {
|
|
36
|
-
obs := NewNoopObserver()
|
|
37
|
-
if obs == nil {
|
|
38
|
-
t.Fatal("expected non-nil observer")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Should not panic with any operations
|
|
42
|
-
ctx := obs.StartTrace("test", nil)
|
|
43
|
-
obs.EndTrace(ctx)
|
|
44
|
-
obs.Debug("test", "message %s", "arg")
|
|
45
|
-
obs.Error("test", "error %s", "arg")
|
|
46
|
-
obs.Event("test", map[string]string{"key": "value"})
|
|
47
|
-
obs.LLMCall("model", 1, 0, time.Second, nil)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
func TestObserverEvents(t *testing.T) {
|
|
51
|
-
obs := NewObserver(ObservabilityConfig{Debug: true})
|
|
52
|
-
|
|
53
|
-
obs.Event("test.event1", map[string]string{"key": "value1"})
|
|
54
|
-
obs.Event("test.event2", map[string]string{"key": "value2"})
|
|
55
|
-
|
|
56
|
-
events := obs.GetEvents()
|
|
57
|
-
if len(events) != 2 {
|
|
58
|
-
t.Errorf("expected 2 events, got %d", len(events))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if events[0].Name != "test.event1" {
|
|
62
|
-
t.Errorf("expected first event name 'test.event1', got '%s'", events[0].Name)
|
|
63
|
-
}
|
|
64
|
-
if events[1].Name != "test.event2" {
|
|
65
|
-
t.Errorf("expected second event name 'test.event2', got '%s'", events[1].Name)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
func TestObserverEventsJSON(t *testing.T) {
|
|
70
|
-
obs := NewObserver(ObservabilityConfig{Debug: true})
|
|
71
|
-
|
|
72
|
-
obs.Event("test.event", map[string]string{"key": "value"})
|
|
73
|
-
|
|
74
|
-
jsonStr, err := obs.GetEventsJSON()
|
|
75
|
-
if err != nil {
|
|
76
|
-
t.Fatalf("unexpected error: %v", err)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if !strings.Contains(jsonStr, "test.event") {
|
|
80
|
-
t.Error("expected JSON to contain event name")
|
|
81
|
-
}
|
|
82
|
-
if !strings.Contains(jsonStr, `"key"`) {
|
|
83
|
-
t.Error("expected JSON to contain attribute key")
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
func TestObserverLLMCall(t *testing.T) {
|
|
88
|
-
obs := NewObserver(ObservabilityConfig{Debug: true})
|
|
89
|
-
|
|
90
|
-
obs.LLMCall("gpt-4o-mini", 3, 150, 2*time.Second, nil)
|
|
91
|
-
|
|
92
|
-
events := obs.GetEvents()
|
|
93
|
-
if len(events) != 1 {
|
|
94
|
-
t.Fatalf("expected 1 event, got %d", len(events))
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
event := events[0]
|
|
98
|
-
if event.Type != "llm_call" {
|
|
99
|
-
t.Errorf("expected type 'llm_call', got '%s'", event.Type)
|
|
100
|
-
}
|
|
101
|
-
if event.Attributes["model"] != "gpt-4o-mini" {
|
|
102
|
-
t.Errorf("expected model 'gpt-4o-mini', got '%s'", event.Attributes["model"])
|
|
103
|
-
}
|
|
104
|
-
if event.Attributes["message_count"] != "3" {
|
|
105
|
-
t.Errorf("expected message_count '3', got '%s'", event.Attributes["message_count"])
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
func TestObserverOnEventCallback(t *testing.T) {
|
|
110
|
-
var receivedEvents []ObservabilityEvent
|
|
111
|
-
|
|
112
|
-
obs := NewObserver(ObservabilityConfig{
|
|
113
|
-
Debug: true,
|
|
114
|
-
OnEvent: func(event ObservabilityEvent) {
|
|
115
|
-
receivedEvents = append(receivedEvents, event)
|
|
116
|
-
},
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
obs.Event("callback.test", map[string]string{"data": "test"})
|
|
120
|
-
|
|
121
|
-
if len(receivedEvents) != 1 {
|
|
122
|
-
t.Fatalf("expected 1 callback event, got %d", len(receivedEvents))
|
|
123
|
-
}
|
|
124
|
-
if receivedEvents[0].Name != "callback.test" {
|
|
125
|
-
t.Errorf("expected event name 'callback.test', got '%s'", receivedEvents[0].Name)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
func TestObserverSpans(t *testing.T) {
|
|
130
|
-
obs := NewObserver(ObservabilityConfig{
|
|
131
|
-
TraceEnabled: true,
|
|
132
|
-
ServiceName: "test",
|
|
133
|
-
})
|
|
134
|
-
defer obs.Shutdown()
|
|
135
|
-
|
|
136
|
-
traceCtx := obs.StartTrace("root", map[string]string{"op": "test"})
|
|
137
|
-
spanCtx := obs.StartSpan("child", map[string]string{"step": "1"})
|
|
138
|
-
obs.EndSpan(spanCtx)
|
|
139
|
-
obs.EndTrace(traceCtx)
|
|
140
|
-
|
|
141
|
-
events := obs.GetEvents()
|
|
142
|
-
// Should have at least trace_start and span_start events
|
|
143
|
-
if len(events) < 2 {
|
|
144
|
-
t.Errorf("expected at least 2 events, got %d", len(events))
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
func TestObservabilityConfigFromMap(t *testing.T) {
|
|
149
|
-
config := map[string]interface{}{
|
|
150
|
-
"debug": true,
|
|
151
|
-
"trace_enabled": true,
|
|
152
|
-
"service_name": "my-service",
|
|
153
|
-
"log_output": "stderr",
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
obs := ObservabilityConfigFromMap(config)
|
|
157
|
-
|
|
158
|
-
if !obs.Debug {
|
|
159
|
-
t.Error("expected debug to be true")
|
|
160
|
-
}
|
|
161
|
-
if !obs.TraceEnabled {
|
|
162
|
-
t.Error("expected trace_enabled to be true")
|
|
163
|
-
}
|
|
164
|
-
if obs.ServiceName != "my-service" {
|
|
165
|
-
t.Errorf("expected service_name 'my-service', got '%s'", obs.ServiceName)
|
|
166
|
-
}
|
|
167
|
-
if obs.LogOutput != "stderr" {
|
|
168
|
-
t.Errorf("expected log_output 'stderr', got '%s'", obs.LogOutput)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
func TestObservabilityConfigFromMap_Nil(t *testing.T) {
|
|
173
|
-
obs := ObservabilityConfigFromMap(nil)
|
|
174
|
-
if obs.Debug || obs.TraceEnabled {
|
|
175
|
-
t.Error("expected all defaults for nil config")
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
func TestExtractObservabilityConfig(t *testing.T) {
|
|
180
|
-
fullConfig := map[string]interface{}{
|
|
181
|
-
"debug": true,
|
|
182
|
-
"model": "gpt-4o",
|
|
183
|
-
"api_key": "key",
|
|
184
|
-
"service_name": "rlm-test",
|
|
185
|
-
"observability": map[string]interface{}{
|
|
186
|
-
"langfuse_enabled": true,
|
|
187
|
-
},
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
obsConfig := ExtractObservabilityConfig(fullConfig)
|
|
191
|
-
|
|
192
|
-
if v, ok := obsConfig["debug"].(bool); !ok || !v {
|
|
193
|
-
t.Error("expected debug to be extracted")
|
|
194
|
-
}
|
|
195
|
-
if v, ok := obsConfig["service_name"].(string); !ok || v != "rlm-test" {
|
|
196
|
-
t.Error("expected service_name to be extracted")
|
|
197
|
-
}
|
|
198
|
-
if v, ok := obsConfig["langfuse_enabled"].(bool); !ok || !v {
|
|
199
|
-
t.Error("expected langfuse_enabled from nested observability config")
|
|
200
|
-
}
|
|
201
|
-
if _, ok := obsConfig["model"]; ok {
|
|
202
|
-
t.Error("model should not be in observability config")
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
func TestRedactSensitive(t *testing.T) {
|
|
207
|
-
attrs := map[string]string{
|
|
208
|
-
"model": "gpt-4o",
|
|
209
|
-
"api_key": "sk-12345",
|
|
210
|
-
"secret": "my-secret",
|
|
211
|
-
"query": "hello world",
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
redacted := RedactSensitive(attrs)
|
|
215
|
-
|
|
216
|
-
if redacted["model"] != "gpt-4o" {
|
|
217
|
-
t.Error("model should not be redacted")
|
|
218
|
-
}
|
|
219
|
-
if redacted["api_key"] != "[REDACTED]" {
|
|
220
|
-
t.Error("api_key should be redacted")
|
|
221
|
-
}
|
|
222
|
-
if redacted["secret"] != "[REDACTED]" {
|
|
223
|
-
t.Error("secret should be redacted")
|
|
224
|
-
}
|
|
225
|
-
if redacted["query"] != "hello world" {
|
|
226
|
-
t.Error("query should not be redacted")
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
func TestFormatStatsWithObservability(t *testing.T) {
|
|
231
|
-
stats := RLMStats{
|
|
232
|
-
LlmCalls: 5,
|
|
233
|
-
Iterations: 3,
|
|
234
|
-
Depth: 1,
|
|
235
|
-
ParsingRetries: 2,
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
obs := NewObserver(ObservabilityConfig{Debug: true})
|
|
239
|
-
obs.Event("test", map[string]string{"data": "value"})
|
|
240
|
-
|
|
241
|
-
result := FormatStatsWithObservability(stats, obs)
|
|
242
|
-
|
|
243
|
-
if result["llm_calls"] != 5 {
|
|
244
|
-
t.Errorf("expected llm_calls 5, got %v", result["llm_calls"])
|
|
245
|
-
}
|
|
246
|
-
if result["parsing_retries"] != 2 {
|
|
247
|
-
t.Errorf("expected parsing_retries 2, got %v", result["parsing_retries"])
|
|
248
|
-
}
|
|
249
|
-
if _, ok := result["trace_events"]; !ok {
|
|
250
|
-
t.Error("expected trace_events in debug mode")
|
|
251
|
-
}
|
|
252
|
-
}
|
package/go/rlm/parser_test.go
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
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
|
-
}
|