Spaces:
Sleeping
Sleeping
| import pytest | |
| from unittest.mock import patch, MagicMock | |
| from src.utils.token_optimizer import ( | |
| TokenOptimizer, | |
| _ApproximateEncoder, | |
| get_token_optimizer, | |
| reset_token_optimizer, | |
| ) | |
| class TestTokenOptimizerInit: | |
| def test_default_max_tokens(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._max_tokens == 4000 | |
| def test_custom_max_tokens(self): | |
| optimizer = TokenOptimizer(max_tokens=2000) | |
| assert optimizer._max_tokens == 2000 | |
| def test_encoder_lazy_loaded(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._encoder is None | |
| def test_encoder_loaded_on_access(self): | |
| optimizer = TokenOptimizer() | |
| enc = optimizer.encoder | |
| assert enc is not None | |
| class TestCountTokens: | |
| def test_empty_string_returns_zero(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer.count_tokens("") == 0 | |
| def test_none_returns_zero(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer.count_tokens(None) == 0 | |
| def test_simple_text_returns_positive(self): | |
| optimizer = TokenOptimizer() | |
| count = optimizer.count_tokens("Hello world this is a test") | |
| assert count > 0 | |
| def test_longer_text_more_tokens(self): | |
| optimizer = TokenOptimizer() | |
| short = optimizer.count_tokens("hello") | |
| long = optimizer.count_tokens("hello world this is a much longer text with many words") | |
| assert long > short | |
| def test_consistent_results(self): | |
| optimizer = TokenOptimizer() | |
| text = "consistent token counting" | |
| assert optimizer.count_tokens(text) == optimizer.count_tokens(text) | |
| def test_turkish_text(self): | |
| optimizer = TokenOptimizer() | |
| count = optimizer.count_tokens("Satışları göster ve toplam tutarı hesapla") | |
| assert count > 0 | |
| class TestTruncateChatHistory: | |
| def test_empty_messages_returns_empty(self): | |
| optimizer = TokenOptimizer() | |
| result, tokens = optimizer.truncate_chat_history([]) | |
| assert result == [] | |
| assert tokens == 0 | |
| def test_short_history_unchanged(self): | |
| optimizer = TokenOptimizer() | |
| messages = [ | |
| {"role": "user", "content": "hello"}, | |
| {"role": "assistant", "content": "hi there"}, | |
| ] | |
| result, tokens = optimizer.truncate_chat_history(messages, max_tokens=500) | |
| assert len(result) == 2 | |
| assert tokens > 0 | |
| def test_truncation_keeps_recent_messages(self): | |
| optimizer = TokenOptimizer() | |
| messages = [ | |
| {"role": "user", "content": "old message " * 200}, | |
| {"role": "assistant", "content": "old reply " * 200}, | |
| {"role": "user", "content": "recent question"}, | |
| {"role": "assistant", "content": "recent answer"}, | |
| ] | |
| result, tokens = optimizer.truncate_chat_history(messages, max_tokens=50) | |
| assert any("recent" in m.get("content", "") for m in result) | |
| def test_never_drops_last_message(self): | |
| optimizer = TokenOptimizer() | |
| messages = [ | |
| {"role": "user", "content": "very " * 500}, | |
| ] | |
| result, tokens = optimizer.truncate_chat_history(messages, max_tokens=10) | |
| assert len(result) >= 1 | |
| def test_preserves_message_pairs(self): | |
| optimizer = TokenOptimizer() | |
| messages = [ | |
| {"role": "user", "content": "q1"}, | |
| {"role": "assistant", "content": "a1"}, | |
| {"role": "user", "content": "q2"}, | |
| {"role": "assistant", "content": "a2"}, | |
| ] | |
| result, _ = optimizer.truncate_chat_history(messages, max_tokens=500) | |
| roles = [m["role"] for m in result] | |
| if len(roles) >= 2: | |
| assert roles[0] == "user" or (roles[0] == "assistant" and len(result) == len(messages)) | |
| def test_respects_token_budget(self): | |
| optimizer = TokenOptimizer() | |
| messages = [{"role": "user", "content": f"message number {i} " * 20} for i in range(20)] | |
| result, tokens = optimizer.truncate_chat_history(messages, max_tokens=100) | |
| assert tokens <= 200 | |
| class TestTruncateContext: | |
| def test_empty_context(self): | |
| optimizer = TokenOptimizer() | |
| result, tokens = optimizer.truncate_context("") | |
| assert result == "" | |
| assert tokens == 0 | |
| def test_short_context_unchanged(self): | |
| optimizer = TokenOptimizer() | |
| text = "Short context text." | |
| result, tokens = optimizer.truncate_context(text, max_tokens=500) | |
| assert result == text | |
| def test_long_context_truncated(self): | |
| optimizer = TokenOptimizer() | |
| text = "This is a sentence. " * 500 | |
| result, tokens = optimizer.truncate_context(text, max_tokens=50) | |
| assert tokens <= 60 | |
| assert result.endswith("...") | |
| def test_preserve_start_true(self): | |
| optimizer = TokenOptimizer() | |
| text = "First sentence. Middle sentence. Last sentence." | |
| result, _ = optimizer.truncate_context(text, max_tokens=10, preserve_start=True) | |
| assert "First" in result | |
| def test_preserve_start_false(self): | |
| optimizer = TokenOptimizer() | |
| text = "First sentence here. " * 10 + "Last sentence here." | |
| result, _ = optimizer.truncate_context(text, max_tokens=15, preserve_start=False) | |
| assert result.startswith("...") | |
| def test_single_long_sentence_hard_truncated(self): | |
| optimizer = TokenOptimizer() | |
| text = "word " * 1000 | |
| result, tokens = optimizer.truncate_context(text, max_tokens=20) | |
| assert tokens <= 30 | |
| class TestOptimizeSchemaContext: | |
| def test_empty_schema(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer.optimize_schema_context("") == "" | |
| def test_short_schema_unchanged(self): | |
| optimizer = TokenOptimizer() | |
| schema = "Table: users\nColumns: id, name" | |
| result = optimizer.optimize_schema_context(schema, max_tokens=500) | |
| assert result == schema | |
| def test_prioritizes_relevant_tables(self): | |
| optimizer = TokenOptimizer() | |
| schema = """Table: customers | |
| Columns: id, name, city | |
| Table: products | |
| Columns: id, name, price | |
| Table: sales | |
| Columns: id, product_id, amount""" | |
| result = optimizer.optimize_schema_context(schema, relevant_tables=["sales"], max_tokens=50) | |
| assert "sales" in result | |
| def test_truncates_when_over_budget(self): | |
| optimizer = TokenOptimizer() | |
| schema = "\n\n".join([f"Table: table_{i}\nColumns: " + ", ".join([f"col_{j}" for j in range(20)]) for i in range(50)]) | |
| result = optimizer.optimize_schema_context(schema, max_tokens=100) | |
| assert optimizer.count_tokens(result) <= 120 | |
| def test_no_relevant_tables_keeps_order(self): | |
| optimizer = TokenOptimizer() | |
| schema = "Table: alpha\nColumns: a\n\nTable: beta\nColumns: b" | |
| result = optimizer.optimize_schema_context(schema, max_tokens=500) | |
| assert "alpha" in result | |
| assert "beta" in result | |
| class TestParseTableBlocks: | |
| def test_parses_multiple_tables(self): | |
| optimizer = TokenOptimizer() | |
| schema = "Table: users\nColumns: id, name\n\nTable: orders\nColumns: id, user_id" | |
| blocks = optimizer._parse_table_blocks(schema) | |
| assert len(blocks) == 2 | |
| assert blocks[0][0] == "users" | |
| assert blocks[1][0] == "orders" | |
| def test_empty_schema_returns_empty(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._parse_table_blocks("") == [] | |
| def test_single_table(self): | |
| optimizer = TokenOptimizer() | |
| schema = "Table: users\nColumns: id, name" | |
| blocks = optimizer._parse_table_blocks(schema) | |
| assert len(blocks) == 1 | |
| def test_no_table_prefix_returns_empty(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._parse_table_blocks("just some text") == [] | |
| class TestSelectFewShotExamples: | |
| def test_empty_examples(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer.select_few_shot_examples([], "query") == [] | |
| def test_selects_relevant_examples(self): | |
| optimizer = TokenOptimizer() | |
| examples = [ | |
| {"query": "show total sales amount", "sql": "SELECT SUM(amount) FROM sales"}, | |
| {"query": "list all customers", "sql": "SELECT * FROM customers"}, | |
| {"query": "calculate average sales price", "sql": "SELECT AVG(price) FROM sales"}, | |
| ] | |
| result = optimizer.select_few_shot_examples(examples, "total sales", max_tokens=500) | |
| assert len(result) > 0 | |
| assert any("sales" in ex.get("query", "") and "total" in ex.get("query", "") for ex in result) | |
| def test_respects_token_budget(self): | |
| optimizer = TokenOptimizer() | |
| examples = [{"query": f"example {i} " * 50, "sql": f"SELECT {i}"} for i in range(20)] | |
| result = optimizer.select_few_shot_examples(examples, "example 1", max_tokens=100) | |
| total = sum(optimizer.count_tokens(optimizer._example_text(ex)) for ex in result) | |
| assert total <= 200 | |
| def test_ranks_by_keyword_similarity(self): | |
| optimizer = TokenOptimizer() | |
| examples = [ | |
| {"query": "customer list", "sql": "SELECT * FROM customers"}, | |
| {"query": "sales report total", "sql": "SELECT SUM(total) FROM sales"}, | |
| ] | |
| result = optimizer.select_few_shot_examples(examples, "sales total report", max_tokens=500) | |
| assert result[0]["query"] == "sales report total" | |
| class TestKeywordSimilarity: | |
| def test_identical_text(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._keyword_similarity("hello world", "hello world") == 1.0 | |
| def test_no_overlap(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._keyword_similarity("hello", "goodbye") == 0.0 | |
| def test_partial_overlap(self): | |
| optimizer = TokenOptimizer() | |
| score = optimizer._keyword_similarity("hello world", "hello there") | |
| assert 0 < score < 1 | |
| def test_empty_query(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._keyword_similarity("", "hello") == 0.0 | |
| def test_case_insensitive(self): | |
| optimizer = TokenOptimizer() | |
| assert optimizer._keyword_similarity("Hello", "hello") == 1.0 | |
| class TestOptimizeForLlmCall: | |
| def test_all_components_present(self): | |
| optimizer = TokenOptimizer() | |
| components = { | |
| "query": "what are total sales?", | |
| "system_prompt": "you are a sql assistant", | |
| "schema": "Table: sales\nColumns: id, amount", | |
| "few_shots": "Example: SELECT COUNT(*) FROM sales", | |
| "chat_history": "user: show sales\nassistant: here are sales", | |
| } | |
| result = optimizer.optimize_for_llm_call(components, total_budget=3000) | |
| assert "query" in result | |
| assert "system_prompt" in result | |
| assert "schema" in result | |
| assert "few_shots" in result | |
| assert "chat_history" in result | |
| def test_query_preserved(self): | |
| optimizer = TokenOptimizer() | |
| components = {"query": "full query text", "system_prompt": "sp", "schema": "s" * 5000} | |
| result = optimizer.optimize_for_llm_call(components, total_budget=100) | |
| assert result["query"] == "full query text" | |
| def test_system_prompt_preserved(self): | |
| optimizer = TokenOptimizer() | |
| components = {"query": "q", "system_prompt": "system prompt text", "schema": "s" * 5000} | |
| result = optimizer.optimize_for_llm_call(components, total_budget=100) | |
| assert result["system_prompt"] == "system prompt text" | |
| def test_empty_components(self): | |
| optimizer = TokenOptimizer() | |
| result = optimizer.optimize_for_llm_call({}, total_budget=1000) | |
| assert result["query"] == "" | |
| assert result["system_prompt"] == "" | |
| def test_large_schema_truncated(self): | |
| optimizer = TokenOptimizer() | |
| components = { | |
| "query": "q", | |
| "system_prompt": "sp", | |
| "schema": "Table: big\nColumns: " + ", ".join([f"col_{i}" for i in range(500)]), | |
| } | |
| result = optimizer.optimize_for_llm_call(components, total_budget=100) | |
| assert optimizer.count_tokens(result["schema"]) < optimizer.count_tokens(components["schema"]) | |
| class TestGetOptimizationStats: | |
| def test_stats_structure(self): | |
| optimizer = TokenOptimizer() | |
| original = {"query": "hello world", "schema": "big " * 100} | |
| optimized = {"query": "hello world", "schema": "big"} | |
| stats = optimizer.get_optimization_stats(original, optimized) | |
| assert "original_tokens" in stats | |
| assert "optimized_tokens" in stats | |
| assert "tokens_saved" in stats | |
| assert "reduction_ratio" in stats | |
| def test_reduction_calculated(self): | |
| optimizer = TokenOptimizer() | |
| original = {"text": "word " * 100} | |
| optimized = {"text": "word " * 10} | |
| stats = optimizer.get_optimization_stats(original, optimized) | |
| assert stats["tokens_saved"] > 0 | |
| assert stats["reduction_ratio"] > 0 | |
| def test_no_reduction(self): | |
| optimizer = TokenOptimizer() | |
| data = {"text": "same"} | |
| stats = optimizer.get_optimization_stats(data, data) | |
| assert stats["tokens_saved"] == 0 | |
| assert stats["reduction_ratio"] == 0.0 | |
| def test_empty_input(self): | |
| optimizer = TokenOptimizer() | |
| stats = optimizer.get_optimization_stats({}, {}) | |
| assert stats["original_tokens"] == 0 | |
| class TestApproximateEncoder: | |
| def test_encode_returns_list(self): | |
| enc = _ApproximateEncoder() | |
| result = enc.encode("hello world") | |
| assert isinstance(result, list) | |
| def test_decode_returns_string(self): | |
| enc = _ApproximateEncoder() | |
| result = enc.decode([1, 2, 3]) | |
| assert isinstance(result, str) | |
| def test_approximate_proportional(self): | |
| enc = _ApproximateEncoder() | |
| short = len(enc.encode("hi")) | |
| long = len(enc.encode("hello world this is longer")) | |
| assert long > short | |
| class TestSingleton: | |
| def setup_method(self): | |
| reset_token_optimizer() | |
| def teardown_method(self): | |
| reset_token_optimizer() | |
| def test_get_returns_instance(self): | |
| optimizer = get_token_optimizer() | |
| assert isinstance(optimizer, TokenOptimizer) | |
| def test_get_returns_same_instance(self): | |
| first = get_token_optimizer() | |
| second = get_token_optimizer() | |
| assert first is second | |
| def test_reset_clears_instance(self): | |
| first = get_token_optimizer() | |
| reset_token_optimizer() | |
| second = get_token_optimizer() | |
| assert first is not second | |
| def test_get_with_custom_max_tokens(self): | |
| optimizer = get_token_optimizer(max_tokens=2000) | |
| assert optimizer._max_tokens == 2000 | |
| class TestExampleText: | |
| def test_extracts_query_and_sql(self): | |
| optimizer = TokenOptimizer() | |
| ex = {"query": "show sales", "sql": "SELECT * FROM sales"} | |
| text = optimizer._example_text(ex) | |
| assert "show sales" in text | |
| assert "SELECT * FROM sales" in text | |
| def test_handles_missing_keys(self): | |
| optimizer = TokenOptimizer() | |
| ex = {"other": "data"} | |
| text = optimizer._example_text(ex) | |
| assert len(text) > 0 | |
| def test_empty_example(self): | |
| optimizer = TokenOptimizer() | |
| ex = {} | |
| text = optimizer._example_text(ex) | |
| assert isinstance(text, str) | |
| class TestEdgeCases: | |
| def test_count_tokens_with_special_characters(self): | |
| optimizer = TokenOptimizer() | |
| count = optimizer.count_tokens("!@#$%^&*()_+{}|:<>?") | |
| assert count > 0 | |
| def test_truncate_context_single_word(self): | |
| optimizer = TokenOptimizer() | |
| result, _ = optimizer.truncate_context("word", max_tokens=500) | |
| assert result == "word" | |
| def test_schema_with_no_table_prefix(self): | |
| optimizer = TokenOptimizer() | |
| result = optimizer.optimize_schema_context("just plain text", max_tokens=500) | |
| assert result == "just plain text" | |
| def test_few_shot_with_single_example(self): | |
| optimizer = TokenOptimizer() | |
| examples = [{"query": "test", "sql": "SELECT 1"}] | |
| result = optimizer.select_few_shot_examples(examples, "test", max_tokens=500) | |
| assert len(result) == 1 | |
| def test_optimize_llm_call_missing_optional_components(self): | |
| optimizer = TokenOptimizer() | |
| components = {"query": "hello"} | |
| result = optimizer.optimize_for_llm_call(components, total_budget=1000) | |
| assert result["schema"] == "" | |
| assert result["few_shots"] == "" | |
| assert result["chat_history"] == "" | |
| def test_truncate_history_with_empty_content(self): | |
| optimizer = TokenOptimizer() | |
| messages = [{"role": "user", "content": ""}, {"role": "assistant", "content": "reply"}] | |
| result, _ = optimizer.truncate_chat_history(messages, max_tokens=500) | |
| assert len(result) == 2 | |
| def test_preserve_message_pairs_no_orphan_assistant(self): | |
| optimizer = TokenOptimizer() | |
| messages = [ | |
| {"role": "user", "content": "q1 " * 200}, | |
| {"role": "assistant", "content": "a1 " * 200}, | |
| {"role": "user", "content": "q2"}, | |
| {"role": "assistant", "content": "a2"}, | |
| ] | |
| result, _ = optimizer.truncate_chat_history(messages, max_tokens=20) | |
| if result and result[0].get("role") == "assistant": | |
| idx = messages.index(result[0]) | |
| assert idx > 0 | |
| def test_fallback_encoder_works(self, _): | |
| optimizer = TokenOptimizer() | |
| optimizer._encoder = _ApproximateEncoder() | |
| count = optimizer.count_tokens("hello world test") | |
| assert count >= 0 | |
| class TestConfigIntegration: | |
| def test_config_constants_available(self): | |
| from utils.token_optimizer import NL2SQL_MAX_CONTEXT_TOKENS, CHAT_HISTORY_MAX_TOKENS, SCHEMA_CONTEXT_MAX_TOKENS, FEW_SHOT_MAX_TOKENS | |
| assert NL2SQL_MAX_CONTEXT_TOKENS > 0 | |
| assert CHAT_HISTORY_MAX_TOKENS > 0 | |
| assert SCHEMA_CONTEXT_MAX_TOKENS > 0 | |
| assert FEW_SHOT_MAX_TOKENS > 0 | |
| def test_optimizer_uses_config_defaults(self): | |
| optimizer = TokenOptimizer() | |
| messages = [{"role": "user", "content": "test"}] | |
| result, _ = optimizer.truncate_chat_history(messages) | |
| assert len(result) == 1 | |