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.
@@ -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
- }
@@ -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
- }