Upload folder using huggingface_hub
Browse files- tests/__init__.py +1 -0
- tests/test_connectors_vision.py +157 -0
- tests/test_conversation_mesh.py +278 -0
- tests/test_fast_reply_identity.py +172 -0
- tests/test_harness.py +88 -0
- tests/test_memory_agents.py +181 -0
- tests/test_model.py +90 -0
- tests/test_quantization.py +94 -0
- tests/test_splitbit.py +77 -0
- tests/test_subscription.py +247 -0
- tests/test_tokenizer.py +72 -0
- tests/test_tools_vault.py +251 -0
- tests/test_voice.py +115 -0
tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Tests for SplitBit LLM."""
|
tests/test_connectors_vision.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test connectors, image generation, self-refinement, and always-on daemon."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 7 |
+
|
| 8 |
+
from splitbit_llm.connectors.api_client import RESTClient, APIConfig, ConnectorRegistry
|
| 9 |
+
from splitbit_llm.connectors.services import ServiceManager, HTTPFetcher
|
| 10 |
+
from splitbit_llm.connectors.webhooks import WebhookManager
|
| 11 |
+
from splitbit_llm.vision.image_gen import ImageGenerator
|
| 12 |
+
from splitbit_llm.agents.self_refine import SelfRefinementEngine
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_rest_client_config():
|
| 16 |
+
"""Test REST client configuration."""
|
| 17 |
+
config = APIConfig(name="test", base_url="https://api.example.com", api_key="secret")
|
| 18 |
+
client = RESTClient(config)
|
| 19 |
+
headers = client._build_headers()
|
| 20 |
+
assert "X-API-Key" in headers, f"API key header missing: {headers}"
|
| 21 |
+
assert headers["X-API-Key"] == "secret"
|
| 22 |
+
print(f" Headers: {list(headers.keys())}")
|
| 23 |
+
|
| 24 |
+
# Test bearer auth
|
| 25 |
+
config2 = APIConfig(name="test2", base_url="https://api.example.com", api_key="tok", auth_type="bearer")
|
| 26 |
+
client2 = RESTClient(config2)
|
| 27 |
+
headers2 = client2._build_headers()
|
| 28 |
+
assert headers2["Authorization"] == "Bearer tok"
|
| 29 |
+
print(f" Bearer auth: OK")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_connector_registry():
|
| 33 |
+
"""Test connector registry."""
|
| 34 |
+
registry = ConnectorRegistry()
|
| 35 |
+
config = APIConfig(name="github", base_url="https://api.github.com", auth_type="none")
|
| 36 |
+
registry.register("github", config)
|
| 37 |
+
assert "github" in registry._connectors
|
| 38 |
+
stats = registry.get_stats()
|
| 39 |
+
assert stats["total_connectors"] == 1
|
| 40 |
+
print(f" Connectors: {stats['total_connectors']}")
|
| 41 |
+
|
| 42 |
+
# Test calling unknown connector
|
| 43 |
+
result = registry.call("unknown", "GET", "/test")
|
| 44 |
+
assert not result.success
|
| 45 |
+
print(f" Unknown connector handled: OK")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_service_manager():
|
| 49 |
+
"""Test service manager."""
|
| 50 |
+
mgr = ServiceManager()
|
| 51 |
+
assert "http_fetcher" in mgr.list_services()
|
| 52 |
+
print(f" Services: {mgr.list_services()}")
|
| 53 |
+
|
| 54 |
+
# Test calling a service method
|
| 55 |
+
result = mgr.call_service("http_fetcher", "fetch", "http://httpbin.org/status/200")
|
| 56 |
+
print(f" HTTP fetch result type: {type(result).__name__}")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_webhooks():
|
| 60 |
+
"""Test webhook manager."""
|
| 61 |
+
mgr = WebhookManager()
|
| 62 |
+
|
| 63 |
+
# Register an endpoint (no secret for simple test)
|
| 64 |
+
def handler(body):
|
| 65 |
+
return {"received": body.get("event", "unknown")}
|
| 66 |
+
|
| 67 |
+
mgr.register_endpoint("/test", handler)
|
| 68 |
+
assert "/test" in mgr.list_endpoints()
|
| 69 |
+
print(f" Endpoints: {mgr.list_endpoints()}")
|
| 70 |
+
|
| 71 |
+
# Handle a request
|
| 72 |
+
result = mgr.handle_request("/test", {"event": "test_event"})
|
| 73 |
+
assert result["received"] == "test_event"
|
| 74 |
+
print(f" Webhook handled: {result}")
|
| 75 |
+
|
| 76 |
+
# Test unknown endpoint
|
| 77 |
+
result = mgr.handle_request("/unknown", {})
|
| 78 |
+
assert "error" in result
|
| 79 |
+
print(f" Unknown endpoint: OK")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_image_generator():
|
| 83 |
+
"""Test image generation."""
|
| 84 |
+
gen = ImageGenerator(default_size=(64, 64))
|
| 85 |
+
|
| 86 |
+
# Test BMP generation
|
| 87 |
+
result = gen.generate("sunset over the ocean", width=64, height=64)
|
| 88 |
+
assert result["width"] == 64
|
| 89 |
+
assert result["height"] == 64
|
| 90 |
+
assert len(result["base64"]) > 0
|
| 91 |
+
assert "sunset" in result["palette"][0].__str__() or True # palette extracted
|
| 92 |
+
print(f" Image: {result['width']}x{result['height']}, pattern={result['pattern']}, size={result['size_bytes']}b")
|
| 93 |
+
|
| 94 |
+
# Test SVG generation
|
| 95 |
+
svg = gen.generate_svg("geometric shapes", width=100, height=100)
|
| 96 |
+
assert "<svg" in svg
|
| 97 |
+
assert "</svg>" in svg
|
| 98 |
+
print(f" SVG: {len(svg)} chars")
|
| 99 |
+
|
| 100 |
+
# Test ASCII art
|
| 101 |
+
ascii_art = gen.generate_ascii("noise texture", width=30, height=10)
|
| 102 |
+
assert len(ascii_art) > 0
|
| 103 |
+
lines = ascii_art.strip().split("\n")
|
| 104 |
+
assert len(lines) == 10
|
| 105 |
+
print(f" ASCII: {len(lines)} lines")
|
| 106 |
+
|
| 107 |
+
# Test palette extraction
|
| 108 |
+
palette = gen._extract_palette("fire and flames")
|
| 109 |
+
assert palette[0][0] > 200 # red dominant
|
| 110 |
+
print(f" Fire palette: {palette[0]}")
|
| 111 |
+
|
| 112 |
+
stats = gen.get_stats()
|
| 113 |
+
assert stats["images_generated"] >= 1
|
| 114 |
+
print(f" Stats: {stats['images_generated']} images generated")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def test_self_refinement():
|
| 118 |
+
"""Test self-refinement engine."""
|
| 119 |
+
engine = SelfRefinementEngine(harness=None)
|
| 120 |
+
stats = engine.get_stats()
|
| 121 |
+
assert stats["refinement_cycles"] == 0
|
| 122 |
+
print(f" Initial stats: {stats}")
|
| 123 |
+
|
| 124 |
+
# Test refine_once without harness (should return error gracefully)
|
| 125 |
+
result = engine.refine_once()
|
| 126 |
+
assert "error" in result
|
| 127 |
+
print(f" No harness handled: OK")
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def test_image_patterns():
|
| 131 |
+
"""Test all image pattern types."""
|
| 132 |
+
gen = ImageGenerator(default_size=(32, 32))
|
| 133 |
+
|
| 134 |
+
for pattern in ["gradient", "radial", "noise", "fractal", "geometric", "waves"]:
|
| 135 |
+
result = gen.generate("test", width=32, height=32, pattern=pattern)
|
| 136 |
+
assert result["pattern"] == pattern
|
| 137 |
+
assert len(result["base64"]) > 0
|
| 138 |
+
print(f" Pattern '{pattern}': OK ({result['size_bytes']}b)")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
print("Running connectors, image gen, and refinement tests...")
|
| 143 |
+
test_rest_client_config()
|
| 144 |
+
print(" ✓ test_rest_client_config")
|
| 145 |
+
test_connector_registry()
|
| 146 |
+
print(" ✓ test_connector_registry")
|
| 147 |
+
test_service_manager()
|
| 148 |
+
print(" ✓ test_service_manager")
|
| 149 |
+
test_webhooks()
|
| 150 |
+
print(" ✓ test_webhooks")
|
| 151 |
+
test_image_generator()
|
| 152 |
+
print(" ✓ test_image_generator")
|
| 153 |
+
test_self_refinement()
|
| 154 |
+
print(" ✓ test_self_refinement")
|
| 155 |
+
test_image_patterns()
|
| 156 |
+
print(" ✓ test_image_patterns")
|
| 157 |
+
print("\nAll connectors & vision tests passed!")
|
tests/test_conversation_mesh.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test multi-LLM conversation mesh, skill building pools, and auto category adder."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 7 |
+
|
| 8 |
+
from splitbit_llm.agents.conversation_mesh import (
|
| 9 |
+
ConversationMesh, AutoCategoryManager, SkillPool, ConversationMessage,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_auto_category_manager():
|
| 14 |
+
"""Test auto skill category adder — discovers new categories dynamically."""
|
| 15 |
+
mgr = AutoCategoryManager()
|
| 16 |
+
|
| 17 |
+
# Should start with seed categories
|
| 18 |
+
cats = mgr.get_categories()
|
| 19 |
+
assert "conversation" in cats
|
| 20 |
+
assert "code" in cats
|
| 21 |
+
assert "speed" in cats
|
| 22 |
+
print(f" Seed categories: {len(cats)} — {cats[:5]}...")
|
| 23 |
+
|
| 24 |
+
# Discover categories from a topic
|
| 25 |
+
discovered = mgr.discover_from_topic("How to improve database query optimization")
|
| 26 |
+
assert len(discovered) > 0
|
| 27 |
+
print(f" Discovered from 'database query optimization': {discovered}")
|
| 28 |
+
|
| 29 |
+
# Should have auto-added new categories
|
| 30 |
+
auto_cats = mgr.get_auto_categories()
|
| 31 |
+
assert len(auto_cats) > 0, f"Expected auto categories, got: {auto_cats}"
|
| 32 |
+
print(f" Auto-added categories: {auto_cats}")
|
| 33 |
+
|
| 34 |
+
# Discover from another topic
|
| 35 |
+
discovered2 = mgr.discover_from_topic("Building better network security protocols")
|
| 36 |
+
print(f" Discovered from 'network security protocols': {discovered2}")
|
| 37 |
+
|
| 38 |
+
# Stats
|
| 39 |
+
stats = mgr.get_stats()
|
| 40 |
+
assert stats["categories_total"] > len(mgr.SEED_CATEGORIES)
|
| 41 |
+
assert stats["categories_auto_added"] > 0
|
| 42 |
+
print(f" Stats: {stats['categories_total']} total, {stats['categories_auto_added']} auto-added")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_category_matching():
|
| 46 |
+
"""Test that similar keywords map to existing categories."""
|
| 47 |
+
mgr = AutoCategoryManager()
|
| 48 |
+
|
| 49 |
+
# First discovery creates the category
|
| 50 |
+
mgr.discover_from_topic("optimization techniques")
|
| 51 |
+
assert "optimization" in mgr.get_categories()
|
| 52 |
+
|
| 53 |
+
# Similar keyword should map to existing category
|
| 54 |
+
mgr.discover_from_topic("optimize performance")
|
| 55 |
+
# "optimize" should map to "optimization" via prefix matching
|
| 56 |
+
cats = mgr.get_categories()
|
| 57 |
+
print(f" Categories after 'optimize': {cats}")
|
| 58 |
+
|
| 59 |
+
stats = mgr.get_stats()
|
| 60 |
+
print(f" Keywords indexed: {stats['keywords_indexed']}")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def test_conversation_mesh():
|
| 64 |
+
"""Test multi-LLM conversation mesh — agents converse to build skills."""
|
| 65 |
+
mesh = ConversationMesh(harness=None)
|
| 66 |
+
|
| 67 |
+
# Run a round-robin conversation
|
| 68 |
+
result = mesh.run_conversation(mode="round_robin", topic="How to improve code quality")
|
| 69 |
+
assert result["mode"] == "round_robin"
|
| 70 |
+
assert result["topic"] == "How to improve code quality"
|
| 71 |
+
assert result["messages"] > 0
|
| 72 |
+
assert len(result["categories"]) > 0
|
| 73 |
+
print(f" Round-robin: {result['messages']} messages, categories: {result['categories']}")
|
| 74 |
+
|
| 75 |
+
# Run a brainstorm
|
| 76 |
+
result2 = mesh.run_conversation(mode="brainstorm", topic="Efficient algorithms for pattern matching")
|
| 77 |
+
assert result2["mode"] == "brainstorm"
|
| 78 |
+
assert result2["messages"] > 0
|
| 79 |
+
print(f" Brainstorm: {result2['messages']} messages, categories: {result2['categories']}")
|
| 80 |
+
|
| 81 |
+
# Run a debate
|
| 82 |
+
result3 = mesh.run_conversation(mode="debate", topic="Best approaches to data compression")
|
| 83 |
+
assert result3["mode"] == "debate"
|
| 84 |
+
print(f" Debate: {result3['messages']} messages")
|
| 85 |
+
|
| 86 |
+
# Run a teaching session
|
| 87 |
+
result4 = mesh.run_conversation(mode="teaching", topic="Methods for adaptive learning")
|
| 88 |
+
assert result4["mode"] == "teaching"
|
| 89 |
+
print(f" Teaching: {result4['messages']} messages")
|
| 90 |
+
|
| 91 |
+
# Run a pairwise discussion
|
| 92 |
+
result5 = mesh.run_conversation(mode="pairwise", topic="Strategies for error handling")
|
| 93 |
+
assert result5["mode"] == "pairwise"
|
| 94 |
+
print(f" Pairwise: {result5['messages']} messages")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_skill_building_pools():
|
| 98 |
+
"""Test skill building pools — collaborative skills from multiple agents."""
|
| 99 |
+
mesh = ConversationMesh(harness=None)
|
| 100 |
+
|
| 101 |
+
# Run multiple conversations to build pools
|
| 102 |
+
for i in range(5):
|
| 103 |
+
mesh.run_conversation(topic=f"Optimizing memory usage in system {i}")
|
| 104 |
+
|
| 105 |
+
pools = mesh.get_skill_pools()
|
| 106 |
+
assert len(pools) > 0, f"Expected skill pools, got {len(pools)}"
|
| 107 |
+
print(f" Skill pools created: {len(pools)}")
|
| 108 |
+
|
| 109 |
+
# Check pool structure
|
| 110 |
+
pool = pools[0]
|
| 111 |
+
assert "id" in pool
|
| 112 |
+
assert "name" in pool
|
| 113 |
+
assert "category" in pool
|
| 114 |
+
assert "contributors" in pool
|
| 115 |
+
assert "confidence" in pool
|
| 116 |
+
print(f" Pool: {pool['name']} (category: {pool['category']}, contributors: {pool['contributors']})")
|
| 117 |
+
|
| 118 |
+
# Stats
|
| 119 |
+
stats = mesh.get_stats()
|
| 120 |
+
assert stats["skills_pooled"] > 0
|
| 121 |
+
assert stats["cross_agent_skills"] > 0, "Expected cross-agent skills"
|
| 122 |
+
print(f" Stats: {stats['skills_pooled']} pooled, {stats['cross_agent_skills']} cross-agent")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def test_mesh_auto_categories():
|
| 126 |
+
"""Test that mesh conversations auto-discover new categories."""
|
| 127 |
+
mesh = ConversationMesh(harness=None)
|
| 128 |
+
|
| 129 |
+
# Run conversations on diverse topics
|
| 130 |
+
topics = [
|
| 131 |
+
"Improving database query performance",
|
| 132 |
+
"Building neural network architectures",
|
| 133 |
+
"Optimizing cache invalidation strategies",
|
| 134 |
+
"Enhancing cryptographic security measures",
|
| 135 |
+
"Implementing blockchain consensus algorithms",
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
all_cats = set()
|
| 139 |
+
for topic in topics:
|
| 140 |
+
result = mesh.run_conversation(topic=topic)
|
| 141 |
+
all_cats.update(result["categories"])
|
| 142 |
+
|
| 143 |
+
cats = mesh.get_categories()
|
| 144 |
+
auto_cats = mesh.get_auto_categories()
|
| 145 |
+
|
| 146 |
+
assert len(auto_cats) > 0, f"Expected auto categories: {auto_cats}"
|
| 147 |
+
print(f" Total categories: {len(cats)}")
|
| 148 |
+
print(f" Auto-discovered: {auto_cats}")
|
| 149 |
+
|
| 150 |
+
# Verify some expected categories were discovered
|
| 151 |
+
# (at least some of: database, neural, cache, cryptographic, blockchain)
|
| 152 |
+
discovered_lower = [c.lower() for c in auto_cats]
|
| 153 |
+
print(f" Discovered categories: {discovered_lower}")
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def test_mesh_stats():
|
| 157 |
+
"""Test mesh stats tracking."""
|
| 158 |
+
mesh = ConversationMesh(harness=None)
|
| 159 |
+
|
| 160 |
+
# Run a few conversations
|
| 161 |
+
mesh.run_conversation(topic="Testing mesh statistics tracking")
|
| 162 |
+
mesh.run_conversation(topic="Another topic for skill building")
|
| 163 |
+
|
| 164 |
+
stats = mesh.get_stats()
|
| 165 |
+
assert stats["conversations_total"] >= 2
|
| 166 |
+
assert stats["messages_exchanged"] > 0
|
| 167 |
+
cat_stats = stats["categories"]
|
| 168 |
+
assert cat_stats["categories_total"] > 0
|
| 169 |
+
print(f" Conversations: {stats['conversations_total']}")
|
| 170 |
+
print(f" Messages: {stats['messages_exchanged']}")
|
| 171 |
+
print(f" Skills pooled: {stats['skills_pooled']}")
|
| 172 |
+
print(f" Categories: {cat_stats['categories_total']}")
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def test_random_mode_selection():
|
| 176 |
+
"""Test that random mode selection works."""
|
| 177 |
+
mesh = ConversationMesh(harness=None)
|
| 178 |
+
|
| 179 |
+
modes_used = set()
|
| 180 |
+
for _ in range(10):
|
| 181 |
+
result = mesh.run_conversation() # no mode specified → random
|
| 182 |
+
modes_used.add(result["mode"])
|
| 183 |
+
|
| 184 |
+
# Should have used at least 2 different modes in 10 random runs
|
| 185 |
+
assert len(modes_used) >= 2, f"Expected variety of modes, got: {modes_used}"
|
| 186 |
+
print(f" Modes used: {modes_used}")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def test_skill_cascade():
|
| 190 |
+
"""Test that building a skill pool cascades into building related skill pools.
|
| 191 |
+
|
| 192 |
+
When a skill pool is created on topic X, the system should automatically
|
| 193 |
+
generate related skill pools on similar topics (advanced techniques,
|
| 194 |
+
best practices, pitfalls, testing, integration, etc.)
|
| 195 |
+
"""
|
| 196 |
+
mesh = ConversationMesh(harness=None)
|
| 197 |
+
|
| 198 |
+
# Run a single conversation — should cascade into multiple related ones
|
| 199 |
+
result = mesh.run_conversation(topic="Optimizing database query performance")
|
| 200 |
+
|
| 201 |
+
# The original conversation should have cascaded
|
| 202 |
+
cascaded = result.get("cascaded", [])
|
| 203 |
+
assert len(cascaded) > 0, f"Expected cascade results, got: {cascaded}"
|
| 204 |
+
print(f" Original topic: 'Optimizing database query performance'")
|
| 205 |
+
print(f" Cascaded into {len(cascaded)} related conversations:")
|
| 206 |
+
for c in cascaded:
|
| 207 |
+
print(f" → {c['topic'][:60]} (mode: {c['mode']}, pool: {c['pool_id'] is not None})")
|
| 208 |
+
|
| 209 |
+
# Should have created multiple skill pools (original + cascaded)
|
| 210 |
+
pools = mesh.get_skill_pools()
|
| 211 |
+
assert len(pools) > 1, f"Expected multiple pools from cascade, got {len(pools)}"
|
| 212 |
+
print(f" Total skill pools: {len(pools)}")
|
| 213 |
+
|
| 214 |
+
# Stats should show cascade activity
|
| 215 |
+
stats = mesh.get_stats()
|
| 216 |
+
assert stats["cascade_pools_created"] > 0, "Expected cascade pools created"
|
| 217 |
+
assert stats["cascade_conversations"] > 0, "Expected cascade conversations"
|
| 218 |
+
print(f" Cascade stats: {stats['cascade_pools_created']} pools, "
|
| 219 |
+
f"{stats['cascade_conversations']} conversations, depth {stats['cascade_depth']}")
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def test_cascade_related_topics():
|
| 223 |
+
"""Test that related topics are generated correctly from categories."""
|
| 224 |
+
mesh = ConversationMesh(harness=None)
|
| 225 |
+
|
| 226 |
+
# Generate related topics from a topic and categories
|
| 227 |
+
related = mesh._generate_related_topics(
|
| 228 |
+
"Optimizing database performance",
|
| 229 |
+
["database", "performance", "optimization"]
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
assert len(related) > 0, "Expected related topics"
|
| 233 |
+
assert len(related) <= 5, f"Should limit to 5, got {len(related)}"
|
| 234 |
+
print(f" Related topics for 'database, performance, optimization':")
|
| 235 |
+
for t in related:
|
| 236 |
+
print(f" → {t}")
|
| 237 |
+
|
| 238 |
+
# Should include category-based variations (first category always included)
|
| 239 |
+
assert any("database" in t.lower() for t in related)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def test_cascade_depth_limit():
|
| 243 |
+
"""Test that cascade depth is limited to prevent infinite recursion."""
|
| 244 |
+
mesh = ConversationMesh(harness=None)
|
| 245 |
+
|
| 246 |
+
# Run a conversation — cascade should be limited to depth 3
|
| 247 |
+
result = mesh.run_conversation(topic="Building neural network architectures")
|
| 248 |
+
|
| 249 |
+
stats = mesh.get_stats()
|
| 250 |
+
assert stats["cascade_depth"] <= 2, f"Cascade depth should be <= 2, got {stats['cascade_depth']}"
|
| 251 |
+
print(f" Cascade depth: {stats['cascade_depth']} (max 2)")
|
| 252 |
+
print(f" Total pools: {stats['skill_pools']}")
|
| 253 |
+
print(f" Total conversations: {stats['conversations_total']}")
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
print("Running conversation mesh, skill pools, and auto category tests...")
|
| 258 |
+
test_auto_category_manager()
|
| 259 |
+
print(" ✓ test_auto_category_manager")
|
| 260 |
+
test_category_matching()
|
| 261 |
+
print(" ✓ test_category_matching")
|
| 262 |
+
test_conversation_mesh()
|
| 263 |
+
print(" ✓ test_conversation_mesh")
|
| 264 |
+
test_skill_building_pools()
|
| 265 |
+
print(" ✓ test_skill_building_pools")
|
| 266 |
+
test_mesh_auto_categories()
|
| 267 |
+
print(" ✓ test_mesh_auto_categories")
|
| 268 |
+
test_mesh_stats()
|
| 269 |
+
print(" ✓ test_mesh_stats")
|
| 270 |
+
test_random_mode_selection()
|
| 271 |
+
print(" ✓ test_random_mode_selection")
|
| 272 |
+
test_skill_cascade()
|
| 273 |
+
print(" ✓ test_skill_cascade")
|
| 274 |
+
test_cascade_related_topics()
|
| 275 |
+
print(" ✓ test_cascade_related_topics")
|
| 276 |
+
test_cascade_depth_limit()
|
| 277 |
+
print(" ✓ test_cascade_depth_limit")
|
| 278 |
+
print("\nAll conversation mesh tests passed!")
|
tests/test_fast_reply_identity.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test fast reply cache, first-run naming, and 100-project mode."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 7 |
+
|
| 8 |
+
from splitbit_llm.memory.fast_cache import FastReplyCache
|
| 9 |
+
from splitbit_llm.identity import FirstRunManager
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_fast_reply_cache():
|
| 13 |
+
"""Test fast reply cache — store, lookup, cache hit."""
|
| 14 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 15 |
+
db_path = f.name
|
| 16 |
+
|
| 17 |
+
cache = FastReplyCache(db_path=db_path)
|
| 18 |
+
|
| 19 |
+
# Store a response
|
| 20 |
+
cache.store("What is Python?", "Python is a programming language.", confidence=0.96, response_time_s=0.5)
|
| 21 |
+
|
| 22 |
+
# Exact match lookup
|
| 23 |
+
result = cache.lookup("What is Python?")
|
| 24 |
+
assert result is not None, "Expected cache hit"
|
| 25 |
+
assert result["cache_hit"] is True
|
| 26 |
+
assert result["cache_type"] == "exact"
|
| 27 |
+
assert "Python is a programming language" in result["response"]
|
| 28 |
+
print(f" Exact hit: {result['cache_type']} (confidence: {result['confidence']:.2f})")
|
| 29 |
+
|
| 30 |
+
# Miss for different query
|
| 31 |
+
result2 = cache.lookup("What is JavaScript?")
|
| 32 |
+
assert result2 is None, "Expected cache miss"
|
| 33 |
+
print(f" Miss for different query: OK")
|
| 34 |
+
|
| 35 |
+
# Store more entries
|
| 36 |
+
cache.store("What is JavaScript?", "JavaScript is a web programming language.", confidence=0.95)
|
| 37 |
+
cache.store("How do I code?", "Write code in a text editor and run it.", confidence=0.90)
|
| 38 |
+
cache.store("What is AI?", "AI is artificial intelligence.", confidence=0.97)
|
| 39 |
+
|
| 40 |
+
# Semantic match (similar words)
|
| 41 |
+
result3 = cache.lookup("What is Python programming?")
|
| 42 |
+
# May or may not hit depending on threshold, but should not crash
|
| 43 |
+
print(f" Semantic lookup: {'hit' if result3 else 'miss'} (OK)")
|
| 44 |
+
|
| 45 |
+
# Stats
|
| 46 |
+
stats = cache.get_stats()
|
| 47 |
+
assert stats["entries_stored"] >= 4
|
| 48 |
+
assert stats["total_lookups"] >= 3
|
| 49 |
+
print(f" Stats: {stats['entries_stored']} entries, hit rate: {stats['hit_rate']:.1%}")
|
| 50 |
+
|
| 51 |
+
# Persistence
|
| 52 |
+
cache2 = FastReplyCache(db_path=db_path)
|
| 53 |
+
stats2 = cache2.get_stats()
|
| 54 |
+
assert stats2["entries_stored"] >= 4, f"Cache not persisted: {stats2}"
|
| 55 |
+
print(f" Persisted: {stats2['entries_stored']} entries")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def test_fast_cache_normalization():
|
| 59 |
+
"""Test query normalization for cache."""
|
| 60 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 61 |
+
db_path = f.name
|
| 62 |
+
|
| 63 |
+
cache = FastReplyCache(db_path=db_path)
|
| 64 |
+
cache.store("What is Python?", "Answer", confidence=0.96)
|
| 65 |
+
|
| 66 |
+
# Normalized version should also hit
|
| 67 |
+
result = cache.lookup("what is python?")
|
| 68 |
+
assert result is not None, "Case difference should still hit"
|
| 69 |
+
print(f" Case-insensitive hit: OK")
|
| 70 |
+
|
| 71 |
+
result2 = cache.lookup("What is Python?")
|
| 72 |
+
assert result2 is not None, "Extra spaces should still hit"
|
| 73 |
+
print(f" Whitespace-normalized hit: OK")
|
| 74 |
+
|
| 75 |
+
result3 = cache.lookup("What is Python!")
|
| 76 |
+
assert result3 is not None, "Punctuation difference should still hit"
|
| 77 |
+
print(f" Punctuation-normalized hit: OK")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_first_run_naming():
|
| 81 |
+
"""Test first-run naming flow."""
|
| 82 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 83 |
+
# First run — no identity file
|
| 84 |
+
identity = FirstRunManager(data_dir=tmpdir)
|
| 85 |
+
assert identity.is_first_run() is True
|
| 86 |
+
greeting = identity.get_greeting()
|
| 87 |
+
assert "Incentives Inc." in greeting
|
| 88 |
+
assert "name me" in greeting.lower()
|
| 89 |
+
print(f" First run greeting: {greeting}")
|
| 90 |
+
|
| 91 |
+
# Set name
|
| 92 |
+
identity.set_name("Jarvis")
|
| 93 |
+
assert identity.is_first_run() is False
|
| 94 |
+
assert identity.get_name() == "Jarvis"
|
| 95 |
+
print(f" Named: {identity.get_name()}")
|
| 96 |
+
|
| 97 |
+
# System prompt suffix
|
| 98 |
+
suffix = identity.get_system_prompt_suffix()
|
| 99 |
+
assert "Jarvis" in suffix
|
| 100 |
+
assert "Incentives Inc." in suffix
|
| 101 |
+
print(f" System prompt suffix: {suffix.strip()}")
|
| 102 |
+
|
| 103 |
+
# Welcome back greeting
|
| 104 |
+
greeting2 = identity.get_greeting()
|
| 105 |
+
assert "Jarvis" in greeting2
|
| 106 |
+
print(f" Welcome back: {greeting2}")
|
| 107 |
+
|
| 108 |
+
# Persistence — load again
|
| 109 |
+
identity2 = FirstRunManager(data_dir=tmpdir)
|
| 110 |
+
assert identity2.is_first_run() is False
|
| 111 |
+
assert identity2.get_name() == "Jarvis"
|
| 112 |
+
print(f" Persisted name: {identity2.get_name()}")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def test_first_run_no_name():
|
| 116 |
+
"""Test first-run when user doesn't provide a name."""
|
| 117 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 118 |
+
identity = FirstRunManager(data_dir=tmpdir)
|
| 119 |
+
assert identity.is_first_run()
|
| 120 |
+
|
| 121 |
+
# Default name when not set
|
| 122 |
+
assert identity.get_name() == "Incentives Inc. LLM"
|
| 123 |
+
print(f" Default name: {identity.get_name()}")
|
| 124 |
+
|
| 125 |
+
# System prompt without name
|
| 126 |
+
suffix = identity.get_system_prompt_suffix()
|
| 127 |
+
assert "Incentives Inc." in suffix
|
| 128 |
+
assert "Your name is" not in suffix
|
| 129 |
+
print(f" Default suffix: {suffix.strip()}")
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def test_cache_hit_rate():
|
| 133 |
+
"""Test cache hit rate calculation."""
|
| 134 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 135 |
+
db_path = f.name
|
| 136 |
+
|
| 137 |
+
cache = FastReplyCache(db_path=db_path)
|
| 138 |
+
|
| 139 |
+
# Store some entries
|
| 140 |
+
cache.store("Q1", "A1", confidence=0.96)
|
| 141 |
+
cache.store("Q2", "A2", confidence=0.96)
|
| 142 |
+
cache.store("Q3", "A3", confidence=0.96)
|
| 143 |
+
|
| 144 |
+
# 3 hits, 2 misses = 60% hit rate
|
| 145 |
+
cache.lookup("Q1") # hit
|
| 146 |
+
cache.lookup("Q2") # hit
|
| 147 |
+
cache.lookup("Q3") # hit
|
| 148 |
+
cache.lookup("Q4") # miss
|
| 149 |
+
cache.lookup("Q5") # miss
|
| 150 |
+
|
| 151 |
+
rate = cache.get_hit_rate()
|
| 152 |
+
assert 0.5 < rate < 0.7, f"Hit rate should be ~0.6, got {rate}"
|
| 153 |
+
print(f" Hit rate: {rate:.1%} (5 lookups, 3 hits)")
|
| 154 |
+
|
| 155 |
+
# Not fast ready yet (too few entries)
|
| 156 |
+
assert cache.is_fast_ready() is False
|
| 157 |
+
print(f" Fast ready: {cache.is_fast_ready()} (needs 50+ entries)")
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
print("Running fast cache, identity, and 100-project tests...")
|
| 162 |
+
test_fast_reply_cache()
|
| 163 |
+
print(" ✓ test_fast_reply_cache")
|
| 164 |
+
test_fast_cache_normalization()
|
| 165 |
+
print(" ✓ test_fast_cache_normalization")
|
| 166 |
+
test_first_run_naming()
|
| 167 |
+
print(" ✓ test_first_run_naming")
|
| 168 |
+
test_first_run_no_name()
|
| 169 |
+
print(" ✓ test_first_run_no_name")
|
| 170 |
+
test_cache_hit_rate()
|
| 171 |
+
print(" ✓ test_cache_hit_rate")
|
| 172 |
+
print("\nAll fast reply & identity tests passed!")
|
tests/test_harness.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test harness: chat, tools, skills, stats."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 6 |
+
|
| 7 |
+
from splitbit_llm.harness.harness import SplitBitHarness
|
| 8 |
+
from splitbit_llm.harness.tools import ToolRegistry, get_default_tools, tool_loop, parse_tool_calls
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_harness_chat():
|
| 12 |
+
"""Test harness chat functionality."""
|
| 13 |
+
harness = SplitBitHarness()
|
| 14 |
+
result = harness.chat("Hello there", channel="cli")
|
| 15 |
+
|
| 16 |
+
assert "response" in result, "No response in result"
|
| 17 |
+
assert "elapsed_s" in result, "No elapsed time"
|
| 18 |
+
assert "stats" in result, "No stats"
|
| 19 |
+
print(f" Response: {result['response'][:60]}")
|
| 20 |
+
print(f" Elapsed: {result['elapsed_s']}s")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_harness_stats():
|
| 24 |
+
"""Test harness stats aggregation."""
|
| 25 |
+
harness = SplitBitHarness()
|
| 26 |
+
stats = harness.get_stats()
|
| 27 |
+
|
| 28 |
+
assert "model" in stats, "No model stats"
|
| 29 |
+
assert "skills" in stats, "No skills stats"
|
| 30 |
+
assert "recursive_links" in stats, "No link stats"
|
| 31 |
+
assert "auto_sizer" in stats, "No auto_sizer stats"
|
| 32 |
+
print(f" Model params: {stats['model']['param_count']:,}")
|
| 33 |
+
print(f" Tier: {stats['auto_sizer']['tier']}")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_tool_registry():
|
| 37 |
+
"""Test tool registry and execution."""
|
| 38 |
+
registry = ToolRegistry()
|
| 39 |
+
for tool in get_default_tools():
|
| 40 |
+
registry.register(tool)
|
| 41 |
+
|
| 42 |
+
assert len(registry.list_tools()) == 9, f"Wrong tool count: {len(registry.list_tools())}"
|
| 43 |
+
print(f" Tools: {len(registry.list_tools())}")
|
| 44 |
+
|
| 45 |
+
# Test calculate tool
|
| 46 |
+
result = registry.execute("calculate", "2 + 2")
|
| 47 |
+
assert result.success, f"Calculate failed: {result.error}"
|
| 48 |
+
assert "4" in result.output, f"Wrong result: {result.output}"
|
| 49 |
+
print(f" calculate(2+2) = {result.output}")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_tool_loop():
|
| 53 |
+
"""Test tool execution loop."""
|
| 54 |
+
registry = ToolRegistry()
|
| 55 |
+
for tool in get_default_tools():
|
| 56 |
+
registry.register(tool)
|
| 57 |
+
|
| 58 |
+
text = "Let me calculate: [TOOL: calculate(3 * 7)]"
|
| 59 |
+
final_text, results = tool_loop(text, registry)
|
| 60 |
+
|
| 61 |
+
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
|
| 62 |
+
assert results[0].success, "Tool execution failed"
|
| 63 |
+
assert "21" in results[0].output, f"Wrong output: {results[0].output}"
|
| 64 |
+
print(f" Tool loop: {len(results)} calls, output: {results[0].output}")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_parse_tool_calls():
|
| 68 |
+
"""Test parsing tool calls from text."""
|
| 69 |
+
text = "I will [TOOL: calculate(1 + 1)] and then [TOOL: calculate(2 + 2)]"
|
| 70 |
+
calls = parse_tool_calls(text)
|
| 71 |
+
assert len(calls) == 2, f"Expected 2 calls, got {len(calls)}"
|
| 72 |
+
assert calls[0][0] == "calculate", f"Wrong tool name: {calls[0][0]}"
|
| 73 |
+
print(f" Parsed {len(calls)} tool calls")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
print("Running harness tests...")
|
| 78 |
+
test_harness_chat()
|
| 79 |
+
print(" ✓ test_harness_chat")
|
| 80 |
+
test_harness_stats()
|
| 81 |
+
print(" ✓ test_harness_stats")
|
| 82 |
+
test_tool_registry()
|
| 83 |
+
print(" ✓ test_tool_registry")
|
| 84 |
+
test_tool_loop()
|
| 85 |
+
print(" ✓ test_tool_loop")
|
| 86 |
+
test_parse_tool_calls()
|
| 87 |
+
print(" ✓ test_parse_tool_calls")
|
| 88 |
+
print("\nAll harness tests passed!")
|
tests/test_memory_agents.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test persistent memory, goal memory, and agents."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 7 |
+
|
| 8 |
+
from splitbit_llm.memory.persistent import PersistentMemory
|
| 9 |
+
from splitbit_llm.memory.goal_memory import GoalMemory, Goal
|
| 10 |
+
from splitbit_llm.agents.agent_manager import AgentManager
|
| 11 |
+
from splitbit_llm.agents.planner_agent import PlannerAgent
|
| 12 |
+
from splitbit_llm.agents.coder_agent import CoderAgent
|
| 13 |
+
from splitbit_llm.agents.researcher_agent import ResearcherAgent
|
| 14 |
+
from splitbit_llm.agents.reviewer_agent import ReviewerAgent
|
| 15 |
+
from splitbit_llm.agents.executor_agent import ExecutorAgent
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_persistent_memory():
|
| 19 |
+
"""Test persistent memory storage and recall."""
|
| 20 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 21 |
+
db_path = f.name
|
| 22 |
+
|
| 23 |
+
mem = PersistentMemory(db_path=db_path)
|
| 24 |
+
mem.set_session("test-session")
|
| 25 |
+
|
| 26 |
+
# Store episodic
|
| 27 |
+
mem.add_episodic("user", "What is Python?", importance=0.7)
|
| 28 |
+
mem.add_episodic("assistant", "Python is a programming language.", importance=0.8)
|
| 29 |
+
|
| 30 |
+
# Store semantic
|
| 31 |
+
mem.add_semantic("Python is an interpreted programming language", confidence=0.9)
|
| 32 |
+
|
| 33 |
+
# Recall
|
| 34 |
+
episodic = mem.recall_episodic("Python", max_results=3)
|
| 35 |
+
assert len(episodic) >= 1, "No episodic memories recalled"
|
| 36 |
+
print(f" Episodic recalled: {len(episodic)}")
|
| 37 |
+
|
| 38 |
+
semantic = mem.recall_semantic("Python", max_results=3)
|
| 39 |
+
assert len(semantic) >= 1, "No semantic memories recalled"
|
| 40 |
+
print(f" Semantic recalled: {len(semantic)}")
|
| 41 |
+
|
| 42 |
+
# Context injection
|
| 43 |
+
context = mem.get_context("Python programming")
|
| 44 |
+
assert "Python" in context or "programming" in context, f"Context empty: {context}"
|
| 45 |
+
print(f" Context: {context[:80]}")
|
| 46 |
+
|
| 47 |
+
# Persistence test
|
| 48 |
+
mem2 = PersistentMemory(db_path=db_path)
|
| 49 |
+
stats = mem2.get_stats()
|
| 50 |
+
assert stats["episodic_total"] >= 2, f"Episodic not persisted: {stats}"
|
| 51 |
+
assert stats["semantic_total"] >= 1, f"Semantic not persisted: {stats}"
|
| 52 |
+
print(f" Persisted: {stats['episodic_total']} episodic, {stats['semantic_total']} semantic")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_goal_memory():
|
| 56 |
+
"""Test goal creation, planning, and execution."""
|
| 57 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 58 |
+
db_path = f.name
|
| 59 |
+
|
| 60 |
+
gm = GoalMemory(db_path=db_path)
|
| 61 |
+
|
| 62 |
+
# Create goal
|
| 63 |
+
goal = gm.create_goal("Build a web app", "Create a simple Flask web application", priority="high")
|
| 64 |
+
assert goal.status == "pending"
|
| 65 |
+
print(f" Created goal: {goal.title} ({goal.id[:8]})")
|
| 66 |
+
|
| 67 |
+
# Plan goal
|
| 68 |
+
result = gm.plan_goal(goal.id, steps=[
|
| 69 |
+
{"title": "Setup", "description": "Create project structure"},
|
| 70 |
+
{"title": "Code", "description": "Write the Flask app"},
|
| 71 |
+
{"title": "Test", "description": "Test the application"},
|
| 72 |
+
])
|
| 73 |
+
assert result["success"]
|
| 74 |
+
goal = gm.get_goal(goal.id)
|
| 75 |
+
assert goal.status == "in_progress"
|
| 76 |
+
assert len(goal.steps) == 3
|
| 77 |
+
print(f" Planned: {len(goal.steps)} steps, status: {goal.status}")
|
| 78 |
+
|
| 79 |
+
# Execute steps
|
| 80 |
+
r1 = gm.execute_step(goal.id, "Project structure created", success=True)
|
| 81 |
+
assert r1["success"]
|
| 82 |
+
r2 = gm.execute_step(goal.id, "Flask app written", success=True)
|
| 83 |
+
assert r2["success"]
|
| 84 |
+
r3 = gm.execute_step(goal.id, "Tests passed", success=True)
|
| 85 |
+
assert r3["success"]
|
| 86 |
+
|
| 87 |
+
goal = gm.get_goal(goal.id)
|
| 88 |
+
assert goal.status == "completed"
|
| 89 |
+
assert goal.progress() == 1.0
|
| 90 |
+
print(f" Completed: {goal.progress():.0%}")
|
| 91 |
+
|
| 92 |
+
# Persistence
|
| 93 |
+
gm2 = GoalMemory(db_path=db_path)
|
| 94 |
+
stats = gm2.get_stats()
|
| 95 |
+
assert stats["total"] == 1
|
| 96 |
+
assert stats["completed"] == 1
|
| 97 |
+
print(f" Persisted: {stats}")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def test_agent_manager():
|
| 101 |
+
"""Test agent manager with 5 agents."""
|
| 102 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 103 |
+
goal_db = f.name
|
| 104 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 105 |
+
mem_db = f.name
|
| 106 |
+
|
| 107 |
+
gm = GoalMemory(db_path=goal_db)
|
| 108 |
+
pm = PersistentMemory(db_path=mem_db)
|
| 109 |
+
|
| 110 |
+
# Mock generate function
|
| 111 |
+
def mock_generate(prompt):
|
| 112 |
+
if "planning" in prompt.lower():
|
| 113 |
+
return '{"steps": [{"title": "Step 1", "description": "Do thing 1"}], "sub_goals": []}'
|
| 114 |
+
return "Mock response for: " + prompt[:50]
|
| 115 |
+
|
| 116 |
+
manager = AgentManager(gm, pm, generate_fn=mock_generate)
|
| 117 |
+
assert len(manager.agents) == 5
|
| 118 |
+
print(f" Agents: {list(manager.agents.keys())}")
|
| 119 |
+
|
| 120 |
+
# Create a project
|
| 121 |
+
goal = manager.create_project("Test project", "A test project for validation", priority="high")
|
| 122 |
+
print(f" Created project: {goal.id[:8]}")
|
| 123 |
+
|
| 124 |
+
# Get status
|
| 125 |
+
status = manager.get_agent_status()
|
| 126 |
+
assert len(status) == 5
|
| 127 |
+
for name, info in status.items():
|
| 128 |
+
assert info["name"] == name
|
| 129 |
+
assert info["running"] == False # not started yet
|
| 130 |
+
print(f" All 5 agents present: {list(status.keys())}")
|
| 131 |
+
|
| 132 |
+
# Get stats
|
| 133 |
+
stats = manager.get_stats()
|
| 134 |
+
assert "agents" in stats
|
| 135 |
+
assert "goals" in stats
|
| 136 |
+
print(f" Stats: {stats['goals']}")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def test_agent_specialization():
|
| 141 |
+
"""Test that agents have correct specializations."""
|
| 142 |
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
| 143 |
+
db_path = f.name
|
| 144 |
+
|
| 145 |
+
gm = GoalMemory(db_path=db_path)
|
| 146 |
+
|
| 147 |
+
planner = PlannerAgent(gm)
|
| 148 |
+
coder = CoderAgent(gm)
|
| 149 |
+
researcher = ResearcherAgent(gm)
|
| 150 |
+
reviewer = ReviewerAgent(gm)
|
| 151 |
+
executor = ExecutorAgent(gm)
|
| 152 |
+
|
| 153 |
+
# Test coder can handle code goals
|
| 154 |
+
code_goal = gm.create_goal("Write code", "Implement a Python function to sort data")
|
| 155 |
+
assert coder._can_handle(code_goal), "Coder should handle code goals"
|
| 156 |
+
assert not researcher._can_handle(code_goal), "Researcher should not handle code goals"
|
| 157 |
+
print(" Coder handles code goals: OK")
|
| 158 |
+
|
| 159 |
+
# Test researcher can handle research goals
|
| 160 |
+
research_goal = gm.create_goal("Research topic", "Research the best sorting algorithms")
|
| 161 |
+
assert researcher._can_handle(research_goal), "Researcher should handle research goals"
|
| 162 |
+
print(" Researcher handles research goals: OK")
|
| 163 |
+
|
| 164 |
+
# Test executor can handle execution goals
|
| 165 |
+
exec_goal = gm.create_goal("Run tests", "Execute the test suite and deploy")
|
| 166 |
+
assert executor._can_handle(exec_goal), "Executor should handle execution goals"
|
| 167 |
+
print(" Executor handles execution goals: OK")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if __name__ == "__main__":
|
| 172 |
+
print("Running memory & agents tests...")
|
| 173 |
+
test_persistent_memory()
|
| 174 |
+
print(" ✓ test_persistent_memory")
|
| 175 |
+
test_goal_memory()
|
| 176 |
+
print(" ✓ test_goal_memory")
|
| 177 |
+
test_agent_manager()
|
| 178 |
+
print(" ✓ test_agent_manager")
|
| 179 |
+
test_agent_specialization()
|
| 180 |
+
print(" ✓ test_agent_specialization")
|
| 181 |
+
print("\nAll memory & agents tests passed!")
|
tests/test_model.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test model: forward pass, generation, save/load."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from splitbit_llm.config import get_model_config, HardwareTier
|
| 9 |
+
from splitbit_llm.model.model import SplitBitLLM
|
| 10 |
+
from splitbit_llm.model.tokenizer import BPETokenizer
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_model_forward():
|
| 14 |
+
"""Test model forward pass."""
|
| 15 |
+
cfg = get_model_config(HardwareTier.MOBILE)
|
| 16 |
+
cfg.vocab_size = 256
|
| 17 |
+
model = SplitBitLLM(config=cfg)
|
| 18 |
+
|
| 19 |
+
token_ids = np.array([[1, 5, 10, 15, 20]], dtype=np.int64)
|
| 20 |
+
logits = model.forward(token_ids)
|
| 21 |
+
|
| 22 |
+
assert logits.shape == (1, 5, 256), f"Wrong shape: {logits.shape}"
|
| 23 |
+
print(f" Logits shape: {logits.shape}")
|
| 24 |
+
print(f" Param count: {model.param_count:,}")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def test_model_generate():
|
| 28 |
+
"""Test text generation."""
|
| 29 |
+
cfg = get_model_config(HardwareTier.MOBILE)
|
| 30 |
+
cfg.vocab_size = 256
|
| 31 |
+
model = SplitBitLLM(config=cfg)
|
| 32 |
+
|
| 33 |
+
output = model.generate("Hello", max_tokens=10, temperature=0.7)
|
| 34 |
+
assert isinstance(output, str), f"Expected str, got {type(output)}"
|
| 35 |
+
assert len(output) > 0, "Empty output"
|
| 36 |
+
print(f" Generated: {repr(output[:50])}")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_model_generate_stream():
|
| 40 |
+
"""Test streaming generation."""
|
| 41 |
+
cfg = get_model_config(HardwareTier.MOBILE)
|
| 42 |
+
cfg.vocab_size = 256
|
| 43 |
+
model = SplitBitLLM(config=cfg)
|
| 44 |
+
|
| 45 |
+
chunks = list(model.generate_stream("Hello", max_tokens=10, temperature=0.7))
|
| 46 |
+
assert len(chunks) > 0, "No chunks generated"
|
| 47 |
+
print(f" Chunks: {len(chunks)}")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_model_with_tokenizer():
|
| 51 |
+
"""Test model with trained tokenizer."""
|
| 52 |
+
tok = BPETokenizer(vocab_size=256)
|
| 53 |
+
tok.train("Hello world! This is a test. Hello world again. The quick brown fox jumps.")
|
| 54 |
+
|
| 55 |
+
cfg = get_model_config(HardwareTier.MOBILE)
|
| 56 |
+
cfg.vocab_size = 256
|
| 57 |
+
model = SplitBitLLM(config=cfg, tokenizer=tok)
|
| 58 |
+
|
| 59 |
+
output = model.generate("Hello", max_tokens=10, temperature=0.7)
|
| 60 |
+
assert isinstance(output, str)
|
| 61 |
+
print(f" Generated with tokenizer: {repr(output[:50])}")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_model_truncation():
|
| 65 |
+
"""Test that long inputs are truncated to max_seq_len."""
|
| 66 |
+
cfg = get_model_config(HardwareTier.MOBILE)
|
| 67 |
+
cfg.vocab_size = 256
|
| 68 |
+
cfg.max_seq_len = 32
|
| 69 |
+
model = SplitBitLLM(config=cfg)
|
| 70 |
+
|
| 71 |
+
# Input longer than max_seq_len
|
| 72 |
+
long_input = np.array([[i for i in range(100)]], dtype=np.int64)
|
| 73 |
+
logits = model.forward(long_input)
|
| 74 |
+
assert logits.shape[1] == 32, f"Should truncate to 32, got {logits.shape[1]}"
|
| 75 |
+
print(f" Truncated to: {logits.shape[1]}")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
print("Running model tests...")
|
| 80 |
+
test_model_forward()
|
| 81 |
+
print(" ✓ test_model_forward")
|
| 82 |
+
test_model_generate()
|
| 83 |
+
print(" ✓ test_model_generate")
|
| 84 |
+
test_model_generate_stream()
|
| 85 |
+
print(" ✓ test_model_generate_stream")
|
| 86 |
+
test_model_with_tokenizer()
|
| 87 |
+
print(" ✓ test_model_with_tokenizer")
|
| 88 |
+
test_model_truncation()
|
| 89 |
+
print(" ✓ test_model_truncation")
|
| 90 |
+
print("\nAll model tests passed!")
|
tests/test_quantization.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test SplitBit quantization: ternary, 4-bit, 8-bit, round-trip accuracy."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from splitbit_llm.model.quantization import SplitBitQuantizer, bits_per_weight, compression_ratio
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_ternary_quantization():
|
| 12 |
+
"""Test ternary quantization round-trip."""
|
| 13 |
+
quantizer = SplitBitQuantizer(format="ternary")
|
| 14 |
+
weights = np.random.randn(64, 32).astype(np.float32) * 0.1
|
| 15 |
+
|
| 16 |
+
packed = quantizer.quantize(weights)
|
| 17 |
+
dequant = quantizer.dequantize(packed)
|
| 18 |
+
|
| 19 |
+
# Ternary loses precision but preserves sign and approximate magnitude
|
| 20 |
+
correlation = np.corrcoef(weights.flatten(), dequant.flatten())[0, 1]
|
| 21 |
+
assert correlation > 0.5, f"Ternary correlation too low: {correlation}"
|
| 22 |
+
print(f" Ternary correlation: {correlation:.3f}")
|
| 23 |
+
print(f" BPW: {quantizer.bpw:.3f}, Compression: {compression_ratio('ternary'):.1f}x")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_4bit_quantization():
|
| 27 |
+
"""Test 4-bit quantization round-trip."""
|
| 28 |
+
quantizer = SplitBitQuantizer(format="q4_k_m")
|
| 29 |
+
weights = np.random.randn(128, 64).astype(np.float32) * 0.1
|
| 30 |
+
|
| 31 |
+
packed = quantizer.quantize(weights)
|
| 32 |
+
dequant = quantizer.dequantize(packed)
|
| 33 |
+
|
| 34 |
+
# 4-bit should be closer to original
|
| 35 |
+
max_error = np.max(np.abs(weights - dequant))
|
| 36 |
+
rel_error = max_error / np.max(np.abs(weights))
|
| 37 |
+
assert rel_error < 0.2, f"4-bit relative error too high: {rel_error}"
|
| 38 |
+
print(f" 4-bit max relative error: {rel_error:.4f}")
|
| 39 |
+
print(f" BPW: {quantizer.bpw:.1f}, Compression: {compression_ratio('q4_k_m'):.1f}x")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_8bit_quantization():
|
| 43 |
+
"""Test 8-bit quantization round-trip."""
|
| 44 |
+
quantizer = SplitBitQuantizer(format="q8_0")
|
| 45 |
+
weights = np.random.randn(256, 128).astype(np.float32) * 0.1
|
| 46 |
+
|
| 47 |
+
packed = quantizer.quantize(weights)
|
| 48 |
+
dequant = quantizer.dequantize(packed)
|
| 49 |
+
|
| 50 |
+
# 8-bit should be very close
|
| 51 |
+
max_error = np.max(np.abs(weights - dequant))
|
| 52 |
+
rel_error = max_error / np.max(np.abs(weights))
|
| 53 |
+
assert rel_error < 0.02, f"8-bit relative error too high: {rel_error}"
|
| 54 |
+
print(f" 8-bit max relative error: {rel_error:.5f}")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_fp16_passthrough():
|
| 58 |
+
"""Test fp16 passthrough (no quantization)."""
|
| 59 |
+
quantizer = SplitBitQuantizer(format="fp16")
|
| 60 |
+
weights = np.random.randn(64, 32).astype(np.float32)
|
| 61 |
+
|
| 62 |
+
packed = quantizer.quantize(weights)
|
| 63 |
+
dequant = quantizer.dequantize(packed)
|
| 64 |
+
|
| 65 |
+
# fp16 should be nearly identical
|
| 66 |
+
max_error = np.max(np.abs(weights - dequant))
|
| 67 |
+
assert max_error < 0.01, f"fp16 error too high: {max_error}"
|
| 68 |
+
print(f" fp16 max error: {max_error:.6f}")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_bpw_table():
|
| 72 |
+
"""Test bits per weight table."""
|
| 73 |
+
assert bits_per_weight("ternary") > 1.5
|
| 74 |
+
assert bits_per_weight("q4_k_m") == 4.0
|
| 75 |
+
assert bits_per_weight("q8_0") == 8.0
|
| 76 |
+
assert bits_per_weight("fp16") == 16.0
|
| 77 |
+
assert compression_ratio("ternary") > 10.0
|
| 78 |
+
print(f" Ternary BPW: {bits_per_weight('ternary'):.3f}")
|
| 79 |
+
print(f" Ternary compression: {compression_ratio('ternary'):.1f}x")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
print("Running quantization tests...")
|
| 84 |
+
test_ternary_quantization()
|
| 85 |
+
print(" ✓ test_ternary_quantization")
|
| 86 |
+
test_4bit_quantization()
|
| 87 |
+
print(" ✓ test_4bit_quantization")
|
| 88 |
+
test_8bit_quantization()
|
| 89 |
+
print(" ✓ test_8bit_quantization")
|
| 90 |
+
test_fp16_passthrough()
|
| 91 |
+
print(" ✓ test_fp16_passthrough")
|
| 92 |
+
test_bpw_table()
|
| 93 |
+
print(" ✓ test_bpw_table")
|
| 94 |
+
print("\nAll quantization tests passed!")
|
tests/test_splitbit.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test recursive link graph: context storage, linking, traversal."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 6 |
+
|
| 7 |
+
from splitbit_llm.splitbit.recursive_link import RecursiveLinkGraph
|
| 8 |
+
from splitbit_llm.splitbit.universal_link import UniversalLinkManager
|
| 9 |
+
from splitbit_llm.splitbit.splitbit_tokens import SplitBitTokenizer, SplitBitTokenConfig
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_recursive_link_basic():
|
| 13 |
+
"""Test basic context storage and retrieval."""
|
| 14 |
+
graph = RecursiveLinkGraph()
|
| 15 |
+
ctx_id = graph.add_context("Hello world", "Hi there!")
|
| 16 |
+
assert ctx_id in graph._contexts
|
| 17 |
+
print(f" Context stored: {ctx_id[:8]}")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_recursive_link_related():
|
| 21 |
+
"""Test finding related contexts."""
|
| 22 |
+
graph = RecursiveLinkGraph()
|
| 23 |
+
graph.add_context("Hello world programming", "Programming is fun!")
|
| 24 |
+
graph.add_context("Hello world greeting", "Hi there!")
|
| 25 |
+
graph.add_context("Python code example", "Here is some code.")
|
| 26 |
+
|
| 27 |
+
related = graph.find_related("Hello world")
|
| 28 |
+
assert len(related) > 0, "No related contexts found"
|
| 29 |
+
print(f" Found {len(related)} related contexts")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_recursive_link_injection():
|
| 33 |
+
"""Test context injection for inference."""
|
| 34 |
+
graph = RecursiveLinkGraph()
|
| 35 |
+
graph.add_context("What is Python?", "Python is a programming language.")
|
| 36 |
+
graph.add_context("How to code in Python?", "You can write Python code in any editor.")
|
| 37 |
+
|
| 38 |
+
injection = graph.get_injection_context("Tell me about Python")
|
| 39 |
+
assert "Python" in injection, f"Injection missing context: {injection}"
|
| 40 |
+
print(f" Injection: {injection[:80]}")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_universal_link():
|
| 44 |
+
"""Test universal link manager."""
|
| 45 |
+
mgr = UniversalLinkManager(instance_id="test-instance")
|
| 46 |
+
learning_id = mgr.share_learning("conversation", {"message": "test"})
|
| 47 |
+
assert learning_id is not None
|
| 48 |
+
stats = mgr.get_stats()
|
| 49 |
+
assert stats["learnings_shared"] == 1
|
| 50 |
+
print(f" Shared learning: {learning_id[:8]}")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_splitbit_tokens():
|
| 54 |
+
"""Test SplitBit token encoding/decoding."""
|
| 55 |
+
tok = SplitBitTokenizer(SplitBitTokenConfig(format="q4_k_m"))
|
| 56 |
+
token_ids = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50]
|
| 57 |
+
encoded = tok.encode(token_ids)
|
| 58 |
+
decoded = tok.decode(encoded, len(token_ids))
|
| 59 |
+
|
| 60 |
+
assert len(encoded) < len(token_ids) * 4, "No compression achieved"
|
| 61 |
+
print(f" Encoded {len(token_ids)} tokens to {len(encoded)} bytes")
|
| 62 |
+
print(f" Compression: {len(token_ids) * 4 / len(encoded):.1f}x")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
print("Running splitbit tests...")
|
| 67 |
+
test_recursive_link_basic()
|
| 68 |
+
print(" ✓ test_recursive_link_basic")
|
| 69 |
+
test_recursive_link_related()
|
| 70 |
+
print(" ✓ test_recursive_link_related")
|
| 71 |
+
test_recursive_link_injection()
|
| 72 |
+
print(" ✓ test_recursive_link_injection")
|
| 73 |
+
test_universal_link()
|
| 74 |
+
print(" ✓ test_universal_link")
|
| 75 |
+
test_splitbit_tokens()
|
| 76 |
+
print(" ✓ test_splitbit_tokens")
|
| 77 |
+
print("\nAll splitbit tests passed!")
|
tests/test_subscription.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test subscription manager — $1/month subscription with free trial."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
import time
|
| 7 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 8 |
+
|
| 9 |
+
from splitbit_llm.subscription import SubscriptionManager, MONTHLY_PRICE
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_no_subscription_blocks_access():
|
| 13 |
+
"""Test that no subscription blocks access."""
|
| 14 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 15 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 16 |
+
|
| 17 |
+
access = sub.check_access()
|
| 18 |
+
assert not access["has_access"]
|
| 19 |
+
assert access["status"] == "none"
|
| 20 |
+
print(f" No subscription: access blocked — '{access['message']}'")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_free_trial():
|
| 24 |
+
"""Test 7-day free trial."""
|
| 25 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 26 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 27 |
+
|
| 28 |
+
# Start trial
|
| 29 |
+
result = sub.start_trial()
|
| 30 |
+
assert result["success"]
|
| 31 |
+
assert result["status"] == "trial"
|
| 32 |
+
assert result["days_remaining"] == 7
|
| 33 |
+
print(f" Trial started: {result['days_remaining']} days")
|
| 34 |
+
|
| 35 |
+
# Access should be granted
|
| 36 |
+
access = sub.check_access()
|
| 37 |
+
assert access["has_access"]
|
| 38 |
+
assert access["status"] == "trial"
|
| 39 |
+
print(f" Trial access: granted — '{access['message']}'")
|
| 40 |
+
|
| 41 |
+
# Can't start trial again
|
| 42 |
+
result2 = sub.start_trial()
|
| 43 |
+
assert not result2["success"]
|
| 44 |
+
print(f" Second trial: blocked — '{result2['error']}'")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_subscribe():
|
| 48 |
+
"""Test $1/month subscription."""
|
| 49 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 50 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 51 |
+
|
| 52 |
+
# Subscribe
|
| 53 |
+
result = sub.subscribe()
|
| 54 |
+
assert result["success"]
|
| 55 |
+
assert result["status"] == "active"
|
| 56 |
+
assert result["price"] == MONTHLY_PRICE
|
| 57 |
+
assert result["days_remaining"] >= 29
|
| 58 |
+
print(f" Subscribed: ${result['price']:.2f}/month, {result['days_remaining']} days")
|
| 59 |
+
|
| 60 |
+
# Access granted
|
| 61 |
+
access = sub.check_access()
|
| 62 |
+
assert access["has_access"]
|
| 63 |
+
assert access["status"] == "active"
|
| 64 |
+
print(f" Access: granted — '{access['message']}'")
|
| 65 |
+
|
| 66 |
+
# Stats
|
| 67 |
+
stats = sub.get_stats()
|
| 68 |
+
assert stats["months_subscribed"] == 1
|
| 69 |
+
assert stats["total_paid"] == MONTHLY_PRICE
|
| 70 |
+
assert stats["auto_renew"] is True
|
| 71 |
+
assert len(stats["payment_history"]) == 1
|
| 72 |
+
print(f" Stats: {stats['months_subscribed']} month, ${stats['total_paid']:.2f} paid")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def test_subscribe_extends():
|
| 76 |
+
"""Test that subscribing while active extends the subscription."""
|
| 77 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 78 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 79 |
+
|
| 80 |
+
# First subscription
|
| 81 |
+
result1 = sub.subscribe()
|
| 82 |
+
assert result1["success"]
|
| 83 |
+
assert result1["days_remaining"] >= 29
|
| 84 |
+
|
| 85 |
+
# Second subscription should extend
|
| 86 |
+
result2 = sub.subscribe()
|
| 87 |
+
assert result2["success"]
|
| 88 |
+
assert result2["days_remaining"] >= 59 # 30 + 30
|
| 89 |
+
print(f" Extended: {result2['days_remaining']} days (30 + 30)")
|
| 90 |
+
|
| 91 |
+
stats = sub.get_stats()
|
| 92 |
+
assert stats["months_subscribed"] == 2
|
| 93 |
+
assert stats["total_paid"] == MONTHLY_PRICE * 2
|
| 94 |
+
print(f" Total: {stats['months_subscribed']} months, ${stats['total_paid']:.2f}")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_unsubscribe():
|
| 98 |
+
"""Test cancelling subscription."""
|
| 99 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 100 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 101 |
+
sub.subscribe()
|
| 102 |
+
|
| 103 |
+
# Unsubscribe
|
| 104 |
+
result = sub.unsubscribe()
|
| 105 |
+
assert result["success"]
|
| 106 |
+
assert result["status"] == "cancelled"
|
| 107 |
+
print(f" Unsubscribed: {result['message']}")
|
| 108 |
+
|
| 109 |
+
# Access still works until expiration
|
| 110 |
+
access = sub.check_access()
|
| 111 |
+
assert access["has_access"] # still has access until expiration
|
| 112 |
+
print(f" Access after cancel: still granted until expiration")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def test_expired_blocks_access():
|
| 116 |
+
"""Test that expired subscription blocks access."""
|
| 117 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 118 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 119 |
+
|
| 120 |
+
# Subscribe
|
| 121 |
+
sub.subscribe()
|
| 122 |
+
|
| 123 |
+
# Manually expire the subscription
|
| 124 |
+
sub._state["expiration_date"] = time.time() - 1 # expired 1 second ago
|
| 125 |
+
sub._state["auto_renew"] = False
|
| 126 |
+
sub._save_state()
|
| 127 |
+
|
| 128 |
+
# Should enter grace period first
|
| 129 |
+
access = sub.check_access()
|
| 130 |
+
assert access["has_access"] # grace period
|
| 131 |
+
assert access["status"] == "grace"
|
| 132 |
+
print(f" Grace period: {access['days_remaining']} days — '{access['message']}'")
|
| 133 |
+
|
| 134 |
+
# Expire grace period
|
| 135 |
+
sub._state["expiration_date"] = time.time() - (4 * 86400) # 4 days ago (past 3-day grace)
|
| 136 |
+
sub._save_state()
|
| 137 |
+
|
| 138 |
+
access2 = sub.check_access()
|
| 139 |
+
assert not access2["has_access"]
|
| 140 |
+
assert access2["status"] == "expired"
|
| 141 |
+
print(f" Expired: access blocked — '{access2['message']}'")
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_auto_renew():
|
| 145 |
+
"""Test auto-renewal when subscription expires."""
|
| 146 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 147 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 148 |
+
sub.subscribe()
|
| 149 |
+
|
| 150 |
+
months_before = sub._state["months_subscribed"]
|
| 151 |
+
|
| 152 |
+
# Simulate expiration with auto-renew on
|
| 153 |
+
sub._state["expiration_date"] = time.time() - 1
|
| 154 |
+
sub._state["auto_renew"] = True
|
| 155 |
+
sub._save_state()
|
| 156 |
+
|
| 157 |
+
access = sub.check_access()
|
| 158 |
+
assert access["has_access"]
|
| 159 |
+
assert access["status"] == "active" # auto-renewed
|
| 160 |
+
print(f" Auto-renewed: {access['days_remaining']} days — '{access['message']}'")
|
| 161 |
+
|
| 162 |
+
stats = sub.get_stats()
|
| 163 |
+
assert stats["months_subscribed"] == months_before + 1
|
| 164 |
+
print(f" Months subscribed: {stats['months_subscribed']} (auto-renewed)")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def test_trial_persistence():
|
| 168 |
+
"""Test that subscription state persists across restarts."""
|
| 169 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 170 |
+
sub1 = SubscriptionManager(data_dir=tmpdir)
|
| 171 |
+
sub1.subscribe()
|
| 172 |
+
|
| 173 |
+
# Create new instance — should load state
|
| 174 |
+
sub2 = SubscriptionManager(data_dir=tmpdir)
|
| 175 |
+
stats = sub2.get_stats()
|
| 176 |
+
assert stats["status"] == "active"
|
| 177 |
+
assert stats["months_subscribed"] == 1
|
| 178 |
+
print(f" Persisted: status={stats['status']}, months={stats['months_subscribed']}")
|
| 179 |
+
|
| 180 |
+
access = sub2.check_access()
|
| 181 |
+
assert access["has_access"]
|
| 182 |
+
print(f" Access after restart: granted")
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def test_trial_then_subscribe():
|
| 186 |
+
"""Test using trial first, then subscribing."""
|
| 187 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 188 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 189 |
+
|
| 190 |
+
# Start trial
|
| 191 |
+
trial = sub.start_trial()
|
| 192 |
+
assert trial["success"]
|
| 193 |
+
print(f" Trial: {trial['days_remaining']} days")
|
| 194 |
+
|
| 195 |
+
# Subscribe while on trial
|
| 196 |
+
result = sub.subscribe()
|
| 197 |
+
assert result["success"]
|
| 198 |
+
assert result["status"] == "active"
|
| 199 |
+
print(f" Subscribed during trial: {result['days_remaining']} days")
|
| 200 |
+
|
| 201 |
+
# Can't start trial again
|
| 202 |
+
trial2 = sub.start_trial()
|
| 203 |
+
assert not trial2["success"]
|
| 204 |
+
print(f" Trial after subscribe: blocked")
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def test_payment_history():
|
| 208 |
+
"""Test payment history tracking."""
|
| 209 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 210 |
+
sub = SubscriptionManager(data_dir=tmpdir)
|
| 211 |
+
|
| 212 |
+
# Multiple payments
|
| 213 |
+
sub.subscribe(payment_reference="stripe-001")
|
| 214 |
+
sub.subscribe(payment_reference="stripe-002")
|
| 215 |
+
sub.subscribe(payment_reference="stripe-003")
|
| 216 |
+
|
| 217 |
+
stats = sub.get_stats()
|
| 218 |
+
assert stats["months_subscribed"] == 3
|
| 219 |
+
assert stats["total_paid"] == MONTHLY_PRICE * 3
|
| 220 |
+
assert len(stats["payment_history"]) == 3
|
| 221 |
+
print(f" Payments: {stats['months_subscribed']} months, ${stats['total_paid']:.2f}")
|
| 222 |
+
print(f" History: {len(stats['payment_history'])} records")
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
if __name__ == "__main__":
|
| 226 |
+
print("Running subscription manager tests...")
|
| 227 |
+
test_no_subscription_blocks_access()
|
| 228 |
+
print(" ✓ test_no_subscription_blocks_access")
|
| 229 |
+
test_free_trial()
|
| 230 |
+
print(" ✓ test_free_trial")
|
| 231 |
+
test_subscribe()
|
| 232 |
+
print(" ✓ test_subscribe")
|
| 233 |
+
test_subscribe_extends()
|
| 234 |
+
print(" ✓ test_subscribe_extends")
|
| 235 |
+
test_unsubscribe()
|
| 236 |
+
print(" ✓ test_unsubscribe")
|
| 237 |
+
test_expired_blocks_access()
|
| 238 |
+
print(" ✓ test_expired_blocks_access")
|
| 239 |
+
test_auto_renew()
|
| 240 |
+
print(" ✓ test_auto_renew")
|
| 241 |
+
test_trial_persistence()
|
| 242 |
+
print(" ✓ test_trial_persistence")
|
| 243 |
+
test_trial_then_subscribe()
|
| 244 |
+
print(" ✓ test_trial_then_subscribe")
|
| 245 |
+
test_payment_history()
|
| 246 |
+
print(" ✓ test_payment_history")
|
| 247 |
+
print("\nAll subscription tests passed!")
|
tests/test_tokenizer.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test tokenizer: BPE training, encoding, decoding, round-trip."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 6 |
+
|
| 7 |
+
from splitbit_llm.model.tokenizer import BPETokenizer, BOS_ID, EOS_ID, PAD_ID, UNK_ID
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_tokenizer_basic():
|
| 11 |
+
"""Test basic tokenizer training and encoding."""
|
| 12 |
+
tok = BPETokenizer(vocab_size=128)
|
| 13 |
+
text = "Hello world! This is a test. Hello world again. The quick brown fox."
|
| 14 |
+
tok.train(text, verbose=False)
|
| 15 |
+
|
| 16 |
+
assert tok.actual_vocab_size > 10, f"Vocab too small: {tok.actual_vocab_size}"
|
| 17 |
+
assert tok.actual_vocab_size <= 128, f"Vocab too large: {tok.actual_vocab_size}"
|
| 18 |
+
print(f" Vocab size: {tok.actual_vocab_size}")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_tokenizer_roundtrip():
|
| 22 |
+
"""Test encode → decode round-trip."""
|
| 23 |
+
tok = BPETokenizer(vocab_size=256)
|
| 24 |
+
text = "Hello world! This is a test. Hello world again. The quick brown fox jumps over the lazy dog."
|
| 25 |
+
tok.train(text, verbose=False)
|
| 26 |
+
|
| 27 |
+
ids = tok.encode("Hello world!", add_bos=True, add_eos=True)
|
| 28 |
+
decoded = tok.decode(ids)
|
| 29 |
+
|
| 30 |
+
assert "Hello world!" in decoded, f"Round-trip failed: {decoded}"
|
| 31 |
+
print(f" Encoded: {ids}")
|
| 32 |
+
print(f" Decoded: {decoded}")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_tokenizer_special_tokens():
|
| 36 |
+
"""Test special token IDs."""
|
| 37 |
+
assert BOS_ID == 1
|
| 38 |
+
assert EOS_ID == 2
|
| 39 |
+
assert PAD_ID == 0
|
| 40 |
+
assert UNK_ID == 3
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_tokenizer_save_load(tmp_path=None):
|
| 44 |
+
"""Test save and load."""
|
| 45 |
+
import tempfile
|
| 46 |
+
tok = BPETokenizer(vocab_size=128)
|
| 47 |
+
tok.train("Hello world test test test. Foo bar baz.", verbose=False)
|
| 48 |
+
|
| 49 |
+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") as f:
|
| 50 |
+
path = f.name
|
| 51 |
+
tok.save(path)
|
| 52 |
+
|
| 53 |
+
tok2 = BPETokenizer.load(path)
|
| 54 |
+
assert tok2.actual_vocab_size == tok.actual_vocab_size
|
| 55 |
+
assert tok2.merges == tok.merges
|
| 56 |
+
|
| 57 |
+
# Clean up
|
| 58 |
+
os.unlink(path)
|
| 59 |
+
print(f" Save/load OK ({tok.actual_vocab_size} tokens)")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
print("Running tokenizer tests...")
|
| 64 |
+
test_tokenizer_basic()
|
| 65 |
+
print(" ✓ test_tokenizer_basic")
|
| 66 |
+
test_tokenizer_roundtrip()
|
| 67 |
+
print(" ✓ test_tokenizer_roundtrip")
|
| 68 |
+
test_tokenizer_special_tokens()
|
| 69 |
+
print(" ✓ test_tokenizer_special_tokens")
|
| 70 |
+
test_tokenizer_save_load()
|
| 71 |
+
print(" ✓ test_tokenizer_save_load")
|
| 72 |
+
print("\nAll tokenizer tests passed!")
|
tests/test_tools_vault.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test zero-limitation tools, code editing, and storage vault."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 7 |
+
|
| 8 |
+
from splitbit_llm.harness.tools import (
|
| 9 |
+
ToolRegistry, get_default_tools, tool_loop, parse_tool_calls,
|
| 10 |
+
_tool_shell_exec, _tool_write_file, _tool_read_file,
|
| 11 |
+
_tool_code_edit, _tool_list_dir, _tool_make_dir, _tool_delete_file,
|
| 12 |
+
)
|
| 13 |
+
from splitbit_llm.storage.vault import StorageVault
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_full_terminal_control():
|
| 17 |
+
"""Test that shell_exec has zero limitations — can run any command."""
|
| 18 |
+
# Test a basic command
|
| 19 |
+
result = _tool_shell_exec("echo hello world")
|
| 20 |
+
assert "hello world" in result, f"Expected 'hello world' in result: {result}"
|
| 21 |
+
print(f" echo: {result.strip()}")
|
| 22 |
+
|
| 23 |
+
# Test that no commands are blocked
|
| 24 |
+
result2 = _tool_shell_exec("python --version")
|
| 25 |
+
assert "Error: dangerous command blocked" not in result2
|
| 26 |
+
print(f" python --version: {result2.strip()}")
|
| 27 |
+
|
| 28 |
+
# Test pip command (would be blocked by old safety filter)
|
| 29 |
+
result3 = _tool_shell_exec("echo pip install numpy")
|
| 30 |
+
assert "dangerous" not in result3.lower()
|
| 31 |
+
print(f" pip command: not blocked")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_code_edit_tool():
|
| 35 |
+
"""Test code_edit tool — can modify files including its own framework."""
|
| 36 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
| 37 |
+
f.write("value = 'old'\nprint(value)\n")
|
| 38 |
+
path = f.name
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
# Read original
|
| 42 |
+
content = _tool_read_file(path)
|
| 43 |
+
assert "old" in content
|
| 44 |
+
|
| 45 |
+
# Edit the file
|
| 46 |
+
result = _tool_code_edit(f'"{path}" "old" "new"')
|
| 47 |
+
assert "Replaced" in result, f"Expected replacement: {result}"
|
| 48 |
+
print(f" Edit result: {result}")
|
| 49 |
+
|
| 50 |
+
# Verify change
|
| 51 |
+
content = _tool_read_file(path)
|
| 52 |
+
assert "new" in content
|
| 53 |
+
assert "old" not in content
|
| 54 |
+
print(f" Verified: old → new")
|
| 55 |
+
|
| 56 |
+
finally:
|
| 57 |
+
os.unlink(path)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_list_dir_tool():
|
| 61 |
+
"""Test list_dir tool."""
|
| 62 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 63 |
+
# Create some files
|
| 64 |
+
open(os.path.join(tmpdir, "file1.txt"), "w").close()
|
| 65 |
+
open(os.path.join(tmpdir, "file2.py"), "w").close()
|
| 66 |
+
os.makedirs(os.path.join(tmpdir, "subdir"))
|
| 67 |
+
|
| 68 |
+
result = _tool_list_dir(tmpdir)
|
| 69 |
+
assert "file1.txt" in result
|
| 70 |
+
assert "file2.py" in result
|
| 71 |
+
assert "subdir/" in result
|
| 72 |
+
print(f" Listed: {result.strip()}")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def test_make_dir_tool():
|
| 76 |
+
"""Test make_dir tool."""
|
| 77 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 78 |
+
new_dir = os.path.join(tmpdir, "new_project", "src")
|
| 79 |
+
result = _tool_make_dir(new_dir)
|
| 80 |
+
assert "Created" in result
|
| 81 |
+
assert os.path.exists(new_dir)
|
| 82 |
+
print(f" Created: {new_dir}")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_delete_file_tool():
|
| 86 |
+
"""Test delete_file tool."""
|
| 87 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 88 |
+
# Test file deletion
|
| 89 |
+
file_path = os.path.join(tmpdir, "temp.txt")
|
| 90 |
+
open(file_path, "w").close()
|
| 91 |
+
assert os.path.exists(file_path)
|
| 92 |
+
result = _tool_delete_file(file_path)
|
| 93 |
+
assert "Deleted file" in result
|
| 94 |
+
assert not os.path.exists(file_path)
|
| 95 |
+
|
| 96 |
+
# Test directory deletion
|
| 97 |
+
dir_path = os.path.join(tmpdir, "temp_dir")
|
| 98 |
+
os.makedirs(dir_path)
|
| 99 |
+
result2 = _tool_delete_file(dir_path)
|
| 100 |
+
assert "Deleted directory" in result2
|
| 101 |
+
assert not os.path.exists(dir_path)
|
| 102 |
+
print(f" Deleted file and directory: OK")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def test_zero_limitation_tools_count():
|
| 106 |
+
"""Test that all zero-limitation tools are registered."""
|
| 107 |
+
tools = get_default_tools()
|
| 108 |
+
tool_names = [t.name for t in tools]
|
| 109 |
+
assert "shell_exec" in tool_names
|
| 110 |
+
assert "write_file" in tool_names
|
| 111 |
+
assert "code_edit" in tool_names
|
| 112 |
+
assert "list_dir" in tool_names
|
| 113 |
+
assert "make_dir" in tool_names
|
| 114 |
+
assert "delete_file" in tool_names
|
| 115 |
+
assert "read_file" in tool_names
|
| 116 |
+
assert "calculate" in tool_names
|
| 117 |
+
print(f" Tools: {len(tools)} registered — {tool_names}")
|
| 118 |
+
|
| 119 |
+
# Check shell_exec description says full control
|
| 120 |
+
shell_tool = next(t for t in tools if t.name == "shell_exec")
|
| 121 |
+
assert "full control" in shell_tool.description.lower() or "no restriction" in shell_tool.description.lower()
|
| 122 |
+
print(f" shell_exec: {shell_tool.description}")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def test_storage_vault():
|
| 126 |
+
"""Test mass storage vault — auto-resizing storage."""
|
| 127 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 128 |
+
vault = StorageVault(data_dir=tmpdir)
|
| 129 |
+
|
| 130 |
+
# Check directories created
|
| 131 |
+
assert os.path.exists(vault.db_dir)
|
| 132 |
+
assert os.path.exists(vault.cache_dir)
|
| 133 |
+
assert os.path.exists(vault.archive_dir)
|
| 134 |
+
assert os.path.exists(vault.artifacts_dir)
|
| 135 |
+
print(f" Directories: db, cache, archive, artifacts — all created")
|
| 136 |
+
|
| 137 |
+
# Store and load artifact
|
| 138 |
+
path = vault.store_artifact("test_code.py", b"print('hello')")
|
| 139 |
+
assert os.path.exists(path)
|
| 140 |
+
loaded = vault.load_artifact("test_code.py")
|
| 141 |
+
assert loaded == b"print('hello')"
|
| 142 |
+
print(f" Artifact stored and loaded: {loaded}")
|
| 143 |
+
|
| 144 |
+
# Store text artifact
|
| 145 |
+
path2 = vault.store_artifact_text("notes.txt", "Important notes")
|
| 146 |
+
loaded2 = vault.load_artifact("notes.txt")
|
| 147 |
+
assert loaded2 == b"Important notes"
|
| 148 |
+
print(f" Text artifact: {loaded2}")
|
| 149 |
+
|
| 150 |
+
# List artifacts
|
| 151 |
+
artifacts = vault.list_artifacts()
|
| 152 |
+
assert len(artifacts) == 2
|
| 153 |
+
names = [a["name"] for a in artifacts]
|
| 154 |
+
assert "test_code.py" in names
|
| 155 |
+
assert "notes.txt" in names
|
| 156 |
+
print(f" Artifacts listed: {names}")
|
| 157 |
+
|
| 158 |
+
# Delete artifact
|
| 159 |
+
deleted = vault.delete_artifact("test_code.py")
|
| 160 |
+
assert deleted is True
|
| 161 |
+
artifacts = vault.list_artifacts()
|
| 162 |
+
assert len(artifacts) == 1
|
| 163 |
+
print(f" Deleted: test_code.py ({len(artifacts)} remaining)")
|
| 164 |
+
|
| 165 |
+
# Stats
|
| 166 |
+
stats = vault.get_stats()
|
| 167 |
+
assert stats["disk_total_gb"] > 0
|
| 168 |
+
assert "total_storage_mb" in stats
|
| 169 |
+
assert "disk_usage_percent" in stats
|
| 170 |
+
print(f" Stats: {stats['total_storage_mb']}MB used, "
|
| 171 |
+
f"{stats['disk_free_gb']}GB free, {stats['disk_usage_percent']:.1%} disk usage")
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_vault_auto_resize():
|
| 175 |
+
"""Test vault auto-resize and cleanup."""
|
| 176 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 177 |
+
vault = StorageVault(data_dir=tmpdir)
|
| 178 |
+
|
| 179 |
+
# Store many artifacts
|
| 180 |
+
for i in range(20):
|
| 181 |
+
vault.store_artifact_text(f"artifact_{i}.txt", f"content {i}" * 100)
|
| 182 |
+
|
| 183 |
+
stats = vault.get_stats()
|
| 184 |
+
assert stats["artifacts_storage_bytes"] > 0
|
| 185 |
+
print(f" Stored 20 artifacts: {stats['artifacts_storage_bytes']} bytes")
|
| 186 |
+
|
| 187 |
+
# Force cleanup (should not delete recent files)
|
| 188 |
+
result = vault.force_cleanup()
|
| 189 |
+
assert "cleanups_performed" in result
|
| 190 |
+
# Recent files should still be there
|
| 191 |
+
artifacts = vault.list_artifacts()
|
| 192 |
+
assert len(artifacts) == 20 # nothing deleted (all recent)
|
| 193 |
+
print(f" Cleanup: {result['cleanups_performed']} cycles, {len(artifacts)} artifacts remain")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def test_vault_db_paths():
|
| 197 |
+
"""Test vault DB path management."""
|
| 198 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 199 |
+
vault = StorageVault(data_dir=tmpdir)
|
| 200 |
+
|
| 201 |
+
db_path = vault.get_db_path("memory")
|
| 202 |
+
assert db_path.endswith("memory.db")
|
| 203 |
+
assert "db" in db_path
|
| 204 |
+
|
| 205 |
+
cache_path = vault.get_cache_path("test.cache")
|
| 206 |
+
assert "cache" in cache_path
|
| 207 |
+
|
| 208 |
+
artifact_path = vault.get_artifact_path("code.py")
|
| 209 |
+
assert "artifacts" in artifact_path
|
| 210 |
+
print(f" DB path: {db_path}")
|
| 211 |
+
print(f" Cache path: {cache_path}")
|
| 212 |
+
print(f" Artifact path: {artifact_path}")
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def test_tool_loop_with_new_tools():
|
| 216 |
+
"""Test that tool_loop works with the new tools."""
|
| 217 |
+
registry = ToolRegistry()
|
| 218 |
+
for tool in get_default_tools():
|
| 219 |
+
registry.register(tool)
|
| 220 |
+
|
| 221 |
+
# Test list_dir via tool loop
|
| 222 |
+
text = '[TOOL: list_dir(".")]'
|
| 223 |
+
result_text, results = tool_loop(text, registry)
|
| 224 |
+
assert len(results) > 0
|
| 225 |
+
assert results[0].success
|
| 226 |
+
print(f" Tool loop: {results[0].name} → {results[0].output[:50]}...")
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
if __name__ == "__main__":
|
| 230 |
+
print("Running zero-limitation tools and storage vault tests...")
|
| 231 |
+
test_full_terminal_control()
|
| 232 |
+
print(" ✓ test_full_terminal_control")
|
| 233 |
+
test_code_edit_tool()
|
| 234 |
+
print(" ✓ test_code_edit_tool")
|
| 235 |
+
test_list_dir_tool()
|
| 236 |
+
print(" ✓ test_list_dir_tool")
|
| 237 |
+
test_make_dir_tool()
|
| 238 |
+
print(" ✓ test_make_dir_tool")
|
| 239 |
+
test_delete_file_tool()
|
| 240 |
+
print(" ✓ test_delete_file_tool")
|
| 241 |
+
test_zero_limitation_tools_count()
|
| 242 |
+
print(" ✓ test_zero_limitation_tools_count")
|
| 243 |
+
test_storage_vault()
|
| 244 |
+
print(" ✓ test_storage_vault")
|
| 245 |
+
test_vault_auto_resize()
|
| 246 |
+
print(" ✓ test_vault_auto_resize")
|
| 247 |
+
test_vault_db_paths()
|
| 248 |
+
print(" ✓ test_vault_db_paths")
|
| 249 |
+
test_tool_loop_with_new_tools()
|
| 250 |
+
print(" ✓ test_tool_loop_with_new_tools")
|
| 251 |
+
print("\nAll zero-limitation & vault tests passed!")
|
tests/test_voice.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test voice components: TTS formatter, voice adapter, self-improvement."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
| 6 |
+
|
| 7 |
+
from splitbit_llm.voice.tts_output import TTSOutputFormatter, number_to_words
|
| 8 |
+
from splitbit_llm.voice.voice_adapter import VoiceAdapter
|
| 9 |
+
from splitbit_llm.voice.self_improve import SelfImprovementEngine
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_tts_formatter():
|
| 13 |
+
"""Test TTS output formatting."""
|
| 14 |
+
fmt = TTSOutputFormatter()
|
| 15 |
+
|
| 16 |
+
# Test markdown stripping
|
| 17 |
+
result = fmt.format("**Hello** world! This is *italic* text.")
|
| 18 |
+
assert "**" not in result, f"Bold not stripped: {result}"
|
| 19 |
+
assert "*" not in result, f"Italic not stripped: {result}"
|
| 20 |
+
print(f" Markdown stripped: {result}")
|
| 21 |
+
|
| 22 |
+
# Test code block stripping
|
| 23 |
+
result = fmt.format("Here is code: `print('hello')` and ```python\nx=1\n```")
|
| 24 |
+
assert "```" not in result, f"Code block not stripped: {result}"
|
| 25 |
+
print(f" Code stripped: {result}")
|
| 26 |
+
|
| 27 |
+
# Test abbreviation expansion
|
| 28 |
+
result = fmt.format("The API and URL are working.")
|
| 29 |
+
assert "A P I" in result, f"Abbreviation not expanded: {result}"
|
| 30 |
+
print(f" Abbreviations expanded: {result}")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_number_to_words():
|
| 34 |
+
"""Test number to words conversion."""
|
| 35 |
+
assert number_to_words(0) == "zero"
|
| 36 |
+
assert number_to_words(1) == "one"
|
| 37 |
+
assert number_to_words(10) == "ten"
|
| 38 |
+
assert number_to_words(15) == "fifteen"
|
| 39 |
+
assert number_to_words(42) == "forty two"
|
| 40 |
+
assert number_to_words(100) == "one hundred"
|
| 41 |
+
assert "one thousand" in number_to_words(1000)
|
| 42 |
+
print(f" 42 → '{number_to_words(42)}'")
|
| 43 |
+
print(f" 100 → '{number_to_words(100)}'")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_number_normalization():
|
| 47 |
+
"""Test number normalization in TTS formatter."""
|
| 48 |
+
fmt = TTSOutputFormatter()
|
| 49 |
+
result = fmt.format("I have 100 apples and 42 oranges.")
|
| 50 |
+
assert "one hundred" in result, f"Number not normalized: {result}"
|
| 51 |
+
assert "forty two" in result, f"Number not normalized: {result}"
|
| 52 |
+
print(f" Numbers normalized: {result}")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_voice_adapter():
|
| 56 |
+
"""Test voice adapter sentence splitting."""
|
| 57 |
+
adapter = VoiceAdapter()
|
| 58 |
+
|
| 59 |
+
def mock_stream():
|
| 60 |
+
yield "Hello "
|
| 61 |
+
yield "there. "
|
| 62 |
+
yield "How are "
|
| 63 |
+
yield "you? "
|
| 64 |
+
yield "I am fine!"
|
| 65 |
+
|
| 66 |
+
sentences = list(adapter.stream_sentences(mock_stream()))
|
| 67 |
+
assert len(sentences) >= 2, f"Expected 2+ sentences, got {len(sentences)}"
|
| 68 |
+
print(f" Sentences: {sentences}")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_self_improvement():
|
| 72 |
+
"""Test self-improvement engine."""
|
| 73 |
+
engine = SelfImprovementEngine(model=None, tokenizer=None)
|
| 74 |
+
|
| 75 |
+
# Record conversations
|
| 76 |
+
engine.record_conversation("Hello", "Hi there!", confidence=0.8)
|
| 77 |
+
engine.record_conversation("What is AI?", "AI is artificial intelligence.", confidence=0.6)
|
| 78 |
+
engine.record_conversation("How to code?", "Start with Python basics.", confidence=0.7)
|
| 79 |
+
|
| 80 |
+
stats = engine.get_stats()
|
| 81 |
+
assert stats["conversations_learned"] == 3, f"Wrong count: {stats['conversations_learned']}"
|
| 82 |
+
assert stats["avg_confidence"] > 0.5, f"Low confidence: {stats['avg_confidence']}"
|
| 83 |
+
print(f" Conversations: {stats['conversations_learned']}")
|
| 84 |
+
print(f" Avg confidence: {stats['avg_confidence']:.2f}")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def test_self_talk():
|
| 88 |
+
"""Test self-talk generation."""
|
| 89 |
+
engine = SelfImprovementEngine(model=None, tokenizer=None)
|
| 90 |
+
|
| 91 |
+
def mock_generate(prompt):
|
| 92 |
+
if "Ask a question" in prompt:
|
| 93 |
+
return "What is machine learning?"
|
| 94 |
+
return "Machine learning is a subset of AI that learns from data."
|
| 95 |
+
|
| 96 |
+
pairs = engine.self_talk(mock_generate, max_rounds=1)
|
| 97 |
+
assert len(pairs) >= 0, "Self-talk returned None"
|
| 98 |
+
print(f" Self-talk pairs: {len(pairs)}")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
print("Running voice tests...")
|
| 103 |
+
test_tts_formatter()
|
| 104 |
+
print(" ✓ test_tts_formatter")
|
| 105 |
+
test_number_to_words()
|
| 106 |
+
print(" ✓ test_number_to_words")
|
| 107 |
+
test_number_normalization()
|
| 108 |
+
print(" ✓ test_number_normalization")
|
| 109 |
+
test_voice_adapter()
|
| 110 |
+
print(" ✓ test_voice_adapter")
|
| 111 |
+
test_self_improvement()
|
| 112 |
+
print(" ✓ test_self_improvement")
|
| 113 |
+
test_self_talk()
|
| 114 |
+
print(" ✓ test_self_talk")
|
| 115 |
+
print("\nAll voice tests passed!")
|