Skip to content

Commit 94f99e8

Browse files
committed
Refactor semantic chunker embedder integration
1 parent 259e22d commit 94f99e8

22 files changed

Lines changed: 604 additions & 347 deletions

infrastructure/rag/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,12 @@ adminBackend:
340340
chunker:
341341
CHUNKER_MAX_SIZE: 1000
342342
CHUNKER_OVERLAP: 300
343+
CHUNKER_SEMANTIC_BREAKPOINT_THRESHOLD_TYPE: "percentile"
344+
CHUNKER_SEMANTIC_BREAKPOINT_THRESHOLD: "95"
345+
CHUNKER_SEMANTIC_BUFFER_SIZE: "1"
346+
CHUNKER_SEMANTIC_MIN_CHUNK_SIZE: "200"
347+
CHUNKER_SEMANTIC_MAX_CHUNK_SIZE: "1200"
348+
CHUNKER_SEMANTIC_TRIM_CHUNKS: "true"
343349
keyValueStore:
344350
USECASE_KEYVALUE_PORT: 6379
345351
USECASE_KEYVALUE_HOST: "rag-keydb"

libs/README.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ It consists of the following python packages:
1313
- [2.1 Requirements](#21-requirements)
1414
- [2.2 Endpoints](#22-endpoints)
1515
- [2.3 Replaceable parts](#23-replaceable-parts)
16-
- [2.4 Summarizer retry behavior](#24-summarizer-retry-behavior)
16+
- [2.4 Chunker configuration](#24-chunker-configuration)
17+
- [2.5 Summarizer retry behavior](#25-summarizer-retry-behavior)
1718
- [`3. Extractor API lib`](#3-extractor-api-lib)
1819
- [3.1 Requirements](#31-requirements)
1920
- [3.2 Endpoints](#32-endpoints)
@@ -106,7 +107,7 @@ The default STACKIT embedder implementation (`StackitEmbedder`) uses the shared
106107

107108
- Decorator: `rag_core_lib.impl.utils.retry_decorator.retry_with_backoff`
108109
- Base settings (fallback): [`RetryDecoratorSettings`](./rag-core-lib/src/rag_core_lib/impl/settings/retry_decorator_settings.py)
109-
- Per-embedder overrides: [`StackitEmbedderSettings`](./rag-core-api/src/rag_core_api/impl/settings/stackit_embedder_settings.py)
110+
- Per-embedder overrides: [`StackitEmbedderSettings`](./rag-core-lib/src/rag_core_lib/impl/settings/stackit_embedder_settings.py)
110111

111112
How it resolves settings
112113

@@ -189,7 +190,7 @@ The extracted information will be summarized using LLM. The summary, as well as
189190
| rag_api | [`admin_api_lib.rag_backend_client.openapi_client.api.rag_api.RagApi`](./admin-api-lib/src/admin_api_lib/rag_backend_client/openapi_client/api/rag_api.py) | [`admin_api_lib.rag_backend_client.openapi_client.api.rag_api.RagApi`](./admin-api-lib/src/admin_api_lib/rag_backend_client/openapi_client/api/rag_api.py) | Needs to be replaced if changes to the `/information_pieces/remove` or `/information_pieces/upload` of the [`rag-core-api`](#1-rag-core-api) are made. |
190191
| summarizer_prompt | `str` | [`admin_api_lib.prompt_templates.summarize_prompt.SUMMARIZE_PROMPT`](./admin-api-lib/src/admin_api_lib/prompt_templates/summarize_prompt.py) | The prompt used of the summarization. |
191192
| langfuse_manager | [`rag_core_lib.impl.langfuse_manager.langfuse_manager.LangfuseManager`](./rag-core-lib/src/rag_core_lib/impl/langfuse_manager/langfuse_manager.py) | [`rag_core_lib.impl.langfuse_manager.langfuse_manager.LangfuseManager`](./rag-core-lib/src/rag_core_lib/impl/langfuse_manager/langfuse_manager.py) | Retrieves additional settings, as well as the prompt from langfuse if available. |
192-
| summarizer | [`admin_api_lib.summarizer.summarizer.Summarizer`](./admin-api-lib/src/admin_api_lib/summarizer/summarizer.py) | [`admin_api_lib.impl.summarizer.langchain_summarizer.LangchainSummarizer`](./admin-api-lib/src/admin_api_lib/impl/summarizer/langchain_summarizer.py) | Creates the summaries. Uses the shared retry decorator with optional per-summarizer overrides (see 2.4). |
193+
| summarizer | [`admin_api_lib.summarizer.summarizer.Summarizer`](./admin-api-lib/src/admin_api_lib/summarizer/summarizer.py) | [`admin_api_lib.impl.summarizer.langchain_summarizer.LangchainSummarizer`](./admin-api-lib/src/admin_api_lib/impl/summarizer/langchain_summarizer.py) | Creates the summaries. Uses the shared retry decorator with optional per-summarizer overrides (see 2.5). |
193194
| untraced_information_enhancer |[`admin_api_lib.information_enhancer.information_enhancer.InformationEnhancer`](./admin-api-lib/src/admin_api_lib/information_enhancer/information_enhancer.py) | [`admin_api_lib.impl.information_enhancer.general_enhancer.GeneralEnhancer`](./admin-api-lib/src/admin_api_lib/impl/information_enhancer/general_enhancer.py) | Uses the *summarizer* to enhance the extracted documents. |
194195
| information_enhancer | [`rag_core_lib.chains.async_chain.AsyncChain[Any, Any]`](./rag-core-lib/src/rag_core_lib/chains/async_chain.py)| [`rag_core_lib.impl.tracers.langfuse_traced_chain.LangfuseTracedGraph`](./rag-core-lib/src/rag_core_lib/impl/tracers/langfuse_traced_chain.py) |Wraps around the *untraced_information_enhancer* and adds langfuse tracing. |
195196
| document_deleter |[`admin_api_lib.api_endpoints.document_deleter.DocumentDeleter`](./admin-api-lib/src/admin_api_lib/api_endpoints/document_deleter.py) | [`admin_api_lib.impl.api_endpoints.default_document_deleter.DefaultDocumentDeleter`](./admin-api-lib/src/admin_api_lib/impl/api_endpoints/default_document_deleter.py) | Handles deletion of sources. |
@@ -198,7 +199,51 @@ The extracted information will be summarized using LLM. The summary, as well as
198199
| document_reference_retriever | [`admin_api_lib.api_endpoints.document_reference_retriever.DocumentReferenceRetriever`](./admin-api-lib/src/admin_api_lib/api_endpoints/document_reference_retriever.py) | [`admin_api_lib.impl.api_endpoints.default_document_reference_retriever.DefaultDocumentReferenceRetriever`](./admin-api-lib/src/admin_api_lib/impl/api_endpoints/default_document_reference_retriever.py) | Handles return of files from connected storage. |
199200
| file_uploader | [`admin_api_lib.api_endpoints.file_uploader.FileUploader`](./admin-api-lib/src/admin_api_lib/api_endpoints/file_uploader.py) | [`admin_api_lib.impl.api_endpoints.default_file_uploader.DefaultFileUploader`](./admin-api-lib/src/admin_api_lib/impl/api_endpoints/default_file_uploader.py) | Handles upload and extraction of files. |
200201

201-
### 2.4 Summarizer retry behavior
202+
### 2.4 Chunker configuration
203+
204+
The default dependency container exposes two chunking strategies via [`ChunkerSettings`](./admin-api-lib/src/admin_api_lib/impl/settings/chunker_settings.py):
205+
206+
- `recursive` (default) wraps LangChain's `RecursiveCharacterTextSplitter`.
207+
- `semantic` wraps LangChain's `SemanticChunker` and requires an embeddings backend.
208+
209+
You can switch between them and fine-tune their behaviour through environment variables:
210+
211+
| Setting | Description | Default |
212+
|---------|-------------|---------|
213+
| `CHUNKER_MODE` | Selects the implementation (`recursive` or `semantic`). | `recursive` |
214+
| `CHUNKER_MAX_SIZE` | Maximum character count per recursive chunk. | `1000` |
215+
| `CHUNKER_OVERLAP` | Character overlap between recursive chunks. | `100` |
216+
| `CHUNKER_SEMANTIC_BREAKPOINT_THRESHOLD_TYPE` | Breakpoint heuristic (`percentile`, `standard_deviation`, `interquartile`). | `percentile` |
217+
| `CHUNKER_SEMANTIC_BREAKPOINT_THRESHOLD` | Threshold associated with the selected heuristic. | `95.0` |
218+
| `CHUNKER_SEMANTIC_BUFFER_SIZE` | Context buffer that is kept on both sides of a semantic breakpoint. | `1` |
219+
| `CHUNKER_SEMANTIC_MIN_CHUNK_SIZE` | Minimum size for semantic chunks. | `200` |
220+
| `CHUNKER_SEMANTIC_MAX_CHUNK_SIZE` | Optional maximum size for semantic chunks (`None` when omitted). | `1200` |
221+
| `CHUNKER_SEMANTIC_TRIM_CHUNKS` | Whether to strip whitespace around semantic chunks. | `true` |
222+
223+
> 📌 The recursive chunker only uses the `CHUNKER_MAX_SIZE` and `CHUNKER_OVERLAP` knobs. The remaining keys are ignored unless `CHUNKER_MODE=semantic`.
224+
225+
#### Embeddings backend for semantic chunking
226+
227+
When `CHUNKER_MODE` is set to `semantic`, the dependency container selects embeddings using [`EmbedderClassTypeSettings`](./rag-core-lib/src/rag_core_lib/impl/settings/embedder_class_type_settings.py). Configure the backend via:
228+
229+
- `EMBEDDER_CLASS_TYPE_EMBEDDER_TYPE`: choose one of `stackit`, `ollama`, or `fake`.
230+
231+
Backend-specific options:
232+
233+
- **STACKIT embeddings** (production default)
234+
- `STACKIT_EMBEDDER_MODEL`
235+
- `STACKIT_EMBEDDER_BASE_URL`
236+
- `STACKIT_EMBEDDER_API_KEY` *(required)*
237+
- Optional retry overrides: `STACKIT_EMBEDDER_MAX_RETRIES`, `STACKIT_EMBEDDER_RETRY_BASE_DELAY`, `STACKIT_EMBEDDER_RETRY_MAX_DELAY`, `STACKIT_EMBEDDER_BACKOFF_FACTOR`, `STACKIT_EMBEDDER_ATTEMPT_CAP`, `STACKIT_EMBEDDER_JITTER_MIN`, `STACKIT_EMBEDDER_JITTER_MAX`
238+
- **Ollama embeddings** (self-hosted)
239+
- `OLLAMA_EMBEDDER_MODEL`
240+
- `OLLAMA_EMBEDDER_BASE_URL`
241+
- **Fake embeddings** (testing)
242+
- `FAKE_EMBEDDER_SIZE`
243+
244+
In the Helm chart set `CHUNKER_*` keys under `adminBackend.envs.chunker`. The admin deployment reuses the embedder config maps from the backend release, so adjust `backend.envs.embedderClassTypes`, `backend.envs.stackitEmbedder`, `backend.envs.ollamaEmbedder`, or `backend.envs.fakeEmbedder` accordingly when switching embeddings for semantic chunking.
245+
246+
### 2.5 Summarizer retry behavior
202247

203248
The default summarizer implementation (`LangchainSummarizer`) now uses the shared retry decorator with exponential backoff from the `rag-core-lib`.
204249

libs/admin-api-lib/src/admin_api_lib/dependency_container.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22

33
from admin_api_lib.impl.api_endpoints.default_file_uploader import DefaultFileUploader
44
from dependency_injector.containers import DeclarativeContainer
5-
from dependency_injector.providers import ( # noqa: WOT001
6-
Configuration,
7-
List,
8-
Selector,
9-
Singleton,
10-
)
5+
from dependency_injector.providers import Configuration, List, Selector, Singleton
116
from langchain.text_splitter import RecursiveCharacterTextSplitter
7+
from langchain_community.embeddings import OllamaEmbeddings
8+
from langchain_core.embeddings.fake import FakeEmbeddings
129
from langfuse import Langfuse
1310

1411
from admin_api_lib.extractor_api_client.openapi_client.api.extractor_api import (
@@ -29,6 +26,7 @@
2926
from admin_api_lib.impl.api_endpoints.default_documents_status_retriever import (
3027
DefaultDocumentsStatusRetriever,
3128
)
29+
from admin_api_lib.impl.chunker.semantic_text_chunker import SemanticTextChunker
3230
from admin_api_lib.impl.chunker.text_chunker import TextChunker
3331
from admin_api_lib.impl.file_services.s3_service import S3Service
3432
from admin_api_lib.impl.information_enhancer.general_enhancer import GeneralEnhancer
@@ -59,17 +57,26 @@
5957
from admin_api_lib.rag_backend_client.openapi_client.configuration import (
6058
Configuration as RagConfiguration,
6159
)
60+
from rag_core_lib.impl.embeddings.langchain_community_embedder import (
61+
LangchainCommunityEmbedder,
62+
)
63+
from rag_core_lib.impl.embeddings.stackit_embedder import StackitEmbedder
6264
from rag_core_lib.impl.langfuse_manager.langfuse_manager import LangfuseManager
6365
from rag_core_lib.impl.llms.llm_factory import chat_model_provider
66+
from rag_core_lib.impl.settings.embedder_class_type_settings import (
67+
EmbedderClassTypeSettings,
68+
)
69+
from rag_core_lib.impl.settings.fake_embedder_settings import FakeEmbedderSettings
6470
from rag_core_lib.impl.settings.langfuse_settings import LangfuseSettings
71+
from rag_core_lib.impl.settings.ollama_embedder_settings import OllamaEmbedderSettings
6572
from rag_core_lib.impl.settings.ollama_llm_settings import OllamaSettings
6673
from rag_core_lib.impl.settings.rag_class_types_settings import RAGClassTypeSettings
6774
from rag_core_lib.impl.settings.retry_decorator_settings import RetryDecoratorSettings
75+
from rag_core_lib.impl.settings.stackit_embedder_settings import StackitEmbedderSettings
6876
from rag_core_lib.impl.settings.stackit_vllm_settings import StackitVllmSettings
6977
from rag_core_lib.impl.tracers.langfuse_traced_runnable import LangfuseTracedRunnable
7078
from rag_core_lib.impl.utils.async_threadsafe_semaphore import AsyncThreadsafeSemaphore
7179

72-
7380
class DependencyContainer(DeclarativeContainer):
7481
"""Dependency injection container for managing application dependencies."""
7582

@@ -78,6 +85,10 @@ class DependencyContainer(DeclarativeContainer):
7885
# Settings
7986
s3_settings = S3Settings()
8087
chunker_settings = ChunkerSettings()
88+
chunker_embedder_type_settings = EmbedderClassTypeSettings()
89+
stackit_chunker_embedder_settings = StackitEmbedderSettings()
90+
ollama_chunker_embedder_settings = OllamaEmbedderSettings()
91+
fake_chunker_embedder_settings = FakeEmbedderSettings()
8192
ollama_settings = OllamaSettings()
8293
langfuse_settings = LangfuseSettings()
8394
stackit_vllm_settings = StackitVllmSettings()
@@ -96,7 +107,40 @@ class DependencyContainer(DeclarativeContainer):
96107
chunk_size=chunker_settings.max_size, chunk_overlap=chunker_settings.overlap
97108
)
98109

99-
chunker = Singleton(TextChunker, text_splitter)
110+
semantic_chunker_embeddings = Selector(
111+
chunker_embedder_type_settings.embedder_type,
112+
stackit=Singleton(
113+
StackitEmbedder,
114+
stackit_chunker_embedder_settings,
115+
retry_decorator_settings,
116+
),
117+
ollama=Singleton(
118+
LangchainCommunityEmbedder,
119+
embedder=Singleton(
120+
OllamaEmbeddings,
121+
model=ollama_chunker_embedder_settings.model,
122+
base_url=ollama_chunker_embedder_settings.base_url,
123+
),
124+
),
125+
fake=Singleton(FakeEmbeddings, size=fake_chunker_embedder_settings.size),
126+
)
127+
128+
semantic_chunker = Singleton(
129+
SemanticTextChunker,
130+
embeddings=semantic_chunker_embeddings,
131+
breakpoint_threshold_type=chunker_settings.semantic_breakpoint_threshold_type,
132+
breakpoint_threshold=chunker_settings.semantic_breakpoint_threshold,
133+
buffer_size=chunker_settings.semantic_buffer_size,
134+
min_chunk_size=chunker_settings.semantic_min_chunk_size,
135+
max_chunk_size=chunker_settings.semantic_max_chunk_size,
136+
trim_chunks=chunker_settings.semantic_trim_chunks,
137+
)
138+
139+
chunker = Selector(
140+
chunker_settings.mode,
141+
recursive=Singleton(TextChunker, text_splitter),
142+
semantic=semantic_chunker,
143+
)
100144
extractor_api_configuration = Singleton(ExtractorConfiguration, host=document_extractor_settings.host)
101145
document_extractor_api_client = Singleton(ApiClient, extractor_api_configuration)
102146
document_extractor = Singleton(ExtractorApi, document_extractor_api_client)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Semantic text chunker backed by LangChain's semantic splitter."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Iterable
6+
from inspect import signature
7+
from typing import Any, Type
8+
9+
from langchain_core.documents import Document
10+
from langchain_core.embeddings import Embeddings
11+
from langchain_text_splitters import SemanticChunker as LangchainSemanticChunker
12+
13+
from admin_api_lib.chunker.chunker import Chunker
14+
15+
16+
class SemanticTextChunker(Chunker):
17+
"""Wrap the LangChain semantic chunker behind the local ``Chunker`` interface."""
18+
19+
def __init__(
20+
self,
21+
embeddings: Embeddings,
22+
*,
23+
breakpoint_threshold_type: str | None = None,
24+
breakpoint_threshold: float | None = None,
25+
buffer_size: int | None = None,
26+
min_chunk_size: int | None = None,
27+
max_chunk_size: int | None = None,
28+
trim_chunks: bool | None = None,
29+
chunker_cls: Type[LangchainSemanticChunker] = LangchainSemanticChunker,
30+
) -> None:
31+
"""Initialise the semantic chunker.
32+
33+
Parameters
34+
----------
35+
embeddings : Embeddings
36+
The embeddings backend that powers semantic similarity detection.
37+
breakpoint_threshold_type : str | None, optional
38+
Strategy used to derive semantic breakpoints. Unsupported values are ignored.
39+
breakpoint_threshold : float | None, optional
40+
Threshold to apply for the selected breakpoint strategy. Unsupported values are ignored.
41+
buffer_size : int | None, optional
42+
Number of neighbouring sentences to include for context. Unsupported values are ignored.
43+
min_chunk_size : int | None, optional
44+
Minimum chunk size enforced by the chunker. Unsupported values are ignored.
45+
max_chunk_size : int | None, optional
46+
Maximum chunk size enforced by the chunker. Unsupported values are ignored.
47+
trim_chunks : bool | None, optional
48+
Whether to strip whitespace from chunk boundaries. Unsupported values are ignored.
49+
chunker_cls : type[LangchainSemanticChunker], optional
50+
Concrete semantic chunker implementation to instantiate. Defaults to
51+
:class:`langchain_text_splitters.SemanticChunker`.
52+
"""
53+
54+
init_params = _supported_parameters(chunker_cls)
55+
candidate_kwargs: dict[str, Any] = {
56+
"breakpoint_threshold_type": breakpoint_threshold_type,
57+
"breakpoint_threshold": breakpoint_threshold,
58+
"buffer_size": buffer_size,
59+
"min_chunk_size": min_chunk_size,
60+
"max_chunk_size": max_chunk_size,
61+
"trim_chunks": trim_chunks,
62+
}
63+
filtered_kwargs = {
64+
key: value
65+
for key, value in candidate_kwargs.items()
66+
if value is not None and key in init_params
67+
}
68+
69+
self._semantic_chunker = chunker_cls(
70+
embeddings=embeddings,
71+
**filtered_kwargs,
72+
)
73+
74+
def chunk(self, documents: Iterable[Document]) -> list[Document]:
75+
"""Split documents using the configured semantic splitter.
76+
77+
Parameters
78+
----------
79+
documents : Iterable[Document]
80+
Documents to be chunked.
81+
82+
Returns
83+
-------
84+
list[Document]
85+
Chunked documents produced by the semantic splitter.
86+
"""
87+
88+
documents_list = list(documents)
89+
if not documents_list:
90+
return []
91+
return self._semantic_chunker.split_documents(documents_list)
92+
93+
94+
def _supported_parameters(chunker_cls: type) -> set[str]:
95+
"""Return constructor parameters supported by ``chunker_cls``.
96+
97+
Parameters
98+
----------
99+
chunker_cls : type
100+
Semantic chunker class whose constructor signature should be inspected.
101+
102+
Returns
103+
-------
104+
set[str]
105+
Set of keyword-parameter names accepted by the constructor.
106+
"""
107+
108+
try:
109+
params = signature(chunker_cls.__init__).parameters
110+
except (TypeError, ValueError): # pragma: no cover - defensive, should not occur
111+
return set()
112+
return {name for name in params if name != "self"}
Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,43 @@
11
"""Contains settings regarding the chunker."""
22

3-
from pydantic import Field
3+
from typing import Literal
4+
5+
from pydantic import Field, model_validator
46
from pydantic_settings import BaseSettings
57

68

79
class ChunkerSettings(BaseSettings):
8-
"""Contains settings regarding the chunker.
9-
10-
Attributes
11-
----------
12-
max_size (int): The maximum size of the chunks.
13-
overlap (int): The overlap between the chunks.
14-
"""
10+
"""Contains settings regarding the chunker configuration."""
1511

1612
class Config:
1713
"""Config class for reading Fields from env."""
1814

1915
env_prefix = "CHUNKER_"
2016
case_sensitive = False
2117

22-
max_size: int = Field(default=1000)
23-
overlap: int = Field(default=100)
18+
mode: Literal["recursive", "semantic"] = Field(default="recursive")
19+
max_size: int = Field(default=1000, gt=0)
20+
overlap: int = Field(default=100, ge=0)
21+
22+
semantic_breakpoint_threshold_type: Literal[
23+
"percentile",
24+
"standard_deviation",
25+
"interquartile",
26+
] = Field(default="percentile")
27+
semantic_breakpoint_threshold: float = Field(default=95.0, ge=0.0)
28+
semantic_buffer_size: int = Field(default=1, ge=0)
29+
semantic_min_chunk_size: int = Field(default=200, gt=0)
30+
semantic_max_chunk_size: int | None = Field(default=1200, gt=0)
31+
semantic_trim_chunks: bool = Field(default=True)
32+
33+
@model_validator(mode="after")
34+
def _validate_min_max(self) -> "ChunkerSettings":
35+
if self.mode != "semantic":
36+
return self
37+
if (
38+
self.semantic_max_chunk_size is not None
39+
and self.semantic_min_chunk_size > self.semantic_max_chunk_size
40+
):
41+
msg = "semantic_min_chunk_size cannot exceed semantic_max_chunk_size"
42+
raise ValueError(msg)
43+
return self

0 commit comments

Comments
 (0)