everalgo-knowledge 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/CHANGELOG.md +16 -1
  2. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/PKG-INFO +2 -3
  3. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/pyproject.toml +2 -3
  4. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_block_split.py +8 -20
  5. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_block_split.py +21 -19
  6. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/.gitignore +0 -0
  7. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/LICENSE +0 -0
  8. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/README.md +0 -0
  9. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/examples/_viz.py +0 -0
  10. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/examples/run_one_doc.py +0 -0
  11. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/__init__.py +0 -0
  12. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_batch_merge.py +0 -0
  13. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_classify.py +0 -0
  14. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_doc_root.py +0 -0
  15. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_flatten.py +0 -0
  16. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_llm_json.py +0 -0
  17. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_postprocess.py +0 -0
  18. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/_topic_build.py +0 -0
  19. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/extractor.py +0 -0
  20. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/__init__.py +0 -0
  21. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/en/__init__.py +0 -0
  22. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/en/category_classify.py +0 -0
  23. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/en/content_merge.py +0 -0
  24. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/en/topic_merge.py +0 -0
  25. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/en/topic_split.py +0 -0
  26. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/en/topic_tree_extract.py +0 -0
  27. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/prompts/en/uncovered_assign.py +0 -0
  28. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/src/everalgo/knowledge/py.typed +0 -0
  29. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/__init__.py +0 -0
  30. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/_loader.py +0 -0
  31. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/conftest.py +0 -0
  32. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/fixtures/document_confluence.json +0 -0
  33. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/fixtures/document_confluence.queries.json +0 -0
  34. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/fixtures/idx_multi_topic.json +0 -0
  35. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/fixtures/idx_multi_topic.queries.json +0 -0
  36. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/outputs/.gitignore +0 -0
  37. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/test_classify_smoke.py +0 -0
  38. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/functional/test_extraction_smoke.py +0 -0
  39. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/__init__.py +0 -0
  40. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_batch_merge.py +0 -0
  41. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_classify.py +0 -0
  42. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_extractor_e2e.py +0 -0
  43. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_flatten_doc_root.py +0 -0
  44. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_knowledge_public_api.py +0 -0
  45. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_llm_json.py +0 -0
  46. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_postprocess.py +0 -0
  47. {everalgo_knowledge-0.2.0 → everalgo_knowledge-0.2.1}/tests/knowledge/test_topic_build.py +0 -0
@@ -6,6 +6,20 @@ follows [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-09-16
10
+
11
+ ### Changed
12
+
13
+ - The minimum `everalgo-core` version is now `0.5.1`, because knowledge splitting relies on that release's special-token-safe `count_tokens` behavior.
14
+
15
+ ### Fixed
16
+
17
+ - `split_content_to_blocks` and `split_and_batch_content` no longer raise `ValueError` on a document that quotes an `o200k_base` special-token literal such as `<|endoftext|>` — tokenizer documentation and bug reports could not be ingested at all. `_block_split` had grown a second, independent tokenizer; it now counts through `everalgo._tokenize.count_tokens`, which is the shared entry point fixed in `everalgo-core` (see that changelog for the rationale). Block boundaries and batch budgets for documents without such a literal are unchanged.
18
+
19
+ ### Removed
20
+
21
+ - The direct `tiktoken` dependency. The package no longer imports it; token counting goes through `everalgo-core`, which declares it. No action is needed from callers — `tiktoken` still arrives transitively.
22
+
9
23
  ## [0.2.0] - 2026-09-15
10
24
 
11
25
  ### Fixed
@@ -31,4 +45,5 @@ Previously a reserved namespace with `Private :: Do Not Upload`.
31
45
  [0.2.0]: https://github.com/EverMind-AI/EverAlgo/releases/tag/everalgo-knowledge/v0.2.0
32
46
  [0.2.0rc1]: https://github.com/EverMind-AI/EverAlgo/releases/tag/everalgo-knowledge/v0.2.0rc1
33
47
  [0.1.1]: https://github.com/EverMind-AI/EverAlgo/releases/tag/everalgo-knowledge/v0.1.1
34
- [Unreleased]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-knowledge/v0.2.0...HEAD
48
+ [Unreleased]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-knowledge/v0.2.1...HEAD
49
+ [0.2.1]: https://github.com/EverMind-AI/EverAlgo/compare/everalgo-knowledge/v0.2.0...everalgo-knowledge/v0.2.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: everalgo-knowledge
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: EverAlgo knowledge: file-based knowledge extractors producing KnowledgeMemory.
5
5
  Project-URL: Homepage, https://github.com/EverMind-AI/EverAlgo
6
6
  Project-URL: Repository, https://github.com/EverMind-AI/EverAlgo
@@ -19,9 +19,8 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
20
  Requires-Python: >=3.12
21
21
  Requires-Dist: asgiref>=3.0
22
- Requires-Dist: everalgo-core<2.0.0,>=0.3.0
22
+ Requires-Dist: everalgo-core<2.0.0,>=0.5.1
23
23
  Requires-Dist: everalgo-parser<2.0.0,>=0.2.0
24
- Requires-Dist: tiktoken>=0.7
25
24
  Description-Content-Type: text/markdown
26
25
 
27
26
  # everalgo-knowledge
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "everalgo-knowledge"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "EverAlgo knowledge: file-based knowledge extractors producing KnowledgeMemory."
9
9
  requires-python = ">=3.12"
10
10
  license = "Apache-2.0"
@@ -21,9 +21,8 @@ classifiers = [
21
21
  ]
22
22
  dependencies = [
23
23
  "asgiref>=3.0",
24
- "everalgo-core>=0.3.0,<2.0.0",
24
+ "everalgo-core>=0.5.1,<2.0.0",
25
25
  "everalgo-parser>=0.2.0,<2.0.0",
26
- "tiktoken>=0.7",
27
26
  ]
28
27
 
29
28
  [project.urls]
@@ -17,9 +17,8 @@ the knowledge extractor pipeline.
17
17
  from __future__ import annotations
18
18
 
19
19
  import re
20
- from functools import lru_cache
21
20
 
22
- import tiktoken
21
+ from everalgo._tokenize import count_tokens
23
22
 
24
23
  __all__ = [
25
24
  "DEFAULT_MAX_TOKENS_PER_ATOM",
@@ -37,8 +36,6 @@ __all__ = [
37
36
  TABLE_START_MARKER = "TABLE_START"
38
37
  TABLE_END_MARKER = "TABLE_END"
39
38
 
40
- _O200K_ENCODING_NAME = "o200k_base"
41
-
42
39
  # Default token budget per LLM-window batch. 80K leaves headroom on a 128K-context
43
40
  # model after the topic-extraction prompt + JSON response. Tunable per call.
44
41
  DEFAULT_MAX_TOKENS_PER_BATCH = 80_000
@@ -52,12 +49,6 @@ DEFAULT_MAX_TOKENS_PER_BATCH = 80_000
52
49
  DEFAULT_MAX_TOKENS_PER_ATOM = DEFAULT_MAX_TOKENS_PER_BATCH // 4
53
50
 
54
51
 
55
- @lru_cache(maxsize=1)
56
- def _get_tokenizer() -> tiktoken.Encoding:
57
- """Return the shared ``o200k_base`` encoding, initialising on first call."""
58
- return tiktoken.get_encoding(_O200K_ENCODING_NAME)
59
-
60
-
61
52
  def preprocess_content(content: str) -> str:
62
53
  """Normalize parsed text before block splitting.
63
54
 
@@ -144,8 +135,7 @@ def _split_long_line(text: str, max_tokens: int) -> list[str]:
144
135
  66,000-character CJK line, which came back 66,006 characters with mojibake at
145
136
  every seam. Character slices concatenate back to the original exactly.
146
137
  """
147
- enc = _get_tokenizer()
148
- tokens_per_char = len(enc.encode(text)) / len(text)
138
+ tokens_per_char = count_tokens(text) / len(text)
149
139
  # 0.9 leaves margin for a denser stretch than the whole-line average.
150
140
  window = max(1, int(max_tokens / tokens_per_char * 0.9))
151
141
 
@@ -153,7 +143,7 @@ def _split_long_line(text: str, max_tokens: int) -> list[str]:
153
143
  start = 0
154
144
  while start < len(text):
155
145
  end = min(start + window, len(text))
156
- while end - start > 1 and len(enc.encode(text[start:end])) > max_tokens:
146
+ while end - start > 1 and count_tokens(text[start:end]) > max_tokens:
157
147
  end = start + (end - start) * 3 // 4
158
148
  pieces.append(text[start:end])
159
149
  start = end
@@ -168,11 +158,10 @@ def _split_oversized_atom(text: str, max_tokens: int) -> list[str]:
168
158
  The pieces stay adjacent in the atom list, so a topic that claims all of them
169
159
  reassembles the original text through ``_topic_build._rebuild_content``.
170
160
  """
171
- enc = _get_tokenizer()
172
161
  lines = text.split("\n")
173
162
  header = _table_header(lines)
174
163
  header_text = "\n".join(header)
175
- header_tokens = len(enc.encode(header_text)) if header else 0
164
+ header_tokens = count_tokens(header_text) if header else 0
176
165
 
177
166
  pieces: list[str] = []
178
167
  current: list[str] = []
@@ -182,12 +171,12 @@ def _split_oversized_atom(text: str, max_tokens: int) -> list[str]:
182
171
  if current:
183
172
  pieces.append("\n".join(header + current) if header else "\n".join(current))
184
173
 
185
- newline_tokens = len(enc.encode("\n"))
174
+ newline_tokens = count_tokens("\n")
186
175
  for line in lines[len(header) :]:
187
176
  # Count the "\n" that ``join`` will insert. Omitting it undercounts a piece by
188
177
  # roughly one token per line, which pushes long runs over the budget and drops
189
178
  # them into the character-boundary fallback — splitting list items mid-item.
190
- line_tokens = len(enc.encode(line)) + newline_tokens
179
+ line_tokens = count_tokens(line) + newline_tokens
191
180
  if current and current_tokens + line_tokens > max_tokens:
192
181
  flush()
193
182
  current, current_tokens = [line], header_tokens + line_tokens
@@ -204,7 +193,7 @@ def _split_oversized_atom(text: str, max_tokens: int) -> list[str]:
204
193
  # PDF often arrives as one very long unwrapped line.
205
194
  bounded: list[str] = []
206
195
  for piece in pieces:
207
- if len(enc.encode(piece)) <= max_tokens:
196
+ if count_tokens(piece) <= max_tokens:
208
197
  bounded.append(piece)
209
198
  else:
210
199
  bounded.extend(_split_long_line(piece, max_tokens))
@@ -297,13 +286,12 @@ def split_and_batch_content(
297
286
  if not atoms:
298
287
  return []
299
288
 
300
- enc = _get_tokenizer()
301
289
  batches: list[list[tuple[int, str]]] = []
302
290
  current_batch: list[tuple[int, str]] = []
303
291
  current_tokens = 0
304
292
 
305
293
  for atom in atoms:
306
- atom_tokens = len(enc.encode(atom[1]))
294
+ atom_tokens = count_tokens(atom[1])
307
295
  if current_batch and current_tokens + atom_tokens > max_tokens:
308
296
  batches.append(current_batch)
309
297
  current_batch = [atom]
@@ -4,12 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import pytest
6
6
 
7
+ from everalgo._tokenize import count_tokens
7
8
  from everalgo.knowledge._block_split import (
8
9
  DEFAULT_MAX_TOKENS_PER_ATOM,
9
10
  DEFAULT_MAX_TOKENS_PER_BATCH,
10
11
  TABLE_END_MARKER,
11
12
  TABLE_START_MARKER,
12
- _get_tokenizer,
13
13
  format_numbered_paragraphs,
14
14
  preprocess_content,
15
15
  split_and_batch_content,
@@ -143,13 +143,12 @@ def _table(rows: int, *, header: bool = True) -> str:
143
143
 
144
144
  def test_unbounded_table_merge_no_longer_yields_an_over_budget_atom() -> None:
145
145
  """A page that is mostly one table used to collapse into a single mega-atom."""
146
- enc = _get_tokenizer()
147
146
  doc = f"# Title\n\n## Variants\n\n{_table(6000)}\n\n## References\n\nSee the index.\n"
148
147
 
149
148
  atoms = split_content_to_blocks(doc)
150
149
 
151
150
  assert len(atoms) > 6, "the table must no longer be one atom"
152
- assert all(len(enc.encode(t)) <= DEFAULT_MAX_TOKENS_PER_ATOM for _, t in atoms)
151
+ assert all(count_tokens(t) <= DEFAULT_MAX_TOKENS_PER_ATOM for _, t in atoms)
153
152
  assert [i for i, _ in atoms] == list(range(len(atoms))), "ids stay dense and sequential"
154
153
 
155
154
 
@@ -173,13 +172,12 @@ def test_oversized_atom_pieces_preserve_every_row() -> None:
173
172
 
174
173
 
175
174
  def test_long_list_is_split_without_a_header() -> None:
176
- enc = _get_tokenizer()
177
175
  doc = "\n".join(f"- Item {i}: the polished lens recipe was introduced here." for i in range(8000))
178
176
 
179
177
  atoms = split_content_to_blocks(doc)
180
178
 
181
179
  assert len(atoms) > 1
182
- assert all(len(enc.encode(t)) <= DEFAULT_MAX_TOKENS_PER_ATOM for _, t in atoms)
180
+ assert all(count_tokens(t) <= DEFAULT_MAX_TOKENS_PER_ATOM for _, t in atoms)
183
181
  assert [ln for _, t in atoms for ln in t.splitlines()] == doc.splitlines()
184
182
 
185
183
 
@@ -191,13 +189,12 @@ def test_ordinary_document_is_untouched_by_the_cap() -> None:
191
189
 
192
190
 
193
191
  def test_cap_is_configurable_and_zero_disables_it() -> None:
194
- enc = _get_tokenizer()
195
192
  doc = _table(2000)
196
193
 
197
194
  assert len(split_content_to_blocks(doc, max_atom_tokens=0)) == 1
198
195
  tight = split_content_to_blocks(doc, max_atom_tokens=5_000)
199
196
  assert len(tight) > 1
200
- assert all(len(enc.encode(t)) <= 5_000 for _, t in tight)
197
+ assert all(count_tokens(t) <= 5_000 for _, t in tight)
201
198
 
202
199
 
203
200
  # ── adversarial: ways an atom could still slip past the cap ──────────
@@ -216,64 +213,59 @@ def test_cap_is_configurable_and_zero_disables_it() -> None:
216
213
  )
217
214
  def test_cap_holds_for_every_script(label: str, unit: str) -> None:
218
215
  """A character-count pre-filter is unsound: CJK and emoji exceed 1 token/char."""
219
- enc = _get_tokenizer()
220
216
  cap = 4_000
221
217
  doc = "\n".join(f"- {unit}" for _ in range(2_000))
222
218
 
223
219
  atoms = split_content_to_blocks(doc, max_atom_tokens=cap)
224
220
 
225
- oversized = [(i, len(enc.encode(t))) for i, t in atoms if len(enc.encode(t)) > cap]
221
+ oversized = [(i, count_tokens(t)) for i, t in atoms if count_tokens(t) > cap]
226
222
  assert not oversized, f"{label}: atoms over cap {cap}: {oversized}"
227
223
 
228
224
 
229
225
  def test_single_line_longer_than_the_cap_is_split_on_token_boundaries() -> None:
230
226
  """CJK from a PDF often arrives as one very long unwrapped line."""
231
- enc = _get_tokenizer()
232
227
  cap = 1_000
233
228
  doc = "洗涤剂的总活性物含量决定去污能力和溶解性能。" * 3_000 # one line, no breaks
234
229
 
235
230
  atoms = split_content_to_blocks(doc, max_atom_tokens=cap)
236
231
 
237
232
  assert len(atoms) > 1
238
- assert all(len(enc.encode(t)) <= cap for _, t in atoms)
233
+ assert all(count_tokens(t) <= cap for _, t in atoms)
239
234
  assert "".join(t for _, t in atoms) == doc.strip(), "no content may be lost"
240
235
 
241
236
 
242
237
  def test_table_marker_path_is_also_capped() -> None:
243
238
  """Rule 1 (TABLE_START/END) is unbounded too, not just the native-row merge."""
244
- enc = _get_tokenizer()
245
239
  cap = 4_000
246
240
  rows = "\n".join(f"<tr><td>Variant {i}</td><td>Wandering merchant</td></tr>" for i in range(3_000))
247
241
  doc = f"# Title\n\n{TABLE_START_MARKER}\n{rows}\n{TABLE_END_MARKER}\n\nAfter.\n"
248
242
 
249
243
  atoms = split_content_to_blocks(doc, max_atom_tokens=cap)
250
244
 
251
- assert all(len(enc.encode(t)) <= cap for _, t in atoms)
245
+ assert all(count_tokens(t) <= cap for _, t in atoms)
252
246
  assert atoms[-1][1] == "After."
253
247
 
254
248
 
255
249
  def test_table_without_a_separator_row_still_splits() -> None:
256
250
  """Header detection must not be a precondition for the cap to apply."""
257
- enc = _get_tokenizer()
258
251
  cap = 4_000
259
252
  doc = "\n".join(f"| Variant {i} | Wandering merchant, biome {i % 40} |" for i in range(3_000))
260
253
 
261
254
  atoms = split_content_to_blocks(doc, max_atom_tokens=cap)
262
255
 
263
256
  assert len(atoms) > 1
264
- assert all(len(enc.encode(t)) <= cap for _, t in atoms)
257
+ assert all(count_tokens(t) <= cap for _, t in atoms)
265
258
 
266
259
 
267
260
  def test_header_larger_than_the_cap_does_not_loop_or_drop_content() -> None:
268
261
  """Degenerate: the repeated header alone already exceeds the budget."""
269
- enc = _get_tokenizer()
270
262
  header = "| " + " | ".join(f"column_{i}" for i in range(600)) + " |"
271
263
  doc = header + "\n|" + "---|" * 600 + "\n" + "\n".join(f"| row {i} |" for i in range(500))
272
264
 
273
265
  atoms = split_content_to_blocks(doc, max_atom_tokens=500)
274
266
 
275
267
  assert atoms, "must not return empty"
276
- assert all(len(enc.encode(t)) <= 500 for _, t in atoms)
268
+ assert all(count_tokens(t) <= 500 for _, t in atoms)
277
269
 
278
270
 
279
271
  def test_ids_stay_dense_after_splitting_several_atoms() -> None:
@@ -290,7 +282,6 @@ def test_ids_stay_dense_after_splitting_several_atoms() -> None:
290
282
 
291
283
  def test_split_atoms_batch_within_budget_end_to_end() -> None:
292
284
  """The whole point: no batch may exceed the window after splitting."""
293
- enc = _get_tokenizer()
294
285
  rows = "\n".join(
295
286
  f"| Shiny variant #{i:04d} | Trading with the wandering merchant in biome {i % 40} "
296
287
  f"| {'Rare' if i % 3 else 'Common'} | Introduced in the winter update. |"
@@ -300,5 +291,16 @@ def test_split_atoms_batch_within_budget_end_to_end() -> None:
300
291
 
301
292
  batches = split_and_batch_content(split_content_to_blocks(preprocess_content(doc)))
302
293
 
303
- over = [len(enc.encode(format_numbered_paragraphs(b))) for b in batches]
294
+ over = [count_tokens(format_numbered_paragraphs(b)) for b in batches]
304
295
  assert all(n <= DEFAULT_MAX_TOKENS_PER_BATCH for n in over), f"over-budget batches: {over}"
296
+
297
+
298
+ def test_split_handles_special_token_literals() -> None:
299
+ """Block splitting counts tokens, so it shared the tokenizer defect.
300
+
301
+ A knowledge document about tokenizers quotes these literals; before the shared encode
302
+ wrapper that document could not be ingested at all.
303
+ """
304
+ content = "The tokenizer reserves the literal `<|endoftext|>` as a control token.\n\n" * 20
305
+ blocks = split_content_to_blocks(content)
306
+ assert blocks # split, not raised