Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ There is no general `lilbee set` command. From a shell, set the environment vari
| `ocr_scan_confidence` | `LILBEE_OCR_SCAN_CONFIDENCE` | `float` | `0.7` | yes | yes | yes | no | How sure xberg must be that a page is a scan before it OCRs it (0-1). Applies only when ocr_strategy is scanned_pages. Lower it to 0.5 to also OCR slides with a full-page background image. |
| `ocr_strategy` | `LILBEE_OCR_STRATEGY` | `str` | `auto` | yes | yes | yes | no | PDF pages to OCR: auto (pages whose text layer is missing or garbled) or scanned_pages (also every page that looks like a scan, e.g. a scanned page with a hidden text layer). One of `auto`, `scanned_pages`. |
| `ocr_timeout` | `LILBEE_OCR_TIMEOUT` | `float` | `300.0` | yes | yes | yes | no | Per-page timeout in seconds for vision OCR (0 = no limit). |
| `semantic_chunking` | `LILBEE_SEMANTIC_CHUNKING` | `bool` | `false` | yes | yes | yes | no | Opt-in topic-aware chunker (default off; may fragment numbered procedures). |
| `semantic_chunking` | `LILBEE_SEMANTIC_CHUNKING` | `bool` | `false` | yes | yes | yes | no | Opt-in topic-aware chunker (default off; may fragment numbered procedures) (changes invalidate the index). **Reindex** with `lilbee rebuild` after changing. |
| `table_extraction` | `LILBEE_TABLE_EXTRACTION` | `bool` | `false` | yes | yes | yes | no | Index each extracted table as its own chunk (changes invalidate the index). **Reindex** with `lilbee rebuild` after changing. |
| `table_model` | `LILBEE_TABLE_MODEL` | `str` | `slanet_auto` | yes | yes | yes | no | Table structure model used when layout detection is on: slanet_auto (docling-parity default), other slanet variants, tatr, or disabled (changes invalidate the index). One of `disabled`, `tatr`, `slanet_auto`, `slanet_plus`, `slanet_wired`, `slanet_wireless`. **Reindex** with `lilbee rebuild` after changing. |
| `tesseract_timeout` | `LILBEE_TESSERACT_TIMEOUT` | `float` | `60.0` | yes | yes | yes | no | Per-page Tesseract timeout in seconds (used when no vision model is set). |
| `token_sizing` | `LILBEE_TOKEN_SIZING` | `bool` | `false` | yes | yes | yes | no | Size chunks by real embedder tokens, not chars; on by itself when the embedder's window is below the character budget (changes invalidate the index). **Reindex** with `lilbee rebuild` after changing. |
| `topic_threshold` | `LILBEE_TOPIC_THRESHOLD` | `float` | `0.75` | yes | yes | yes | no | Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking is on. |
| `topic_threshold` | `LILBEE_TOPIC_THRESHOLD` | `float` | `0.75` | yes | yes | yes | no | Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking is on (changes invalidate the index). **Reindex** with `lilbee rebuild` after changing. |
| `vision_load_budget_s` | `LILBEE_VISION_LOAD_BUDGET_S` | `float` | `300.0` | yes | yes | yes | no | Wall-clock seconds reserved for the vision worker to load the model. Total PDF-OCR budget = load_budget + ocr_timeout * pages. |
| `vision_ocr_concurrency` | `LILBEE_VISION_OCR_CONCURRENCY` | `int` | `4` | yes | yes | yes | no | Pages OCR'd concurrently per vision server; each slot adds KV cache memory. |
| `vision_ocr_max_tokens` | `LILBEE_VISION_OCR_MAX_TOKENS` | `int` | `4096` | yes | yes | yes | no | Hard cap on tokens generated per OCR page (bounds runaway repetition loops); raising it lengthens page generation, so give ocr_timeout headroom. |
Expand Down
12 changes: 9 additions & 3 deletions src/lilbee/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,16 @@ def reconcile_embedding_dim(registry: ModelRegistry | None = None) -> None:
def _inert_reindex_keys() -> set[str]:
"""Reindex keys that change no extraction output under the effective config.

xberg reads ``table_model`` only inside layout detection, so a change to it
while ``layout_detection`` is off is not worth a rebuild.
``table_model`` is read only inside layout detection, and ``topic_threshold``
only inside semantic chunking, so a change to either while its gating flag is
off is not worth a rebuild.
"""
return set() if cfg.layout_detection else {"table_model"}
inert: set[str] = set()
if not cfg.layout_detection:
inert.add("table_model")
if not cfg.semantic_chunking:
inert.add("topic_threshold")
return inert


def _embed_reindex_required() -> bool:
Expand Down
10 changes: 8 additions & 2 deletions src/lilbee/app/settings_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,19 @@ def get_default(key: str) -> object:
bool,
nullable=False,
group=SettingGroup.INGEST,
help_text="Opt-in topic-aware chunker (default off; may fragment numbered procedures)",
help_text=(
"Opt-in topic-aware chunker (default off; may fragment numbered procedures) "
"(changes invalidate the index)"
),
),
"topic_threshold": SettingDef(
float,
nullable=False,
group=SettingGroup.INGEST,
help_text="Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking is on",
help_text=(
"Topic-boundary similarity threshold, 0.0-1.0, used when semantic chunking "
"is on (changes invalidate the index)"
),
),
"token_sizing": SettingDef(
bool,
Expand Down
5 changes: 3 additions & 2 deletions src/lilbee/core/config/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,9 @@ class Config(BaseSettings):
force_ocr_pages: list[int] = ConfigField(default_factory=list, writable=True)
# Typed entity table for exact counting/cross-referencing; corpus-scale pass, off by default.
entity_extraction: bool = ConfigField(default=False, writable=True)
semantic_chunking: bool = ConfigField(default=False, writable=True)
topic_threshold: float = ConfigField(default=0.75, ge=0.0, le=1.0, writable=True)
semantic_chunking: bool = ConfigField(default=False, writable=True, reindex=True)
# Boundary-similarity threshold; only applied when semantic_chunking is on.
topic_threshold: float = ConfigField(default=0.75, ge=0.0, le=1.0, writable=True, reindex=True)
# Size chunks in real tokens via the embedder's tokenizer backend, not the
# chars-per-token heuristic. Plain/heading chunkers only; semantic sizes by chars.
token_sizing: bool = ConfigField(default=False, writable=True, reindex=True)
Expand Down
70 changes: 70 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,76 @@ def test_table_model_change_flags_a_reindex_once_layout_detection_is_on(self, mo
assert both.reindex_required is True


class TestSemanticChunkingSetting:
"""The semantic-chunking flag is writable, grouped with ingest, and reindex-marked."""

def test_semantic_chunking_in_settings_map(self):
from lilbee.app.settings_map import SETTINGS_MAP, get_default

defn = SETTINGS_MAP["semantic_chunking"]
assert defn.writable is True
assert defn.nullable is False
assert defn.type is bool
assert defn.group == "Ingest"
assert get_default("semantic_chunking") is False

def test_semantic_chunking_requires_reindex(self):
from lilbee.config_meta import REINDEX_FIELDS, WRITABLE_CONFIG_FIELDS

assert "semantic_chunking" in WRITABLE_CONFIG_FIELDS
assert "semantic_chunking" in REINDEX_FIELDS

def test_semantic_chunking_change_flags_a_reindex(self, monkeypatch):
from lilbee.app import settings as appset

monkeypatch.setattr(appset.persistent_settings, "update_values", lambda *_a, **_k: None)
result = appset.apply_settings_update({"semantic_chunking": True})
assert result.updated == ["semantic_chunking"]
assert result.reindex_required is True


class TestTopicThresholdSetting:
"""The topic-threshold value is writable, grouped with ingest, reindex-marked, and
inert while semantic chunking is off (xberg reads it only inside that chunker)."""

def test_topic_threshold_in_settings_map(self):
from lilbee.app.settings_map import SETTINGS_MAP, get_default

defn = SETTINGS_MAP["topic_threshold"]
assert defn.writable is True
assert defn.nullable is False
assert defn.type is float
assert defn.group == "Ingest"
assert get_default("topic_threshold") == 0.75

def test_topic_threshold_requires_reindex(self):
from lilbee.config_meta import REINDEX_FIELDS, WRITABLE_CONFIG_FIELDS

assert "topic_threshold" in WRITABLE_CONFIG_FIELDS
assert "topic_threshold" in REINDEX_FIELDS

def test_topic_threshold_change_flags_no_reindex_while_semantic_chunking_is_off(
self, monkeypatch
):
from lilbee.app import settings as appset

monkeypatch.setattr(appset.cfg, "semantic_chunking", False)
monkeypatch.setattr(appset.persistent_settings, "update_values", lambda *_a, **_k: None)
result = appset.apply_settings_update({"topic_threshold": 0.5})
assert result.updated == ["topic_threshold"]
assert result.reindex_required is False

def test_topic_threshold_change_flags_a_reindex_once_semantic_chunking_is_on(self, monkeypatch):
from lilbee.app import settings as appset

monkeypatch.setattr(appset.persistent_settings, "update_values", lambda *_a, **_k: None)
monkeypatch.setattr(appset.cfg, "semantic_chunking", True)
assert appset.apply_settings_update({"topic_threshold": 0.5}).reindex_required is True
monkeypatch.setattr(appset.cfg, "semantic_chunking", False)
both = appset.apply_settings_update({"topic_threshold": 0.6, "semantic_chunking": True})
assert both.reindex_required is True


class TestOcrPageSelectionSettings:
"""The PDF OCR page-selection settings are writable and survive a config.toml reload."""

Expand Down
Loading