| import pytest |
|
|
| from lightrag.exceptions import ChunkTokenLimitExceededError |
| from lightrag.operate import chunking_by_token_size |
| from lightrag.utils import Tokenizer, TokenizerInterface |
|
|
|
|
| class DummyTokenizer(TokenizerInterface): |
| """Simple 1:1 character-to-token mapping.""" |
|
|
| def encode(self, content: str): |
| return [ord(ch) for ch in content] |
|
|
| def decode(self, tokens): |
| return "".join(chr(token) for token in tokens) |
|
|
|
|
| class MultiTokenCharacterTokenizer(TokenizerInterface): |
| """ |
| Tokenizer where character-to-token ratio is non-uniform. |
| This helps catch bugs where code incorrectly counts characters instead of tokens. |
| |
| Mapping: |
| - Uppercase letters: 2 tokens each |
| - Punctuation (!, ?, .): 3 tokens each |
| - Other characters: 1 token each |
| """ |
|
|
| def encode(self, content: str): |
| tokens = [] |
| for ch in content: |
| if ch.isupper(): |
| tokens.extend([ord(ch), ord(ch) + 1000]) |
| elif ch in ["!", "?", "."]: |
| tokens.extend([ord(ch), ord(ch) + 2000, ord(ch) + 3000]) |
| else: |
| tokens.append(ord(ch)) |
| return tokens |
|
|
| def decode(self, tokens): |
| |
| result = [] |
| i = 0 |
| while i < len(tokens): |
| base_token = tokens[i] |
| |
| if ( |
| i + 2 < len(tokens) |
| and tokens[i + 1] == base_token + 2000 |
| and tokens[i + 2] == base_token + 3000 |
| ): |
| |
| result.append(chr(base_token)) |
| i += 3 |
| elif i + 1 < len(tokens) and tokens[i + 1] == base_token + 1000: |
| |
| result.append(chr(base_token)) |
| i += 2 |
| else: |
| |
| result.append(chr(base_token)) |
| i += 1 |
| return "".join(result) |
|
|
|
|
| def make_tokenizer() -> Tokenizer: |
| return Tokenizer(model_name="dummy", tokenizer=DummyTokenizer()) |
|
|
|
|
| def make_multi_token_tokenizer() -> Tokenizer: |
| return Tokenizer(model_name="multi", tokenizer=MultiTokenCharacterTokenizer()) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.offline |
| def test_split_by_character_only_within_limit(): |
| """Test chunking when all chunks are within token limit.""" |
| tokenizer = make_tokenizer() |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| "alpha\n\nbeta", |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| assert [chunk["content"] for chunk in chunks] == ["alpha", "beta"] |
|
|
|
|
| @pytest.mark.offline |
| def test_split_by_character_only_exceeding_limit_raises(): |
| """Test that oversized chunks raise ChunkTokenLimitExceededError.""" |
| tokenizer = make_tokenizer() |
| oversized = "a" * 12 |
|
|
| with pytest.raises(ChunkTokenLimitExceededError) as excinfo: |
| chunking_by_token_size( |
| tokenizer, |
| oversized, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=5, |
| ) |
|
|
| err = excinfo.value |
| assert err.chunk_tokens == len(oversized) |
| assert err.chunk_token_limit == 5 |
|
|
|
|
| @pytest.mark.offline |
| def test_chunk_error_includes_preview(): |
| """Test that error message includes chunk preview.""" |
| tokenizer = make_tokenizer() |
| oversized = "x" * 100 |
|
|
| with pytest.raises(ChunkTokenLimitExceededError) as excinfo: |
| chunking_by_token_size( |
| tokenizer, |
| oversized, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| err = excinfo.value |
| |
| assert err.chunk_preview == "x" * 80 |
| assert "Preview:" in str(err) |
|
|
|
|
| @pytest.mark.offline |
| def test_split_by_character_only_at_exact_limit(): |
| """Test chunking when chunk is exactly at token limit.""" |
| tokenizer = make_tokenizer() |
| exact_size = "a" * 10 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| exact_size, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| assert len(chunks) == 1 |
| assert chunks[0]["content"] == exact_size |
| assert chunks[0]["tokens"] == 10 |
|
|
|
|
| @pytest.mark.offline |
| def test_split_by_character_only_one_over_limit(): |
| """Test that chunk with one token over limit raises error.""" |
| tokenizer = make_tokenizer() |
| one_over = "a" * 11 |
|
|
| with pytest.raises(ChunkTokenLimitExceededError) as excinfo: |
| chunking_by_token_size( |
| tokenizer, |
| one_over, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| err = excinfo.value |
| assert err.chunk_tokens == 11 |
| assert err.chunk_token_limit == 10 |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.offline |
| def test_split_recursive_oversized_chunk(): |
| """Test recursive splitting of oversized chunk with split_by_character_only=False.""" |
| tokenizer = make_tokenizer() |
| |
| oversized = "a" * 30 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| oversized, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| |
| assert len(chunks) == 3 |
| assert all(chunk["tokens"] == 10 for chunk in chunks) |
| assert all(chunk["content"] == "a" * 10 for chunk in chunks) |
|
|
|
|
| @pytest.mark.offline |
| def test_split_with_chunk_overlap(): |
| """ |
| Test chunk splitting with overlap using distinctive content. |
| |
| With distinctive characters, we can verify overlap positions are exact. |
| Misaligned overlap would produce wrong content and fail the test. |
| """ |
| tokenizer = make_tokenizer() |
| |
| content = "0123456789abcdefghijklmno" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=3, |
| ) |
|
|
| |
| |
| assert len(chunks) == 4 |
|
|
| |
| assert chunks[0]["tokens"] == 10 |
| assert chunks[0]["content"] == "0123456789" |
|
|
| assert chunks[1]["tokens"] == 10 |
| assert chunks[1]["content"] == "789abcdefg" |
|
|
| assert chunks[2]["tokens"] == 10 |
| assert chunks[2]["content"] == "efghijklmn" |
|
|
| assert chunks[3]["tokens"] == 4 |
| assert chunks[3]["content"] == "lmno" |
|
|
|
|
| @pytest.mark.offline |
| def test_split_multiple_chunks_with_mixed_sizes(): |
| """Test splitting text with multiple chunks of different sizes.""" |
| tokenizer = make_tokenizer() |
| |
| |
| content = "small\n\n" + "a" * 16 + "\n\nmedium" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=2, |
| ) |
|
|
| |
| |
| |
| assert len(chunks) == 4 |
| assert chunks[0]["content"] == "small" |
| assert chunks[0]["tokens"] == 5 |
|
|
|
|
| @pytest.mark.offline |
| def test_split_exact_boundary(): |
| """Test splitting at exact chunk boundaries.""" |
| tokenizer = make_tokenizer() |
| |
| content = "a" * 20 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| assert len(chunks) == 2 |
| assert chunks[0]["tokens"] == 10 |
| assert chunks[1]["tokens"] == 10 |
|
|
|
|
| @pytest.mark.offline |
| def test_split_very_large_text(): |
| """Test splitting very large text into multiple chunks.""" |
| tokenizer = make_tokenizer() |
| |
| content = "a" * 100 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| assert len(chunks) == 10 |
| assert all(chunk["tokens"] == 10 for chunk in chunks) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.offline |
| def test_empty_content(): |
| """Test chunking with empty content.""" |
| tokenizer = make_tokenizer() |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| "", |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| assert len(chunks) == 1 |
| assert chunks[0]["content"] == "" |
| assert chunks[0]["tokens"] == 0 |
|
|
|
|
| @pytest.mark.offline |
| def test_single_character(): |
| """Test chunking with single character.""" |
| tokenizer = make_tokenizer() |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| "a", |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| assert len(chunks) == 1 |
| assert chunks[0]["content"] == "a" |
| assert chunks[0]["tokens"] == 1 |
|
|
|
|
| @pytest.mark.offline |
| def test_no_delimiter_in_content(): |
| """Test chunking when content has no delimiter.""" |
| tokenizer = make_tokenizer() |
| content = "a" * 30 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| |
| assert len(chunks) == 3 |
| assert all(chunk["tokens"] == 10 for chunk in chunks) |
|
|
|
|
| @pytest.mark.offline |
| def test_no_split_character(): |
| """Test chunking without split_by_character (None).""" |
| tokenizer = make_tokenizer() |
| content = "a" * 30 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character=None, |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| |
| assert len(chunks) == 3 |
| assert all(chunk["tokens"] == 10 for chunk in chunks) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.offline |
| def test_different_delimiter_newline(): |
| """Test with single newline delimiter.""" |
| tokenizer = make_tokenizer() |
| content = "alpha\nbeta\ngamma" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| assert len(chunks) == 3 |
| assert [c["content"] for c in chunks] == ["alpha", "beta", "gamma"] |
|
|
|
|
| @pytest.mark.offline |
| def test_delimiter_based_splitting_verification(): |
| """ |
| Verify that chunks are actually split at delimiter positions. |
| |
| This test ensures split_by_character truly splits at the delimiter, |
| not at arbitrary positions. |
| """ |
| tokenizer = make_tokenizer() |
|
|
| |
| content = "part1||part2||part3||part4" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="||", |
| split_by_character_only=True, |
| chunk_token_size=20, |
| ) |
|
|
| |
| assert len(chunks) == 4 |
| assert chunks[0]["content"] == "part1" |
| assert chunks[1]["content"] == "part2" |
| assert chunks[2]["content"] == "part3" |
| assert chunks[3]["content"] == "part4" |
|
|
| |
| for chunk in chunks: |
| assert "||" not in chunk["content"] |
|
|
|
|
| @pytest.mark.offline |
| def test_multi_character_delimiter_splitting(): |
| """ |
| Verify that multi-character delimiters are correctly recognized and not partially matched. |
| |
| Tests various multi-character delimiter scenarios to ensure the entire delimiter |
| sequence is used for splitting, not individual characters. |
| """ |
| tokenizer = make_tokenizer() |
|
|
| |
| content = "data<SEP>more<SEP>final" |
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="<SEP>", |
| split_by_character_only=True, |
| chunk_token_size=50, |
| ) |
|
|
| assert len(chunks) == 3 |
| assert chunks[0]["content"] == "data" |
| assert chunks[1]["content"] == "more" |
| assert chunks[2]["content"] == "final" |
| |
| for chunk in chunks: |
| assert "<SEP>" not in chunk["content"] |
|
|
| |
| content = "first><second><third" |
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="><", |
| split_by_character_only=True, |
| chunk_token_size=50, |
| ) |
|
|
| |
| assert len(chunks) == 3 |
| assert chunks[0]["content"] == "first" |
| assert chunks[1]["content"] == "second" |
| assert chunks[2]["content"] == "third" |
|
|
| |
| content = "section1[***]section2[***]section3" |
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="[***]", |
| split_by_character_only=True, |
| chunk_token_size=50, |
| ) |
|
|
| assert len(chunks) == 3 |
| assert chunks[0]["content"] == "section1" |
| assert chunks[1]["content"] == "section2" |
| assert chunks[2]["content"] == "section3" |
|
|
| |
| content = "partA...partB...partC" |
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="...", |
| split_by_character_only=True, |
| chunk_token_size=50, |
| ) |
|
|
| assert len(chunks) == 3 |
| assert chunks[0]["content"] == "partA" |
| assert chunks[1]["content"] == "partB" |
| assert chunks[2]["content"] == "partC" |
|
|
|
|
| @pytest.mark.offline |
| def test_delimiter_partial_match_not_split(): |
| """ |
| Verify that partial matches of multi-character delimiters don't cause splits. |
| |
| Only the complete delimiter sequence should trigger a split. |
| """ |
| tokenizer = make_tokenizer() |
|
|
| |
| content = "data|single||data|with|pipes||final" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="||", |
| split_by_character_only=True, |
| chunk_token_size=50, |
| ) |
|
|
| |
| assert len(chunks) == 3 |
| assert chunks[0]["content"] == "data|single" |
| assert chunks[1]["content"] == "data|with|pipes" |
| assert chunks[2]["content"] == "final" |
|
|
| |
| assert "|" in chunks[0]["content"] |
| assert "|" in chunks[1]["content"] |
| assert "||" not in chunks[0]["content"] |
| assert "||" not in chunks[1]["content"] |
|
|
|
|
| @pytest.mark.offline |
| def test_no_delimiter_forces_token_based_split(): |
| """ |
| Verify that when split_by_character doesn't appear in content, |
| chunking falls back to token-based splitting. |
| """ |
| tokenizer = make_tokenizer() |
|
|
| |
| content = "0123456789abcdefghijklmnop" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| |
| assert len(chunks) == 3 |
| assert chunks[0]["content"] == "0123456789" |
| assert chunks[1]["content"] == "abcdefghij" |
| assert chunks[2]["content"] == "klmnop" |
|
|
| |
| for chunk in chunks: |
| assert "\n\n" not in chunk["content"] |
|
|
|
|
| @pytest.mark.offline |
| def test_delimiter_at_exact_chunk_boundary(): |
| """ |
| Verify correct behavior when delimiter appears exactly at chunk token limit. |
| """ |
| tokenizer = make_tokenizer() |
|
|
| |
| content = "12345\n\nabcde" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| |
| assert len(chunks) == 2 |
| assert chunks[0]["content"] == "12345" |
| assert chunks[1]["content"] == "abcde" |
|
|
|
|
| @pytest.mark.offline |
| def test_different_delimiter_comma(): |
| """Test with comma delimiter.""" |
| tokenizer = make_tokenizer() |
| content = "one,two,three" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character=",", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| assert len(chunks) == 3 |
| assert [c["content"] for c in chunks] == ["one", "two", "three"] |
|
|
|
|
| @pytest.mark.offline |
| def test_zero_overlap(): |
| """Test with zero overlap (no overlap).""" |
| tokenizer = make_tokenizer() |
| content = "a" * 20 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character=None, |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| |
| assert len(chunks) == 2 |
| assert chunks[0]["tokens"] == 10 |
| assert chunks[1]["tokens"] == 10 |
|
|
|
|
| @pytest.mark.offline |
| def test_large_overlap(): |
| """ |
| Test with overlap close to chunk size using distinctive content. |
| |
| Large overlap (9 out of 10) means step size is only 1, creating many overlapping chunks. |
| Distinctive characters ensure each chunk has correct positioning. |
| """ |
| tokenizer = make_tokenizer() |
| |
| content = "0123456789abcdefghijklmnopqrst" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character=None, |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=9, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| assert len(chunks) == 30 |
|
|
| |
| assert chunks[0]["content"] == "0123456789" |
| assert ( |
| chunks[1]["content"] == "123456789a" |
| ) |
| assert ( |
| chunks[2]["content"] == "23456789ab" |
| ) |
| assert chunks[3]["content"] == "3456789abc" |
|
|
| |
| assert chunks[-1]["content"] == "t" |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.offline |
| def test_chunk_order_index_simple(): |
| """Test that chunk_order_index is correctly assigned.""" |
| tokenizer = make_tokenizer() |
| content = "a\n\nb\n\nc" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=10, |
| ) |
|
|
| assert len(chunks) == 3 |
| assert chunks[0]["chunk_order_index"] == 0 |
| assert chunks[1]["chunk_order_index"] == 1 |
| assert chunks[2]["chunk_order_index"] == 2 |
|
|
|
|
| @pytest.mark.offline |
| def test_chunk_order_index_with_splitting(): |
| """Test chunk_order_index with recursive splitting.""" |
| tokenizer = make_tokenizer() |
| content = "a" * 30 |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character=None, |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| assert len(chunks) == 3 |
| assert chunks[0]["chunk_order_index"] == 0 |
| assert chunks[1]["chunk_order_index"] == 1 |
| assert chunks[2]["chunk_order_index"] == 2 |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.offline |
| def test_mixed_size_chunks_no_error(): |
| """Test that mixed size chunks work without error in recursive mode.""" |
| tokenizer = make_tokenizer() |
| |
| content = "small\n\n" + "a" * 50 + "\n\nmedium" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=10, |
| chunk_overlap_token_size=2, |
| ) |
|
|
| |
| assert len(chunks) > 0 |
| |
| assert chunks[0]["content"] == "small" |
| |
| assert any(chunk["content"] == "a" * 10 for chunk in chunks) |
| |
| assert any("medium" in chunk["content"] for chunk in chunks) |
|
|
|
|
| @pytest.mark.offline |
| def test_whitespace_handling(): |
| """Test that whitespace is properly handled in chunk content.""" |
| tokenizer = make_tokenizer() |
| content = " alpha \n\n beta " |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=20, |
| ) |
|
|
| |
| assert chunks[0]["content"] == "alpha" |
| assert chunks[1]["content"] == "beta" |
|
|
|
|
| @pytest.mark.offline |
| def test_consecutive_delimiters(): |
| """Test handling of consecutive delimiters.""" |
| tokenizer = make_tokenizer() |
| content = "alpha\n\n\n\nbeta" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=20, |
| ) |
|
|
| |
| assert len(chunks) >= 2 |
| assert "alpha" in [c["content"] for c in chunks] |
| assert "beta" in [c["content"] for c in chunks] |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.offline |
| def test_token_counting_not_character_counting(): |
| """ |
| Verify chunking uses token count, not character count. |
| |
| With MultiTokenCharacterTokenizer: |
| - "aXa" = 3 chars but 4 tokens (a=1, X=2, a=1) |
| |
| This test would PASS if code incorrectly used character count (3 <= 3) |
| but correctly FAILS because token count (4 > 3). |
| """ |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| content = "aXa" |
|
|
| with pytest.raises(ChunkTokenLimitExceededError) as excinfo: |
| chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=3, |
| ) |
|
|
| err = excinfo.value |
| assert err.chunk_tokens == 4 |
| assert err.chunk_token_limit == 3 |
|
|
|
|
| @pytest.mark.offline |
| def test_token_limit_with_punctuation(): |
| """ |
| Test that punctuation token expansion is handled correctly. |
| |
| "Hi!" = 3 chars but 6 tokens (H=2, i=1, !=3) |
| """ |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| content = "Hi!" |
|
|
| with pytest.raises(ChunkTokenLimitExceededError) as excinfo: |
| chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=4, |
| ) |
|
|
| err = excinfo.value |
| assert err.chunk_tokens == 6 |
| assert err.chunk_token_limit == 4 |
|
|
|
|
| @pytest.mark.offline |
| def test_multi_token_within_limit(): |
| """Test that multi-token characters work when within limit.""" |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| content = "Hi" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=5, |
| ) |
|
|
| assert len(chunks) == 1 |
| assert chunks[0]["tokens"] == 3 |
| assert chunks[0]["content"] == "Hi" |
|
|
|
|
| @pytest.mark.offline |
| def test_recursive_split_with_multi_token_chars(): |
| """ |
| Test recursive splitting respects token boundaries, not character boundaries. |
| |
| "AAAAA" = 5 chars but 10 tokens (each A = 2 tokens) |
| With chunk_size=6, should split at token positions, not character positions. |
| """ |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| content = "AAAAA" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=6, |
| chunk_overlap_token_size=0, |
| ) |
|
|
| |
| |
| assert len(chunks) == 2 |
| assert chunks[0]["tokens"] == 6 |
| assert chunks[1]["tokens"] == 4 |
|
|
|
|
| @pytest.mark.offline |
| def test_overlap_uses_token_count(): |
| """ |
| Verify overlap calculation uses token count, not character count. |
| |
| "aAaAa" = 5 chars, 7 tokens (a=1, A=2, a=1, A=2, a=1) |
| """ |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| content = "aAaAa" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=False, |
| chunk_token_size=4, |
| chunk_overlap_token_size=2, |
| ) |
|
|
| |
| |
| assert len(chunks) == 4 |
| assert chunks[0]["tokens"] == 4 |
| assert chunks[1]["tokens"] == 4 |
| assert chunks[2]["tokens"] == 3 |
| assert chunks[3]["tokens"] == 1 |
|
|
|
|
| @pytest.mark.offline |
| def test_mixed_multi_token_content(): |
| """Test chunking with mixed single and multi-token characters.""" |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| |
| |
| content = "hello\n\nWORLD!" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=20, |
| ) |
|
|
| assert len(chunks) == 2 |
| assert chunks[0]["content"] == "hello" |
| assert chunks[0]["tokens"] == 5 |
| assert chunks[1]["content"] == "WORLD!" |
| assert chunks[1]["tokens"] == 13 |
|
|
|
|
| @pytest.mark.offline |
| def test_exact_token_boundary_multi_token(): |
| """Test splitting exactly at token limit with multi-token characters.""" |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| content = "AAA" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character="\n\n", |
| split_by_character_only=True, |
| chunk_token_size=6, |
| ) |
|
|
| assert len(chunks) == 1 |
| assert chunks[0]["tokens"] == 6 |
| assert chunks[0]["content"] == "AAA" |
|
|
|
|
| @pytest.mark.offline |
| def test_multi_token_overlap_with_distinctive_content(): |
| """ |
| Verify overlap works correctly with multi-token characters using distinctive content. |
| |
| With non-uniform tokenization, overlap must be calculated in token space, not character space. |
| Distinctive characters ensure we catch any misalignment. |
| |
| Content: "abcABCdef" |
| - "abc" = 3 tokens (1+1+1) |
| - "ABC" = 6 tokens (2+2+2) |
| - "def" = 3 tokens (1+1+1) |
| - Total = 12 tokens |
| """ |
| tokenizer = make_multi_token_tokenizer() |
|
|
| |
| content = "abcABCdef" |
|
|
| chunks = chunking_by_token_size( |
| tokenizer, |
| content, |
| split_by_character=None, |
| split_by_character_only=False, |
| chunk_token_size=6, |
| chunk_overlap_token_size=2, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| assert len(chunks) == 3 |
|
|
| |
| assert chunks[0]["tokens"] == 6 |
| assert chunks[1]["tokens"] == 6 |
| assert chunks[2]["tokens"] == 4 |
|
|
|
|
| @pytest.mark.offline |
| def test_decode_preserves_content(): |
| """Verify that decode correctly reconstructs original content.""" |
| tokenizer = make_multi_token_tokenizer() |
|
|
| test_strings = [ |
| "Hello", |
| "WORLD", |
| "Test!", |
| "Mixed?Case.", |
| "ABC123xyz", |
| ] |
|
|
| for original in test_strings: |
| tokens = tokenizer.encode(original) |
| decoded = tokenizer.decode(tokens) |
| assert decoded == original, f"Failed to decode: {original}" |
|
|