Turn-aware chunking for embedding.
Chunks are derived from turns rows for a single conversation. The default
strategy is turn_group: consecutive turns are grouped into overlapping
windows of turns_per_chunk turns. Turns are never split. A chunk's text
is the speaker-prefixed lines joined with newlines so the embedder sees
turn-level structure.
fixed_tokens is a placeholder for a future strategy and currently raises.
chunk_turn_group(turns, cfg, *, count_tokens=None)
Group consecutive turns into overlapping windows.
Windows of turns_per_chunk turns advance by turns_per_chunk - overlap_turns.
Windows whose token count falls below min_chunk_tokens are dropped (when
a token counter is supplied). Without a counter, every window is emitted.
Source code in src/transcript_indexer/chunking.py
| def chunk_turn_group(
turns: Sequence[TurnRow],
cfg: ChunkingConfig,
*,
count_tokens: Callable[[str], int] | None = None,
) -> list[Chunk]:
"""Group consecutive turns into overlapping windows.
Windows of `turns_per_chunk` turns advance by `turns_per_chunk - overlap_turns`.
Windows whose token count falls below `min_chunk_tokens` are dropped (when
a token counter is supplied). Without a counter, every window is emitted.
"""
n = len(turns)
if n == 0:
return []
size = max(1, cfg.turns_per_chunk)
overlap = max(0, min(cfg.overlap_turns, size - 1))
step = size - overlap
chunks: list[Chunk] = []
seen_hashes: set[str] = set()
start = 0
while start < n:
end = min(start + size, n)
window = turns[start:end]
text = _format_chunk_text(window)
if count_tokens is not None and cfg.max_chunk_tokens > 0:
# Shrink window from the tail until it fits within the token ceiling.
# Stop at 1 turn — we don't split individual turns.
while len(window) > 1 and count_tokens(text) > cfg.max_chunk_tokens:
window = window[:-1]
text = _format_chunk_text(window)
# When the window was shrunk, advance by the actual window size so no turn
# is skipped. Use the configured step only for full-size windows.
was_shrunk = len(window) < end - start
advance = len(window) if was_shrunk else step
if count_tokens is not None and count_tokens(text) < cfg.min_chunk_tokens:
# Tail windows below threshold are skipped; mid-corpus undersized
# windows are rare with overlap > 0.
if end == n and not was_shrunk:
break
start += advance
continue
text_hash = sha256_text(text)
if text_hash not in seen_hashes:
seen_hashes.add(text_hash)
chunks.append(
Chunk(
kind="turn_group",
start_turn_idx=window[0].idx,
end_turn_idx=window[-1].idx,
text=text,
text_hash=text_hash,
)
)
if end == n and not was_shrunk:
break
start += advance
return chunks
|
chunk_conversation(turns, cfg, *, count_tokens=None)
Dispatch to the chunking strategy named in cfg.strategy.
Source code in src/transcript_indexer/chunking.py
| def chunk_conversation(
turns: Sequence[TurnRow],
cfg: ChunkingConfig,
*,
count_tokens: Callable[[str], int] | None = None,
) -> list[Chunk]:
"""Dispatch to the chunking strategy named in `cfg.strategy`."""
if cfg.strategy == "turn_group":
return chunk_turn_group(turns, cfg, count_tokens=count_tokens)
if cfg.strategy == "fixed_tokens":
raise NotImplementedError("fixed_tokens chunking is not implemented yet")
raise ValueError(f"unknown chunking strategy: {cfg.strategy}")
|