entroplain 0.1.0
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/CONTRIBUTING.md +103 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/dist/entroplain-0.1.0-py3-none-any.whl +0 -0
- package/dist/entroplain-0.1.0.tar.gz +0 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/monitor.d.ts.map +1 -0
- package/dist/types.d.ts.map +1 -0
- package/docs/USAGE.md +302 -0
- package/entroplain/__init__.py +30 -0
- package/entroplain/cli.py +152 -0
- package/entroplain/hooks.py +183 -0
- package/entroplain/monitor.py +272 -0
- package/entroplain/providers.py +626 -0
- package/examples.md +40 -0
- package/package.json +44 -0
- package/pyproject.toml +85 -0
- package/src/hooks.ts +130 -0
- package/src/index.ts +9 -0
- package/src/monitor.ts +252 -0
- package/src/types.ts +58 -0
- package/tests/test_functional.py +303 -0
- package/tests/test_monitor.py +165 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Functional test for Entroplain - verify core functionality works.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
9
|
+
|
|
10
|
+
from entroplain import EntropyMonitor, calculate_entropy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_entropy_calculation():
|
|
14
|
+
"""Test Shannon entropy calculation."""
|
|
15
|
+
print("=" * 60)
|
|
16
|
+
print("TEST: Entropy Calculation")
|
|
17
|
+
print("=" * 60)
|
|
18
|
+
|
|
19
|
+
# Test 1: Uniform distribution (should be ~1.0 for 2 choices)
|
|
20
|
+
entropy = calculate_entropy([-0.693, -0.693]) # log(0.5) ≈ -0.693
|
|
21
|
+
print(f"Uniform distribution entropy: {entropy:.4f}")
|
|
22
|
+
assert 0.99 < entropy < 1.01, f"Expected ~1.0, got {entropy}"
|
|
23
|
+
print("✓ Uniform distribution test passed")
|
|
24
|
+
|
|
25
|
+
# Test 2: Deterministic (should be ~0)
|
|
26
|
+
entropy = calculate_entropy([0.0, -100]) # One certain, one impossible
|
|
27
|
+
print(f"Deterministic entropy: {entropy:.4f}")
|
|
28
|
+
assert entropy < 0.01, f"Expected ~0, got {entropy}"
|
|
29
|
+
print("✓ Deterministic test passed")
|
|
30
|
+
|
|
31
|
+
# Test 3: Empty input
|
|
32
|
+
entropy = calculate_entropy([])
|
|
33
|
+
print(f"Empty input entropy: {entropy}")
|
|
34
|
+
assert entropy == 0.0, f"Expected 0.0, got {entropy}"
|
|
35
|
+
print("✓ Empty input test passed")
|
|
36
|
+
|
|
37
|
+
print()
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_valley_detection():
|
|
42
|
+
"""Test valley (local minimum) detection."""
|
|
43
|
+
print("=" * 60)
|
|
44
|
+
print("TEST: Valley Detection")
|
|
45
|
+
print("=" * 60)
|
|
46
|
+
|
|
47
|
+
monitor = EntropyMonitor()
|
|
48
|
+
|
|
49
|
+
# Create trajectory: high -> low -> high (valley at index 1)
|
|
50
|
+
trajectory = [0.8, 0.3, 0.7]
|
|
51
|
+
for i, entropy in enumerate(trajectory):
|
|
52
|
+
point = monitor.track(f"token_{i}", entropy)
|
|
53
|
+
print(f" Token {i}: entropy={entropy}, is_valley={point.is_valley}")
|
|
54
|
+
|
|
55
|
+
valleys = monitor.get_valleys()
|
|
56
|
+
print(f"Valleys detected: {valleys}")
|
|
57
|
+
assert len(valleys) == 1, f"Expected 1 valley, got {len(valleys)}"
|
|
58
|
+
assert valleys[0] == (1, 0.3), f"Expected (1, 0.3), got {valleys[0]}"
|
|
59
|
+
print("✓ Valley detection test passed")
|
|
60
|
+
|
|
61
|
+
print()
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_velocity_calculation():
|
|
66
|
+
"""Test entropy velocity (rate of change)."""
|
|
67
|
+
print("=" * 60)
|
|
68
|
+
print("TEST: Velocity Calculation")
|
|
69
|
+
print("=" * 60)
|
|
70
|
+
|
|
71
|
+
monitor = EntropyMonitor()
|
|
72
|
+
|
|
73
|
+
monitor.track("A", 0.5)
|
|
74
|
+
velocity_initial = monitor.get_velocity()
|
|
75
|
+
print(f"Initial velocity: {velocity_initial}")
|
|
76
|
+
assert velocity_initial == 0.0, "Initial velocity should be 0"
|
|
77
|
+
|
|
78
|
+
monitor.track("B", 0.7)
|
|
79
|
+
velocity = monitor.get_velocity()
|
|
80
|
+
print(f"Velocity after second token: {velocity:.4f}")
|
|
81
|
+
assert abs(velocity - 0.2) < 0.01, f"Expected ~0.2, got {velocity}"
|
|
82
|
+
|
|
83
|
+
print("✓ Velocity calculation test passed")
|
|
84
|
+
|
|
85
|
+
print()
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_exit_conditions():
|
|
90
|
+
"""Test various exit conditions."""
|
|
91
|
+
print("=" * 60)
|
|
92
|
+
print("TEST: Exit Conditions")
|
|
93
|
+
print("=" * 60)
|
|
94
|
+
|
|
95
|
+
# Test min_tokens requirement
|
|
96
|
+
monitor = EntropyMonitor(min_tokens=10, min_valleys=0)
|
|
97
|
+
for i in range(5):
|
|
98
|
+
monitor.track(f"t_{i}", 0.1) # Low entropy
|
|
99
|
+
should_exit = monitor.should_exit()
|
|
100
|
+
print(f"min_tokens=10, 5 tokens: should_exit={should_exit}")
|
|
101
|
+
assert not should_exit, "Should not exit before min_tokens"
|
|
102
|
+
print("✓ min_tokens requirement works")
|
|
103
|
+
|
|
104
|
+
# Test min_valleys requirement
|
|
105
|
+
monitor = EntropyMonitor(min_tokens=0, min_valleys=3)
|
|
106
|
+
for entropy in [0.8, 0.3, 0.8, 0.4, 0.8]: # Only 2 valleys
|
|
107
|
+
monitor.track("t", entropy)
|
|
108
|
+
should_exit = monitor.should_exit()
|
|
109
|
+
print(f"min_valleys=3, 2 valleys: should_exit={should_exit}")
|
|
110
|
+
assert not should_exit, "Should not exit before min_valleys"
|
|
111
|
+
print("✓ min_valleys requirement works")
|
|
112
|
+
|
|
113
|
+
# Test entropy_drop condition
|
|
114
|
+
monitor = EntropyMonitor(
|
|
115
|
+
entropy_threshold=0.15,
|
|
116
|
+
min_valleys=0,
|
|
117
|
+
min_tokens=5,
|
|
118
|
+
exit_condition="entropy_drop"
|
|
119
|
+
)
|
|
120
|
+
for i in range(10):
|
|
121
|
+
monitor.track(f"t_{i}", 0.1) # Below threshold
|
|
122
|
+
should_exit = monitor.should_exit()
|
|
123
|
+
print(f"entropy_drop, entropy=0.1, threshold=0.15: should_exit={should_exit}")
|
|
124
|
+
assert should_exit, "Should exit when entropy below threshold"
|
|
125
|
+
print("✓ entropy_drop condition works")
|
|
126
|
+
|
|
127
|
+
print()
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_stats():
|
|
132
|
+
"""Test statistics output."""
|
|
133
|
+
print("=" * 60)
|
|
134
|
+
print("TEST: Statistics")
|
|
135
|
+
print("=" * 60)
|
|
136
|
+
|
|
137
|
+
monitor = EntropyMonitor()
|
|
138
|
+
|
|
139
|
+
trajectory = [0.5, 0.3, 0.7, 0.2, 0.6]
|
|
140
|
+
for i, entropy in enumerate(trajectory):
|
|
141
|
+
monitor.track(f"t_{i}", entropy)
|
|
142
|
+
|
|
143
|
+
stats = monitor.get_stats()
|
|
144
|
+
print(f"Statistics: {stats}")
|
|
145
|
+
|
|
146
|
+
assert stats["token_count"] == 5
|
|
147
|
+
assert stats["min_entropy"] == 0.2
|
|
148
|
+
assert stats["max_entropy"] == 0.7
|
|
149
|
+
assert abs(stats["mean_entropy"] - 0.46) < 0.01
|
|
150
|
+
|
|
151
|
+
print("✓ Statistics test passed")
|
|
152
|
+
|
|
153
|
+
print()
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_reset():
|
|
158
|
+
"""Test monitor reset."""
|
|
159
|
+
print("=" * 60)
|
|
160
|
+
print("TEST: Reset")
|
|
161
|
+
print("=" * 60)
|
|
162
|
+
|
|
163
|
+
monitor = EntropyMonitor()
|
|
164
|
+
|
|
165
|
+
for i in range(10):
|
|
166
|
+
monitor.track(f"t_{i}", 0.5)
|
|
167
|
+
|
|
168
|
+
assert len(monitor.get_trajectory()) == 10
|
|
169
|
+
print(f"Before reset: {len(monitor.get_trajectory())} tokens")
|
|
170
|
+
|
|
171
|
+
monitor.reset()
|
|
172
|
+
|
|
173
|
+
assert len(monitor.get_trajectory()) == 0
|
|
174
|
+
assert len(monitor.get_valleys()) == 0
|
|
175
|
+
print(f"After reset: {len(monitor.get_trajectory())} tokens")
|
|
176
|
+
print("✓ Reset test passed")
|
|
177
|
+
|
|
178
|
+
print()
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def run_security_audit():
|
|
183
|
+
"""Security audit for the codebase."""
|
|
184
|
+
print("=" * 60)
|
|
185
|
+
print("SECURITY AUDIT")
|
|
186
|
+
print("=" * 60)
|
|
187
|
+
|
|
188
|
+
issues = []
|
|
189
|
+
warnings = []
|
|
190
|
+
|
|
191
|
+
# Check 1: No hardcoded secrets
|
|
192
|
+
print("\n[CHECK] Hardcoded secrets...")
|
|
193
|
+
import entroplain.monitor as monitor_module
|
|
194
|
+
import entroplain.providers as providers_module
|
|
195
|
+
|
|
196
|
+
monitor_code = open(monitor_module.__file__).read()
|
|
197
|
+
providers_code = open(providers_module.__file__).read()
|
|
198
|
+
combined = monitor_code + providers_code
|
|
199
|
+
|
|
200
|
+
# Look for potential secrets
|
|
201
|
+
import re
|
|
202
|
+
api_key_patterns = [
|
|
203
|
+
r'sk-[a-zA-Z0-9]{20,}',
|
|
204
|
+
r'sk-ant-[a-zA-Z0-9]{20,}',
|
|
205
|
+
r'nvapi-[a-zA-Z0-9]{20,}',
|
|
206
|
+
r'AIza[a-zA-Z0-9_-]{35}',
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
for pattern in api_key_patterns:
|
|
210
|
+
matches = re.findall(pattern, combined)
|
|
211
|
+
if matches:
|
|
212
|
+
issues.append(f"Potential API key found: {matches[0][:10]}...")
|
|
213
|
+
|
|
214
|
+
if not issues:
|
|
215
|
+
print("✓ No hardcoded secrets found")
|
|
216
|
+
|
|
217
|
+
# Check 2: Environment variables for auth
|
|
218
|
+
print("\n[CHECK] Authentication handling...")
|
|
219
|
+
if 'os.environ.get' in providers_code:
|
|
220
|
+
print("✓ Uses environment variables for API keys")
|
|
221
|
+
else:
|
|
222
|
+
warnings.append("Consider using environment variables for API keys")
|
|
223
|
+
|
|
224
|
+
# Check 3: Input validation
|
|
225
|
+
print("\n[CHECK] Input validation...")
|
|
226
|
+
if 'if not logprobs' in monitor_code:
|
|
227
|
+
print("✓ Empty input validation present")
|
|
228
|
+
|
|
229
|
+
# Check 4: No eval/exec
|
|
230
|
+
print("\n[CHECK] Dangerous functions...")
|
|
231
|
+
dangerous = ['eval(', 'exec(', 'compile(', '__import__']
|
|
232
|
+
for d in dangerous:
|
|
233
|
+
if d in combined:
|
|
234
|
+
issues.append(f"Dangerous function found: {d}")
|
|
235
|
+
|
|
236
|
+
if not any(d in combined for d in dangerous):
|
|
237
|
+
print("✓ No dangerous eval/exec found")
|
|
238
|
+
|
|
239
|
+
# Check 5: No shell injection vectors
|
|
240
|
+
print("\n[CHECK] Shell injection vectors...")
|
|
241
|
+
if 'subprocess' in combined or 'os.system' in combined:
|
|
242
|
+
issues.append("Potential shell injection vector")
|
|
243
|
+
else:
|
|
244
|
+
print("✓ No shell injection vectors found")
|
|
245
|
+
|
|
246
|
+
# Check 6: Data sanitization
|
|
247
|
+
print("\n[CHECK] Data sanitization...")
|
|
248
|
+
if '1e-10' in monitor_code: # Prevents log(0)
|
|
249
|
+
print("✓ Math sanitization present (prevents log(0))")
|
|
250
|
+
|
|
251
|
+
print("\n" + "=" * 60)
|
|
252
|
+
print("SECURITY SUMMARY")
|
|
253
|
+
print("=" * 60)
|
|
254
|
+
|
|
255
|
+
if issues:
|
|
256
|
+
print(f"\n❌ ISSUES ({len(issues)}):")
|
|
257
|
+
for issue in issues:
|
|
258
|
+
print(f" - {issue}")
|
|
259
|
+
else:
|
|
260
|
+
print("\n✅ No security issues found")
|
|
261
|
+
|
|
262
|
+
if warnings:
|
|
263
|
+
print(f"\n⚠️ WARNINGS ({len(warnings)}):")
|
|
264
|
+
for warning in warnings:
|
|
265
|
+
print(f" - {warning}")
|
|
266
|
+
|
|
267
|
+
return len(issues) == 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main():
|
|
271
|
+
"""Run all tests."""
|
|
272
|
+
print("\n" + "=" * 60)
|
|
273
|
+
print("ENTROPPLAIN FUNCTIONAL TESTS")
|
|
274
|
+
print("=" * 60 + "\n")
|
|
275
|
+
|
|
276
|
+
all_passed = True
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
all_passed &= test_entropy_calculation()
|
|
280
|
+
all_passed &= test_valley_detection()
|
|
281
|
+
all_passed &= test_velocity_calculation()
|
|
282
|
+
all_passed &= test_exit_conditions()
|
|
283
|
+
all_passed &= test_stats()
|
|
284
|
+
all_passed &= test_reset()
|
|
285
|
+
all_passed &= run_security_audit()
|
|
286
|
+
except Exception as e:
|
|
287
|
+
print(f"\n❌ TEST FAILED: {e}")
|
|
288
|
+
import traceback
|
|
289
|
+
traceback.print_exc()
|
|
290
|
+
return 1
|
|
291
|
+
|
|
292
|
+
print("\n" + "=" * 60)
|
|
293
|
+
if all_passed:
|
|
294
|
+
print("✅ ALL TESTS PASSED")
|
|
295
|
+
else:
|
|
296
|
+
print("❌ SOME TESTS FAILED")
|
|
297
|
+
print("=" * 60 + "\n")
|
|
298
|
+
|
|
299
|
+
return 0 if all_passed else 1
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
sys.exit(main())
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Tests for Entroplain entropy monitor."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from entroplain import EntropyMonitor, calculate_entropy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestEntropyCalculation:
|
|
8
|
+
"""Tests for entropy calculation."""
|
|
9
|
+
|
|
10
|
+
def test_calculate_entropy_uniform(self):
|
|
11
|
+
"""Uniform distribution should have maximum entropy."""
|
|
12
|
+
# log(0.5) ≈ -0.693
|
|
13
|
+
entropy = calculate_entropy([-0.693, -0.693], from_probs=False)
|
|
14
|
+
assert 0.99 < entropy < 1.01 # Should be ~1.0 for 50/50
|
|
15
|
+
|
|
16
|
+
def test_calculate_entropy_deterministic(self):
|
|
17
|
+
"""Deterministic distribution should have zero entropy."""
|
|
18
|
+
entropy = calculate_entropy([0.0, -100], from_probs=False)
|
|
19
|
+
assert entropy < 0.01 # Should be ~0
|
|
20
|
+
|
|
21
|
+
def test_calculate_entropy_from_probs(self):
|
|
22
|
+
"""Should work with probabilities directly."""
|
|
23
|
+
entropy = calculate_entropy([0.5, 0.5], from_probs=True)
|
|
24
|
+
assert 0.99 < entropy < 1.01
|
|
25
|
+
|
|
26
|
+
def test_calculate_entropy_empty(self):
|
|
27
|
+
"""Empty input should return zero."""
|
|
28
|
+
entropy = calculate_entropy([])
|
|
29
|
+
assert entropy == 0.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestEntropyMonitor:
|
|
33
|
+
"""Tests for the EntropyMonitor class."""
|
|
34
|
+
|
|
35
|
+
def test_track_token(self):
|
|
36
|
+
"""Should track tokens and return entropy points."""
|
|
37
|
+
monitor = EntropyMonitor()
|
|
38
|
+
|
|
39
|
+
point = monitor.track("Hello", 0.5)
|
|
40
|
+
|
|
41
|
+
assert point.token == "Hello"
|
|
42
|
+
assert point.entropy == 0.5
|
|
43
|
+
assert point.index == 0
|
|
44
|
+
assert point.is_valley == False # First token can't be valley
|
|
45
|
+
|
|
46
|
+
def test_valley_detection(self):
|
|
47
|
+
"""Should detect local minima (valleys)."""
|
|
48
|
+
monitor = EntropyMonitor()
|
|
49
|
+
|
|
50
|
+
# Create trajectory: high -> low -> high (valley in middle)
|
|
51
|
+
monitor.track("A", 0.8)
|
|
52
|
+
monitor.track("B", 0.3) # Valley
|
|
53
|
+
monitor.track("C", 0.7)
|
|
54
|
+
|
|
55
|
+
valleys = monitor.get_valleys()
|
|
56
|
+
assert len(valleys) == 1
|
|
57
|
+
assert valleys[0][0] == 1 # Index of "B"
|
|
58
|
+
assert valleys[0][1] == 0.3
|
|
59
|
+
|
|
60
|
+
def test_velocity_calculation(self):
|
|
61
|
+
"""Should calculate entropy velocity (rate of change)."""
|
|
62
|
+
monitor = EntropyMonitor()
|
|
63
|
+
|
|
64
|
+
monitor.track("A", 0.5)
|
|
65
|
+
monitor.track("B", 0.7)
|
|
66
|
+
|
|
67
|
+
velocity = monitor.get_velocity()
|
|
68
|
+
assert abs(velocity - 0.2) < 0.01
|
|
69
|
+
|
|
70
|
+
def test_should_exit_respects_min_tokens(self):
|
|
71
|
+
"""Should not exit before min_tokens."""
|
|
72
|
+
monitor = EntropyMonitor(min_tokens=10)
|
|
73
|
+
|
|
74
|
+
for i in range(5):
|
|
75
|
+
monitor.track(f"token_{i}", 0.1) # Low entropy
|
|
76
|
+
|
|
77
|
+
assert not monitor.should_exit() # Too few tokens
|
|
78
|
+
|
|
79
|
+
def test_should_exit_respects_min_valleys(self):
|
|
80
|
+
"""Should not exit before min_valleys."""
|
|
81
|
+
monitor = EntropyMonitor(min_valleys=3, min_tokens=0)
|
|
82
|
+
|
|
83
|
+
# Create trajectory with only 2 valleys
|
|
84
|
+
for entropy in [0.8, 0.3, 0.8, 0.4, 0.8]:
|
|
85
|
+
monitor.track("t", entropy)
|
|
86
|
+
|
|
87
|
+
assert len(monitor.get_valleys()) == 2
|
|
88
|
+
assert not monitor.should_exit() # Not enough valleys
|
|
89
|
+
|
|
90
|
+
def test_get_stats(self):
|
|
91
|
+
"""Should return correct statistics."""
|
|
92
|
+
monitor = EntropyMonitor()
|
|
93
|
+
|
|
94
|
+
for entropy in [0.5, 0.3, 0.7, 0.2, 0.6]:
|
|
95
|
+
monitor.track("t", entropy)
|
|
96
|
+
|
|
97
|
+
stats = monitor.get_stats()
|
|
98
|
+
|
|
99
|
+
assert stats["token_count"] == 5
|
|
100
|
+
assert stats["min_entropy"] == 0.2
|
|
101
|
+
assert stats["max_entropy"] == 0.7
|
|
102
|
+
assert abs(stats["mean_entropy"] - 0.46) < 0.01
|
|
103
|
+
|
|
104
|
+
def test_reset(self):
|
|
105
|
+
"""Should clear all tracked data."""
|
|
106
|
+
monitor = EntropyMonitor()
|
|
107
|
+
|
|
108
|
+
for i in range(10):
|
|
109
|
+
monitor.track("t", 0.5)
|
|
110
|
+
|
|
111
|
+
monitor.reset()
|
|
112
|
+
|
|
113
|
+
assert len(monitor.get_trajectory()) == 0
|
|
114
|
+
assert len(monitor.get_valleys()) == 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TestExitConditions:
|
|
118
|
+
"""Tests for different exit conditions."""
|
|
119
|
+
|
|
120
|
+
def test_entropy_drop_exit(self):
|
|
121
|
+
"""Should exit when entropy drops below threshold."""
|
|
122
|
+
monitor = EntropyMonitor(
|
|
123
|
+
entropy_threshold=0.15,
|
|
124
|
+
min_valleys=0,
|
|
125
|
+
min_tokens=5,
|
|
126
|
+
exit_condition="entropy_drop"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for i in range(5):
|
|
130
|
+
monitor.track("t", 0.1) # Below threshold
|
|
131
|
+
|
|
132
|
+
assert monitor.should_exit()
|
|
133
|
+
|
|
134
|
+
def test_velocity_zero_exit(self):
|
|
135
|
+
"""Should exit when velocity stabilizes."""
|
|
136
|
+
monitor = EntropyMonitor(
|
|
137
|
+
velocity_threshold=0.01,
|
|
138
|
+
min_tokens=5,
|
|
139
|
+
exit_condition="velocity_zero"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Start varying, then stabilize
|
|
143
|
+
for entropy in [0.5, 0.3, 0.4, 0.39, 0.395, 0.392]:
|
|
144
|
+
monitor.track("t", entropy)
|
|
145
|
+
|
|
146
|
+
# Should exit because velocity is low
|
|
147
|
+
assert monitor.should_exit()
|
|
148
|
+
|
|
149
|
+
def test_combined_exit(self):
|
|
150
|
+
"""Combined condition should work."""
|
|
151
|
+
monitor = EntropyMonitor(
|
|
152
|
+
entropy_threshold=0.2,
|
|
153
|
+
velocity_threshold=0.05,
|
|
154
|
+
min_tokens=10,
|
|
155
|
+
min_valleys=2,
|
|
156
|
+
exit_condition="combined"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Create trajectory with valleys and low entropy
|
|
160
|
+
trajectory = [0.8, 0.3, 0.8, 0.3, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15]
|
|
161
|
+
for e in trajectory:
|
|
162
|
+
monitor.track("t", e)
|
|
163
|
+
|
|
164
|
+
# Should exit: entropy is low and velocity is low
|
|
165
|
+
assert monitor.should_exit()
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
19
|
+
}
|