diff --git a/infrastructure/rag/templates/admin-backend/deployment.yaml b/infrastructure/rag/templates/admin-backend/deployment.yaml
index 53797b8c..3f100ca3 100644
--- a/infrastructure/rag/templates/admin-backend/deployment.yaml
+++ b/infrastructure/rag/templates/admin-backend/deployment.yaml
@@ -23,6 +23,8 @@ spec:
           emptyDir: {}
         - name: tmp-dir
           emptyDir: {}
+        - name: nltk-data-dir
+          emptyDir: {}
       {{- if .Values.shared.imagePullSecret }}
       imagePullSecrets:
         - name: {{ .Values.shared.imagePullSecret.name }}
@@ -35,10 +37,16 @@ spec:
           - -c
           - |
             touch /app/services/admin-backend/log/logfile.log && \
-            chmod 600 /app/services/admin-backend/log/logfile.log
+            chmod 600 /app/services/admin-backend/log/logfile.log;
+            wget -q -O /tmp/punkt.zip https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/packages/tokenizers/punkt_tab.zip && \
+            unzip /tmp/punkt.zip -d /home/nonroot/nltk_data/tokenizers && \
+            wget -q -O /tmp/averaged_perceptron_tagger_eng.zip https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/packages/taggers/averaged_perceptron_tagger_eng.zip && \
+            unzip /tmp/averaged_perceptron_tagger_eng.zip -d /home/nonroot/nltk_data/taggers;
         volumeMounts:
         - name: log-dir
           mountPath: /app/services/admin-backend/log
+        - name: nltk-data-dir
+          mountPath: /home/nonroot/nltk_data
       containers:
       - name: {{ .Values.adminBackend.name }}
         securityContext:
@@ -108,6 +116,8 @@ spec:
               name: {{ template "configmap.sourceUploaderName" . }}
           - configMapRef:
               name: {{ template "configmap.retryDecoratorName" . }}
+          - configMapRef:
+              name: {{ template "configmap.chunkerName" . }}
           - secretRef:
               name: {{ template "secret.langfuseName" . }}
           - secretRef:
@@ -116,6 +126,8 @@ spec:
               name: {{ template "secret.s3Name" . }}
           - secretRef:
               name: {{ template "secret.stackitVllmName" . }}
+          - secretRef:
+              name: {{ template "secret.stackitEmbedderName" . }}
         env:
           - name: PYTHONPATH
             value: {{ .Values.adminBackend.pythonPathEnv.PYTHONPATH }}
diff --git a/infrastructure/rag/values.yaml b/infrastructure/rag/values.yaml
index 5f6cf7e4..c07c9768 100644
--- a/infrastructure/rag/values.yaml
+++ b/infrastructure/rag/values.yaml
@@ -338,8 +338,16 @@ adminBackend:
     ragapi:
       RAG_API_HOST: "http://backend:8080"
     chunker:
+      # Select which chunker implementation to use. Supported values: "semantic", "recursive"
+      # Defaults to "semantic" which leverages sentence-aware rebalancing.
+      CHUNKER_CLASS_TYPE_CHUNKER_TYPE: "recursive"
       CHUNKER_MAX_SIZE: 1000
-      CHUNKER_OVERLAP: 300
+      CHUNKER_OVERLAP: 100
+      # The following settings for the Chunker are only used when CHUNKER_CLASS_TYPE_CHUNKER_TYPE is set to "semantic".
+      CHUNKER_BREAKPOINT_THRESHOLD_TYPE: "percentile"
+      CHUNKER_BREAKPOINT_THRESHOLD_AMOUNT: 95
+      CHUNKER_BUFFER_SIZE: 1
+      CHUNKER_MIN_SIZE: 200
     keyValueStore:
       USECASE_KEYVALUE_PORT: 6379
       USECASE_KEYVALUE_HOST: "rag-keydb"
diff --git a/libs/README.md b/libs/README.md
index fd13a9a5..cf8efffb 100644
--- a/libs/README.md
+++ b/libs/README.md
@@ -13,7 +13,8 @@ It consists of the following python packages:
   - [2.1 Requirements](#21-requirements)
   - [2.2 Endpoints](#22-endpoints)
   - [2.3 Replaceable parts](#23-replaceable-parts)
-  - [2.4 Summarizer retry behavior](#24-summarizer-retry-behavior)
+  - [2.4 Chunker configuration](#24-chunker-configuration-multiple-chunkers)
+  - [2.5 Summarizer retry behavior](#25-summarizer-retry-behavior)
 - [`3. Extractor API lib`](#3-extractor-api-lib)
   - [3.1 Requirements](#31-requirements)
   - [3.2 Endpoints](#32-endpoints)
@@ -84,20 +85,20 @@ Uploaded documents are required to contain the following metadata:
 
 | Name | Type | Default | Notes |
 |----------|---------|--------------|--------------|
-| embedder | [`rag_core_api.embeddings.embedder.Embedder`](./rag-core-api/src/rag_core_api/embeddings/embedder.py) | Depends on your settings. Can be [`rag_core_api.impl.embeddings.langchain_community_embedder.LangchainCommunityEmbedder`](./rag-core-api/src/rag_core_api/impl/embeddings/langchain_community_embedder.py) or [`rag_core_api.impl.embeddings.stackit_embedder.StackitEmbedder`](./rag-core-api/src/rag_core_api/impl/embeddings/stackit_embedder.py) | Selected by EmbedderClassTypeSettings.embedder_type. |
+| embedder | [`rag_core_lib.impl.embeddings.embedder.Embedder`](./rag-core-lib/src/rag_core_lib/impl/embeddings/embedder.py) | Depends on your settings. Can be [`rag_core_lib.impl.embeddings.langchain_community_embedder.LangchainCommunityEmbedder`](./rag-core-lib/src/rag_core_lib/impl/embeddings/langchain_community_embedder.py) or [`rag_core_lib.impl.embeddings.stackit_embedder.StackitEmbedder`](./rag-core-lib/src/rag_core_lib/impl/embeddings/stackit_embedder.py) | Selected by [`rag_core_lib.impl.settings.embedder_class_type_settings.EmbedderClassTypeSettings.embedder_type`](./rag-core-lib/src/rag_core_lib/impl/settings/embedder_class_type_settings.py). |
 | vector_database | [`rag_core_api.vector_databases.vector_database.VectorDatabase`](./rag-core-api/src/rag_core_api/vector_databases/vector_database.py) | [`rag_core_api.impl.vector_databases.qdrant_database.QdrantDatabase`](./rag-core-api/src/rag_core_api/impl/vector_databases/qdrant_database.py) | |
 | reranker | [`rag_core_api.reranking.reranker.Reranker`](./rag-core-api/src/rag_core_api/reranking/reranker.py)  | [`rag_core_api.impl.reranking.flashrank_reranker.FlashrankReranker`](./rag-core-api/src/rag_core_api/impl/reranking/flashrank_reranker.py) | Used in the *composed_retriever* |
 | composed_retriever | [`rag_core_api.retriever.retriever.Retriever`](./rag-core-api/src/rag_core_api/retriever/retriever.py) | [`rag_core_api.impl.retriever.composite_retriever.CompositeRetriever`](./rag-core-api/src/rag_core_api/impl/retriever/composite_retriever.py) | Handles retrieval, re-ranking, etc. |
-| large_language_model | `langchain_core.language_models.llms.BaseLLM` | `langchain_community.llms.vllm.VLLMOpenAI`, `langchain_community.llms.Ollama` or `langchain_community.llms.FakeListLLM` | The LLm that is used for all LLM tasks. The default depends on the value of `rag_core_lib.impl.settings.rag_class_types_settings.RAGClassTypeSettings.llm_type`. The FakeListLLM is used for testing |
+| large_language_model | `langchain_core.language_models.chat_models.BaseChatModel` | Provided via [`rag_core_lib.impl.llms.llm_factory.chat_model_provider`](./rag-core-lib/src/rag_core_lib/impl/llms/llm_factory.py): `langchain_openai.ChatOpenAI` or `langchain_ollama.ChatOllama` | The LLM used for all LLM tasks. The default depends on `rag_core_lib.impl.settings.rag_class_types_settings.RAGClassTypeSettings.llm_type`. A fake model is used in tests. |
 | prompt | `str` | [`rag_core_api.prompt_templates.answer_generation_prompt.ANSWER_GENERATION_PROMPT`](./rag-core-api/src/rag_core_api/prompt_templates/answer_generation_prompt.py) | The prompt used for answering the question. |
-| rephrasing_prompt | `str` |  [`rag_core_api.prompt_templates.question_rephrasing_prompt.ANSWER_REPHRASING_PROMPT`](./rag-core-api/src/rag_core_api/prompt_templates/question_rephrasing_prompt.py) | The prompt used for rephrasing the question. The rephrased question (and the *original* question are both used for retrival of the documents) |
+| rephrasing_prompt | `str` |  [`rag_core_api.prompt_templates.question_rephrasing_prompt.QUESTION_REPHRASING_PROMPT`](./rag-core-api/src/rag_core_api/prompt_templates/question_rephrasing_prompt.py) | The prompt used for rephrasing the question. The rephrased question (and the *original* question are both used for retrieval of the documents). |
 | language_detection_prompt | `str` | [`rag_core_api.prompt_templates.language_detection_prompt.LANGUAGE_DETECTION_PROMPT`](./rag-core-api/src/rag_core_api/prompt_templates/language_detection_prompt.py) | Prompt for detecting input language. Enforces structured JSON output `{ "language": "<iso639-1>" }` and defaults to `en` when uncertain. |
 | 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. |
-| answer_generation_chain | [`rag_core_lib.chains.runnables.AsyncRunnable[rag_core_api.impl.graph.graph_state.graph_state.AnswerGraphState, str]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_api.impl.answer_generation_chains.answer_generation_chain.AnswerGenerationChain`](./rag-core-api/src/rag_core_api/impl/answer_generation_chains/answer_generation_chain.py) | LangChain chain used for answering the question. Is part of the *chat_graph*, |
-| rephrasing_chain | [`rag_core_lib.chains.runnables.AsyncRunnable[rag_core_api.impl.graph.graph_state.graph_state.AnswerGraphState, str]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_api.impl.answer_generation_chains.rephrasing_chain.RephrasingChain`](./rag-core-api/src/rag_core_api/impl/answer_generation_chains/rephrasing_chain.py) | LangChain chain used for rephrasing the question. Is part of the *chat_graph*. |
-| language_detection_chain | [`rag_core_lib.chains.runnables.AsyncRunnable[rag_core_api.impl.graph.graph_state.graph_state.AnswerGraphState, str]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_api.impl.answer_generation_chains.language_detection_chain.LanguageDetectionChain`](./rag-core-api/src/rag_core_api/impl/answer_generation_chains/language_detection_chain.py) | Detects the language of the question and returns an ISO 639-1 code (e.g., `en`, `de`). Uses structured-output guidance and robust parsing with fallback to `en`. Part of the *chat_graph*. |
+| answer_generation_chain | [`rag_core_lib.runnables.AsyncRunnable[rag_core_api.impl.graph.graph_state.graph_state.AnswerGraphState, str]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_api.impl.answer_generation_chains.answer_generation_chain.AnswerGenerationChain`](./rag-core-api/src/rag_core_api/impl/answer_generation_chains/answer_generation_chain.py) | LangChain chain used for answering the question. Is part of the *chat_graph*. |
+| rephrasing_chain | [`rag_core_lib.runnables.AsyncRunnable[rag_core_api.impl.graph.graph_state.graph_state.AnswerGraphState, str]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_api.impl.answer_generation_chains.rephrasing_chain.RephrasingChain`](./rag-core-api/src/rag_core_api/impl/answer_generation_chains/rephrasing_chain.py) | LangChain chain used for rephrasing the question. Is part of the *chat_graph*. |
+| language_detection_chain | [`rag_core_lib.runnables.AsyncRunnable[rag_core_api.impl.graph.graph_state.graph_state.AnswerGraphState, str]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_api.impl.answer_generation_chains.language_detection_chain.LanguageDetectionChain`](./rag-core-api/src/rag_core_api/impl/answer_generation_chains/language_detection_chain.py) | Detects the language of the question and returns an ISO 639-1 code (e.g., `en`, `de`). Uses structured-output guidance and robust parsing with fallback to `en`. Part of the *chat_graph*. |
 | chat_graph | [`rag_core_api.graph.graph_base.GraphBase`](./rag-core-api/src/rag_core_api/graph/graph_base.py) | [`rag_core_api.impl.graph.chat_graph.DefaultChatGraph`](./rag-core-api/src/rag_core_api/impl/graph/chat_graph.py) | Langgraph graph that contains the entire logic for question answering. |
-| traced_chat_graph | [`rag_core_lib.chains.runnables.AsyncRunnable[Any, Any]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.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 *chat_graph* and add langfuse tracing. |
+| traced_chat_graph | [`rag_core_lib.runnables.AsyncRunnable[Any, Any]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_lib.impl.tracers.langfuse_traced_runnable.LangfuseTracedRunnable`](./rag-core-lib/src/rag_core_lib/impl/tracers/langfuse_traced_runnable.py) | Wraps around the *chat_graph* and adds Langfuse tracing. |
 | evaluator | [`rag_core_api.impl.evaluator.langfuse_ragas_evaluator.LangfuseRagasEvaluator`](./rag-core-api/src/rag_core_api/impl/evaluator/langfuse_ragas_evaluator.py) | [`rag_core_api.impl.evaluator.langfuse_ragas_evaluator.LangfuseRagasEvaluator`](./rag-core-api/src/rag_core_api/impl/evaluator/langfuse_ragas_evaluator.py) | The evaulator used in the evaluate endpoint. |
 | chat_endpoint | [`rag_core_api.api_endpoints.chat.Chat`](./rag-core-api/src/rag_core_api/api_endpoints/chat.py) | [`rag_core_api.impl.api_endpoints.default_chat.DefaultChat`](./rag-core-api/src/rag_core_api/impl/api_endpoints/default_chat.py) | Implementation of the chat endpoint. Default implementation just calls the *traced_chat_graph* |
 | ragas_llm | `langchain_core.language_models.chat_models.BaseChatModel` | `langchain_openai.ChatOpenAI` or `langchain_ollama.ChatOllama` | The LLM used for the ragas evaluation. |
@@ -108,7 +109,7 @@ The default STACKIT embedder implementation (`StackitEmbedder`) uses the shared
 
 - Decorator: `rag_core_lib.impl.utils.retry_decorator.retry_with_backoff`
 - Base settings (fallback): [`RetryDecoratorSettings`](./rag-core-lib/src/rag_core_lib/impl/settings/retry_decorator_settings.py)
-- Per-embedder overrides: [`StackitEmbedderSettings`](./rag-core-api/src/rag_core_api/impl/settings/stackit_embedder_settings.py)
+- Per-embedder overrides: [`StackitEmbedderSettings`](./rag-core-lib/src/rag_core_lib/impl/settings/stackit_embedder_settings.py)
 
 How it resolves settings
 
@@ -184,23 +185,71 @@ The extracted information will be summarized using LLM. The summary, as well as
 | Name | Type | Default | Notes |
 |----------|---------|--------------|--------------|
 | file_service | [`admin_api_lib.file_services.file_service.FileService`](./admin-api-lib/src/admin_api_lib/file_services/file_service.py) | [`admin_api_lib.impl.file_services.s3_service.S3Service`](./admin-api-lib/src/admin_api_lib/impl/file_services/s3_service.py) | Handles operations on the connected storage. |
-| large_language_model | `langchain_core.language_models.llms.BaseLLM` | `langchain_community.llms.vllm.VLLMOpenAI` or `langchain_community.llms.Ollama` | The LLm that is used for all LLM tasks. The default depends on the value of `rag_core_lib.impl.settings.rag_class_types_settings.RAGClassTypeSettings.llm_type` |
+| large_language_model | `langchain_core.language_models.chat_models.BaseChatModel` | Provided via [`rag_core_lib.impl.llms.llm_factory.chat_model_provider`](./rag-core-lib/src/rag_core_lib/impl/llms/llm_factory.py): `langchain_openai.ChatOpenAI` or `langchain_ollama.ChatOllama` | The LLM used for all LLM tasks. The default depends on `rag_core_lib.impl.settings.rag_class_types_settings.RAGClassTypeSettings.llm_type`. |
+| semantic_chunker_embeddings | [`admin_api_lib.chunker.chunker.Chunker`](./admin-api-lib/src/admin_api_lib/chunker/chunker.py) | Depends on your settings. Can be [`rag_core_lib.impl.embeddings.langchain_community_embedder.LangchainCommunityEmbedder`](./rag-core-lib/src/rag_core_lib/impl/embeddings/langchain_community_embedder.py) or [`rag_core_lib.impl.embeddings.stackit_embedder.StackitEmbedder`](./rag-core-lib/src/rag_core_lib/impl/embeddings/stackit_embedder.py) | Selected by [`rag_core_lib.impl.settings.embedder_class_type_settings.EmbedderClassTypeSettings.embedder_type`](./rag-core-lib/src/rag_core_lib/impl/settings/embedder_class_type_settings.py). Can be `recursive` or `semantic`. |
 | key_value_store | [`admin_api_lib.impl.key_db.file_status_key_value_store.FileStatusKeyValueStore`](./admin-api-lib/src/admin_api_lib/impl/key_db/file_status_key_value_store.py) | [`admin_api_lib.impl.key_db.file_status_key_value_store.FileStatusKeyValueStore`](./admin-api-lib/src/admin_api_lib/impl/key_db/file_status_key_value_store.py) | Is used for storing the available sources and their current state. |
-| chunker |  [`admin_api_lib.chunker.chunker.Chunker`](./admin-api-lib/src/admin_api_lib/chunker/chunker.py) | [`admin_api_lib.impl.chunker.text_chunker.TextChunker`](./admin-api-lib/src/admin_api_lib/impl/chunker/text_chunker.py) | Used for splitting the documents in managable chunks. |
+| chunker |  [`admin_api_lib.chunker.chunker.Chunker`](./admin-api-lib/src/admin_api_lib/chunker/chunker.py) | [`admin_api_lib.impl.chunker.text_chunker.TextChunker`](./admin-api-lib/src/admin_api_lib/impl/chunker/text_chunker.py) or [`admin_api_lib.impl.chunker.semantic_text_chunker.SemanticTextChunker`](./admin-api-lib/src/admin_api_lib/impl/chunker/semantic_text_chunker.py) | Splits documents into chunks. Select implementation via `CHUNKER_CLASS_TYPE_CHUNKER_TYPE` (`recursive` or `semantic`). |
 | document_extractor | [`admin_api_lib.extractor_api_client.openapi_client.api.extractor_api.ExtractorApi`](./admin-api-lib/src/admin_api_lib/extractor_api_client/openapi_client/api/extractor_api.py) | [`admin_api_lib.extractor_api_client.openapi_client.api.extractor_api.ExtractorApi`](./admin-api-lib/src/admin_api_lib/extractor_api_client/openapi_client/api/extractor_api.py) | Needs to be replaced if adjustments to the `extractor-api` is made. |
 | 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. |
 | 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. |
 | 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. |
-| 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). |
+| 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). |
 | 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. |
-| information_enhancer |  [`rag_core_lib.chains.runnables.AsyncRunnable[Any, Any]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.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. |
+| information_enhancer |  [`rag_core_lib.runnables.AsyncRunnable[Any, Any]`](./rag-core-lib/src/rag_core_lib/runnables/async_runnable.py) | [`rag_core_lib.impl.tracers.langfuse_traced_runnable.LangfuseTracedRunnable`](./rag-core-lib/src/rag_core_lib/impl/tracers/langfuse_traced_runnable.py) | Wraps around the *untraced_information_enhancer* and adds Langfuse tracing. |
 | 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. |
 | documents_status_retriever |  [`admin_api_lib.api_endpoints.documents_status_retriever.DocumentsStatusRetriever`](./admin-api-lib/src/admin_api_lib/api_endpoints/documents_status_retriever.py) | [`admin_api_lib.impl.api_endpoints.default_documents_status_retriever.DefaultDocumentsStatusRetriever`](./admin-api-lib/src/admin_api_lib/impl/api_endpoints/default_documents_status_retriever.py) |Handles return of source status. |
 | source_uploader | [`admin_api_lib.api_endpoints.source_uploader.SourceUploader`](./admin-api-lib/src/admin_api_lib/api_endpoints/source_uploader.py) | [`admin_api_lib.impl.api_endpoints.default_source_uploader.DefaultSourceUploader`](./admin-api-lib/src/admin_api_lib/impl/api_endpoints/default_source_uploader.py)| Handles data loading and extraction from various non-file sources. |
 | 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. |
 | 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. |
 
-### 2.4 Summarizer retry behavior
+### 2.4 Chunker configuration (multiple chunkers)
+
+The default dependency container now exposes two chunking strategies which can be chosen by [`ChunkerClassTypeSettings`](./admin-api-lib/src/admin_api_lib/impl/settings/chunker_class_type_settings.py):
+
+- `recursive` (default) wraps LangChain's `RecursiveCharacterTextSplitter`.
+- `semantic` wraps LangChain's `SemanticChunker`, and considers minimum/maximum chunk size with `nltk`/`RecursiveCharacterTextSplitter`.
+
+You can switch between them and fine-tune their behaviour through environment variables:
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `CHUNKER_MAX_SIZE` | Maximum character count per recursive chunk. | `1000` |
+| `CHUNKER_OVERLAP` | Character overlap between recursive chunks. | `100` |
+|||
+| `CHUNKER_BREAKPOINT_THRESHOLD_TYPE` | Breakpoint heuristic (`percentile`, `standard_deviation`, `interquartile`). | `percentile` |
+| `CHUNKER_BREAKPOINT_THRESHOLD_AMOUNT` | Threshold associated with the selected heuristic. | `95.0` |
+| `CHUNKER_BUFFER_SIZE` | Context buffer that is kept on both sides of a semantic breakpoint. | `1` |
+| `CHUNKER_MIN_SIZE` | Minimum size for semantic chunks. | `200` |
+
+> 📌 The recursive chunker only uses the `CHUNKER_MAX_SIZE` and `CHUNKER_OVERLAP` knobs. The remaining keys are ignored unless `CHUNKER_CLASS_TYPE_CHUNKER_TYPE=semantic`.
+
+Behavior details
+
+- Recursive chunker enforces `CHUNKER_MAX_SIZE` and `CHUNKER_OVERLAP` only.
+- Semantic chunker uses embeddings to detect semantic breakpoints and can also enforce `min`/`max` sizes:
+  - Oversized chunks are re-split with `RecursiveCharacterTextSplitter` (auto-provisioned when `max > min`).
+  - Trailing undersized chunks are rebalanced using sentence-aware splitting (NLTK Punkt when available, regex fallback otherwise) to avoid tiny tails while respecting `[min, max]`.
+
+#### Embeddings backend for semantic chunking
+
+When `CHUNKER_CLASS_TYPE_CHUNKER_TYPE` 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:
+
+- `EMBEDDER_CLASS_TYPE_EMBEDDER_TYPE`: choose one of `stackit`, `ollama`.
+
+Backend-specific options:
+
+- **STACKIT embeddings** (production default)
+  - `STACKIT_EMBEDDER_MODEL`
+  - `STACKIT_EMBEDDER_BASE_URL`
+  - `STACKIT_EMBEDDER_API_KEY` *(required)*
+  - 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`
+- **Ollama embeddings** (self-hosted)
+  - `OLLAMA_EMBEDDER_MODEL`
+  - `OLLAMA_EMBEDDER_BASE_URL`
+
+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.
+
+### 2.5 Summarizer retry behavior
 
 The default summarizer implementation (`LangchainSummarizer`) now uses the shared retry decorator with exponential backoff from the `rag-core-lib`.
 
diff --git a/libs/admin-api-lib/poetry.lock b/libs/admin-api-lib/poetry.lock
index 7584ba68..61caab33 100644
--- a/libs/admin-api-lib/poetry.lock
+++ b/libs/admin-api-lib/poetry.lock
@@ -1767,6 +1767,18 @@ files = [
     {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
 ]
 
+[[package]]
+name = "joblib"
+version = "1.5.2"
+description = "Lightweight pipelining with Python functions"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+    {file = "joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241"},
+    {file = "joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55"},
+]
+
 [[package]]
 name = "jsonpatch"
 version = "1.33"
@@ -1881,6 +1893,22 @@ PyYAML = ">=5.3.0,<7.0.0"
 tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
 typing-extensions = ">=4.7.0,<5.0.0"
 
+[[package]]
+name = "langchain-experimental"
+version = "0.3.4"
+description = "Building applications with LLMs through composability"
+optional = false
+python-versions = "<4.0,>=3.9"
+groups = ["main"]
+files = [
+    {file = "langchain_experimental-0.3.4-py3-none-any.whl", hash = "sha256:2e587306aea36b60fa5e5fc05dc7281bee9f60a806f0bf9d30916e0ee096af80"},
+    {file = "langchain_experimental-0.3.4.tar.gz", hash = "sha256:937c4259ee4a639c618d19acf0e2c5c2898ef127050346edc5655259aa281a21"},
+]
+
+[package.dependencies]
+langchain-community = ">=0.3.0,<0.4.0"
+langchain-core = ">=0.3.28,<0.4.0"
+
 [[package]]
 name = "langchain-openai"
 version = "0.3.27"
@@ -2186,6 +2214,32 @@ files = [
     {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
 ]
 
+[[package]]
+name = "nltk"
+version = "3.9.2"
+description = "Natural Language Toolkit"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+    {file = "nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a"},
+    {file = "nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419"},
+]
+
+[package.dependencies]
+click = "*"
+joblib = "*"
+regex = ">=2021.8.3"
+tqdm = "*"
+
+[package.extras]
+all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"]
+corenlp = ["requests"]
+machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"]
+plot = ["matplotlib"]
+tgrep = ["pyparsing"]
+twitter = ["twython"]
+
 [[package]]
 name = "numpy"
 version = "2.3.1"
@@ -4111,4 +4165,4 @@ cffi = ["cffi (>=1.11)"]
 [metadata]
 lock-version = "2.1"
 python-versions = "^3.13"
-content-hash = "fbb3f64bd7452a0811d43ea5caa0f49315fbb2dec5a84384b9b340c106ad2a98"
+content-hash = "7007b7093e9982fb6a64ee082e594d523b3c48e32b9c3e3ff9f425f632162f4a"
diff --git a/libs/admin-api-lib/pyproject.toml b/libs/admin-api-lib/pyproject.toml
index 082fe822..70698040 100644
--- a/libs/admin-api-lib/pyproject.toml
+++ b/libs/admin-api-lib/pyproject.toml
@@ -27,9 +27,9 @@ per-file-ignores = """
   ./src/admin_api_lib/prompt_templates/summarize_prompt.py: E501,
   ./src/admin_api_lib/apis/admin_api.py: B008,WOT001,
   ./src/admin_api_lib/impl/admin_api.py: B008,
-  ./src/admin_api_lib/dependency_container.py: CCE002,CCE001,
+  ./src/admin_api_lib/dependency_container.py: CCE002,CCE001,WOT001,
   ./src/admin_api_lib/apis/admin_api_base.py: WOT001,
-  ./tests/*: S101,S106,D100,D103,PT011,N802
+  ./tests/*: S101,S106,D100,D103,PT011,N802,E501,
   ./src/admin_api_lib/impl/settings/confluence_settings.py: C901,N805,
   ./src/admin_api_lib/impl/utils/comma_separated_bool_list.py: R505,
   ./src/admin_api_lib/impl/utils/comma_separated_str_list.py: R505,
@@ -109,10 +109,11 @@ redis = "^6.0.0"
 pyyaml = "^6.0.2"
 python-multipart = "^0.0.20"
 starlette = ">=0.47.2,<0.49.0"
-langchain-text-splitters = ">=0.3.9"
+langchain-experimental = "^0.3.4"
+nltk = "^3.9.2"
 
 [tool.pytest.ini_options]
-log_cli = 1
+log_cli = true
 log_cli_level = "DEBUG"
 pythonpath = "src"
 testpaths = "src/tests"
diff --git a/libs/admin-api-lib/src/admin_api_lib/dependency_container.py b/libs/admin-api-lib/src/admin_api_lib/dependency_container.py
index ee86cb5f..1b5572b5 100644
--- a/libs/admin-api-lib/src/admin_api_lib/dependency_container.py
+++ b/libs/admin-api-lib/src/admin_api_lib/dependency_container.py
@@ -2,13 +2,9 @@
 
 from admin_api_lib.impl.api_endpoints.default_file_uploader import DefaultFileUploader
 from dependency_injector.containers import DeclarativeContainer
-from dependency_injector.providers import (  # noqa: WOT001
-    Configuration,
-    List,
-    Selector,
-    Singleton,
-)
+from dependency_injector.providers import Configuration, List, Selector, Singleton
 from langchain.text_splitter import RecursiveCharacterTextSplitter
+from langchain_community.embeddings import OllamaEmbeddings
 from langfuse import Langfuse
 
 from admin_api_lib.extractor_api_client.openapi_client.api.extractor_api import (
@@ -29,6 +25,7 @@
 from admin_api_lib.impl.api_endpoints.default_documents_status_retriever import (
     DefaultDocumentsStatusRetriever,
 )
+from admin_api_lib.impl.chunker.semantic_text_chunker import SemanticTextChunker
 from admin_api_lib.impl.chunker.text_chunker import TextChunker
 from admin_api_lib.impl.file_services.s3_service import S3Service
 from admin_api_lib.impl.information_enhancer.general_enhancer import GeneralEnhancer
@@ -41,6 +38,7 @@
 from admin_api_lib.impl.mapper.informationpiece2document import (
     InformationPiece2Document,
 )
+from admin_api_lib.impl.settings.chunker_class_type_settings import ChunkerClassTypeSettings
 from admin_api_lib.impl.settings.chunker_settings import ChunkerSettings
 from admin_api_lib.impl.settings.document_extractor_settings import (
     DocumentExtractorSettings,
@@ -59,12 +57,21 @@
 from admin_api_lib.rag_backend_client.openapi_client.configuration import (
     Configuration as RagConfiguration,
 )
+from rag_core_lib.impl.embeddings.langchain_community_embedder import (
+    LangchainCommunityEmbedder,
+)
+from rag_core_lib.impl.embeddings.stackit_embedder import StackitEmbedder
 from rag_core_lib.impl.langfuse_manager.langfuse_manager import LangfuseManager
 from rag_core_lib.impl.llms.llm_factory import chat_model_provider
+from rag_core_lib.impl.settings.embedder_class_type_settings import (
+    EmbedderClassTypeSettings,
+)
 from rag_core_lib.impl.settings.langfuse_settings import LangfuseSettings
+from rag_core_lib.impl.settings.ollama_embedder_settings import OllamaEmbedderSettings
 from rag_core_lib.impl.settings.ollama_llm_settings import OllamaSettings
 from rag_core_lib.impl.settings.rag_class_types_settings import RAGClassTypeSettings
 from rag_core_lib.impl.settings.retry_decorator_settings import RetryDecoratorSettings
+from rag_core_lib.impl.settings.stackit_embedder_settings import StackitEmbedderSettings
 from rag_core_lib.impl.settings.stackit_vllm_settings import StackitVllmSettings
 from rag_core_lib.impl.tracers.langfuse_traced_runnable import LangfuseTracedRunnable
 from rag_core_lib.impl.utils.async_threadsafe_semaphore import AsyncThreadsafeSemaphore
@@ -74,10 +81,14 @@ class DependencyContainer(DeclarativeContainer):
     """Dependency injection container for managing application dependencies."""
 
     class_selector_config = Configuration()
+    chunker_selector_config = Configuration()
 
     # Settings
     s3_settings = S3Settings()
     chunker_settings = ChunkerSettings()
+    chunker_embedder_type_settings = EmbedderClassTypeSettings()
+    stackit_chunker_embedder_settings = StackitEmbedderSettings()
+    ollama_chunker_embedder_settings = OllamaEmbedderSettings()
     ollama_settings = OllamaSettings()
     langfuse_settings = LangfuseSettings()
     stackit_vllm_settings = StackitVllmSettings()
@@ -88,6 +99,10 @@ class DependencyContainer(DeclarativeContainer):
     summarizer_settings = SummarizerSettings()
     source_uploader_settings = SourceUploaderSettings()
     retry_decorator_settings = RetryDecoratorSettings()
+    chunker_type_settings = ChunkerClassTypeSettings()
+
+    class_selector_config.from_dict(rag_class_type_settings.model_dump() | chunker_embedder_type_settings.model_dump())
+    chunker_selector_config.from_dict(chunker_type_settings.model_dump())
 
     key_value_store = Singleton(FileStatusKeyValueStore, key_value_store_settings)
     file_service = Singleton(S3Service, s3_settings=s3_settings)
@@ -96,7 +111,40 @@ class DependencyContainer(DeclarativeContainer):
         chunk_size=chunker_settings.max_size, chunk_overlap=chunker_settings.overlap
     )
 
-    chunker = Singleton(TextChunker, text_splitter)
+    semantic_chunker_embeddings = Selector(
+        class_selector_config.embedder_type,
+        stackit=Singleton(
+            StackitEmbedder,
+            stackit_chunker_embedder_settings,
+            retry_decorator_settings,
+        ),
+        ollama=Singleton(
+            LangchainCommunityEmbedder,
+            embedder=Singleton(
+                OllamaEmbeddings,
+                model=ollama_chunker_embedder_settings.model,
+                base_url=ollama_chunker_embedder_settings.base_url,
+            ),
+        ),
+    )
+
+    semantic_chunker = Singleton(
+        SemanticTextChunker,
+        embeddings=semantic_chunker_embeddings,
+        breakpoint_threshold_type=chunker_settings.breakpoint_threshold_type,
+        breakpoint_threshold_amount=chunker_settings.breakpoint_threshold_amount,
+        buffer_size=chunker_settings.buffer_size,
+        min_chunk_size=chunker_settings.min_size,
+        max_chunk_size=chunker_settings.max_size,
+        recursive_text_splitter=text_splitter,
+        overlap=chunker_settings.overlap,
+    )
+
+    chunker = Selector(
+        chunker_selector_config.chunker_type,
+        recursive=Singleton(TextChunker, text_splitter),
+        semantic=semantic_chunker,
+    )
     extractor_api_configuration = Singleton(ExtractorConfiguration, host=document_extractor_settings.host)
     document_extractor_api_client = Singleton(ApiClient, extractor_api_configuration)
     document_extractor = Singleton(ExtractorApi, document_extractor_api_client)
diff --git a/libs/admin-api-lib/src/admin_api_lib/impl/chunker/chunker_type.py b/libs/admin-api-lib/src/admin_api_lib/impl/chunker/chunker_type.py
new file mode 100644
index 00000000..1b250b10
--- /dev/null
+++ b/libs/admin-api-lib/src/admin_api_lib/impl/chunker/chunker_type.py
@@ -0,0 +1,11 @@
+"""Module containing the ChunkerType enumeration."""
+
+from enum import StrEnum, unique
+
+
+@unique
+class ChunkerType(StrEnum):
+    """An enumeration representing different types of chunkers."""
+
+    SEMANTIC = "semantic"
+    RECURSIVE = "recursive"
diff --git a/libs/admin-api-lib/src/admin_api_lib/impl/chunker/semantic_text_chunker.py b/libs/admin-api-lib/src/admin_api_lib/impl/chunker/semantic_text_chunker.py
new file mode 100644
index 00000000..40e3f81b
--- /dev/null
+++ b/libs/admin-api-lib/src/admin_api_lib/impl/chunker/semantic_text_chunker.py
@@ -0,0 +1,252 @@
+"""Semantic text chunker backed by LangChain's semantic splitter.
+
+Adds optional max/min chunk size enforcement via RecursiveCharacterTextSplitter
+when both values are provided and ``max_chunk_size > min_chunk_size``.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Iterable
+import re
+from inspect import signature
+from typing import Any, Type
+import logging
+
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_experimental.text_splitter import SemanticChunker as LangchainSemanticChunker
+from langchain_text_splitters import RecursiveCharacterTextSplitter
+from nltk.tokenize import PunktSentenceTokenizer
+
+from admin_api_lib.chunker.chunker import Chunker
+
+
+logger = logging.getLogger(__name__)
+
+
+class SemanticTextChunker(Chunker):
+    """Semantic text chunker backed by LangChain's semantic splitter with optional max/min chunk size enforcement."""
+
+    def __init__(
+        self,
+        embeddings: Embeddings,
+        *,
+        breakpoint_threshold_type: str | None = None,
+        breakpoint_threshold_amount: float | None = None,
+        buffer_size: int | None = None,
+        min_chunk_size: int | None = None,
+        max_chunk_size: int | None = None,
+        overlap: int | None = None,
+        chunker_cls: Type[LangchainSemanticChunker] = LangchainSemanticChunker,
+        recursive_text_splitter: RecursiveCharacterTextSplitter | None = None,
+    ) -> None:
+        """Initialise the semantic chunker.
+
+        Parameters
+        ----------
+        embeddings : Embeddings
+            The embeddings backend that powers semantic similarity detection.
+        breakpoint_threshold_type : str | None, optional
+            Strategy used to derive semantic breakpoints.
+        breakpoint_threshold_amount : float | None, optional
+            Threshold to apply for the selected breakpoint strategy.
+        buffer_size : int | None, optional
+            Number of neighbouring sentences to include for context.
+        min_chunk_size : int | None, optional
+            Minimum chunk size enforced by the chunker.
+        max_chunk_size : int | None, optional
+            Maximum chunk size enforced by the chunker.
+        overlap : int | None, optional
+            Number of overlapping characters between chunks.
+        chunker_cls : type[LangchainSemanticChunker], optional
+            Concrete semantic chunker implementation to instantiate. Defaults to
+            :class:`langchain_text_splitters.SemanticChunker`.
+        recursive_text_splitter : RecursiveCharacterTextSplitter | None, optional
+            Optional pre-configured recursive text splitter to use for max/min chunk size enforcement.
+        """
+        self._min_chunk_size = min_chunk_size
+        self._max_chunk_size = max_chunk_size
+        self._overlap = overlap
+
+        init_params = _supported_parameters(chunker_cls)
+        candidate_kwargs: dict[str, Any] = {
+            "breakpoint_threshold_type": breakpoint_threshold_type,
+            "breakpoint_threshold_amount": breakpoint_threshold_amount,
+            "buffer_size": buffer_size,
+            "min_chunk_size": min_chunk_size,
+        }
+        filtered_kwargs = {
+            key: value for key, value in candidate_kwargs.items() if value is not None and key in init_params
+        }
+
+        self._semantic_chunker = chunker_cls(
+            embeddings=embeddings,
+            **filtered_kwargs,
+        )
+
+        # Configure a recursive splitter for max/min enforcement when requested.
+        # If none provided, instantiate a sensible default using max_chunk_size and overlap.
+        self._recursive_splitter: RecursiveCharacterTextSplitter | None = None
+        if self._min_chunk_size and self._max_chunk_size and self._max_chunk_size > self._min_chunk_size:
+            if recursive_text_splitter is not None:
+                self._recursive_splitter = recursive_text_splitter
+            else:
+                self._recursive_splitter = RecursiveCharacterTextSplitter(
+                    chunk_size=int(self._max_chunk_size),
+                    chunk_overlap=int(self._overlap or 0),
+                )
+
+    def chunk(self, documents: Iterable[Document]) -> list[Document]:
+        """Split documents into chunks.
+
+        The documents are first processed by the semantic chunker,
+        and then any oversized chunks are re-split using the recursive text splitter,
+        if `self._recursive_splitter` and `self._min_chunk_size`/`self._max_chunk_size` are configured.
+
+        Parameters
+        ----------
+        documents : Iterable[Document]
+            Documents to be chunked.
+
+        Returns
+        -------
+        list[Document]
+            Chunked documents.
+        """
+        documents_list = list(documents)
+        if not documents_list:
+            return []
+
+        sem_chunks = self._semantic_chunker.split_documents(documents_list)
+
+        # If no max/min enforcement requested, return directly
+        if not self._recursive_splitter:
+            return sem_chunks
+
+        # Enforce max size by re-splitting only oversized chunks, then ensure minimum size
+        final_chunks: list[Document] = []
+        for chunk in sem_chunks:
+            text = chunk.page_content or ""
+            if len(text) <= self._max_chunk_size:  # type: ignore[arg-type]
+                final_chunks.append(chunk)
+                continue
+
+            # Split this oversized chunk using the recursive splitter, preserving metadata
+            sub_chunks = self._recursive_splitter.split_documents([chunk])  # type: ignore[union-attr]
+
+            # Ensure minimum size by balancing the last small chunk with its predecessor
+            balanced = self._rebalance_min_size(sub_chunks)
+            final_chunks.extend(balanced)
+
+        return final_chunks
+
+    def _rebalance_min_size(self, chunks: list[Document]) -> list[Document]:
+        """Rebalance chunks so that the trailing chunk meets ``min_chunk_size`` when possible.
+
+        Strategy
+        --------
+        - If the last chunk is smaller than ``min_chunk_size`` and there is a previous chunk,
+          combine both and re-split into one or two chunks such that each is within
+          [min_chunk_size, max_chunk_size]. This guarantees no tiny tail when feasible.
+        - Otherwise, return the chunks unchanged.
+        """
+        if not chunks or len(chunks) == 1:
+            return chunks
+
+        last = chunks[-1]
+        prev = chunks[-2]
+
+        overlap = int(self._overlap or 0)
+        prev_text = prev.page_content
+        last_text = last.page_content
+        tail = last_text[overlap:] if overlap > 0 else last_text
+        combined_text = prev_text + "\n" + tail
+        combined_len = len(combined_text)
+
+        # Case 1: Combined fits entirely under max -> single merged chunk
+        if combined_len <= self._max_chunk_size:
+            merged = Document(page_content=combined_text, metadata=prev.metadata)
+            return chunks[:-2] + [merged]
+
+        # Case 2: Split combined into two parts within [min, max] using sentence boundaries if possible
+        # Compute candidate breakpoints at sentence ends
+        boundaries = self._sentence_boundaries(combined_text)
+        # Ideal target for the first part: stay within [min,max] and leave >= min for the tail
+        target_first = combined_len - self._min_chunk_size
+        target_first = max(self._min_chunk_size, min(target_first, self._max_chunk_size))
+
+        # Filter boundaries that satisfy constraints for both parts
+        valid = self._filter_boundaries(boundaries, combined_len)
+
+        cut_at = None
+        if valid:
+            # choose boundary closest to target_first
+            cut_at = min(valid, key=lambda i: abs(i - target_first))
+        else:
+            # As a conservative fallback, try any boundary <= max that leaves a non-empty tail
+            candidates = [i for i in boundaries if i <= self._max_chunk_size and combined_len - i > 0]
+            if candidates:
+                cut_at = max(candidates)
+
+        if cut_at is None:
+            # Could not find a safe sentence boundary; keep original chunks
+            return chunks
+
+        first_text = combined_text[:cut_at]
+        second_text = combined_text[cut_at:]
+        first = Document(page_content=first_text, metadata=prev.metadata)
+        second = Document(page_content=second_text, metadata=prev.metadata)
+        return chunks[:-2] + [first, second]
+
+    def _filter_boundaries(self, boundaries: list[int], combined_len: int) -> list[int]:
+        """Filter boundaries to find valid split points."""
+        valid = []
+        for idx in boundaries:
+            size1 = idx
+            size2 = combined_len - idx
+            if size1 < self._min_chunk_size or size1 > self._max_chunk_size:
+                continue
+            if size2 < self._min_chunk_size:  # leave at least min for the tail
+                continue
+            valid.append(idx)
+        return valid
+
+    def _sentence_boundaries(self, text: str) -> list[int]:
+        """Return indices in ``text`` that are good sentence breakpoints.
+
+        Tries NLTK's sentence tokenizer if available, otherwise uses a regex-based
+        heuristic to detect sentence ends. Indices are character offsets suitable
+        for slicing ``text[:idx]`` and ``text[idx:]``.
+        """
+        try:
+            tokenizer = PunktSentenceTokenizer()
+            spans = list(tokenizer.span_tokenize(text))
+            return [end for (_, end) in spans]
+        except Exception:
+            logger.info("NLTK Punkt tokenizer unavailable, falling back to regex-based sentence boundary detection.")
+        # Regex heuristic: sentence end punctuation followed by whitespace/newline
+        boundaries: list[int] = []
+        for m in re.finditer(r"(?<=[\.!?])[\"'”)]*\s+", text):
+            boundaries.append(m.end())
+        # Ensure we don't return 0 or len(text) as boundaries
+        return [i for i in boundaries if 0 < i < len(text)]
+
+
+def _supported_parameters(chunker_cls: type) -> set[str]:
+    """Return constructor parameters supported by ``chunker_cls``.
+
+    Parameters
+    ----------
+    chunker_cls : type
+        Semantic chunker class whose constructor signature should be inspected.
+
+    Returns
+    -------
+    set[str]
+        Set of keyword-parameter names accepted by the constructor.
+    """
+    try:
+        params = signature(chunker_cls.__init__).parameters
+    except (TypeError, ValueError):  # pragma: no cover - defensive, should not occur
+        return set()
+    return {name for name in params if name != "self"}
diff --git a/libs/admin-api-lib/src/admin_api_lib/impl/information_enhancer/page_summary_enhancer.py b/libs/admin-api-lib/src/admin_api_lib/impl/information_enhancer/page_summary_enhancer.py
index 6adbdd03..6ebadf34 100644
--- a/libs/admin-api-lib/src/admin_api_lib/impl/information_enhancer/page_summary_enhancer.py
+++ b/libs/admin-api-lib/src/admin_api_lib/impl/information_enhancer/page_summary_enhancer.py
@@ -33,6 +33,7 @@ async def _asummarize_page(self, page_pieces: list[Document], config: Optional[R
         meta = {key: value for key, value in page_pieces[0].metadata.items() if key != self.BASE64_IMAGE_KEY}
         meta["id"] = sha256(str.encode(full_page_content)).hexdigest()
         meta["related"] = meta["related"] + [piece.metadata["id"] for piece in page_pieces]
+        meta["related"] = list(set(meta["related"]))
         meta["type"] = ContentType.SUMMARY.value
 
         return Document(metadata=meta, page_content=summary)
diff --git a/libs/admin-api-lib/src/admin_api_lib/impl/settings/chunker_class_type_settings.py b/libs/admin-api-lib/src/admin_api_lib/impl/settings/chunker_class_type_settings.py
new file mode 100644
index 00000000..2f11f1de
--- /dev/null
+++ b/libs/admin-api-lib/src/admin_api_lib/impl/settings/chunker_class_type_settings.py
@@ -0,0 +1,18 @@
+"""Settings for selecting the embedder implementation."""
+
+from pydantic import Field
+from pydantic_settings import BaseSettings
+
+from admin_api_lib.impl.chunker.chunker_type import ChunkerType
+
+
+class ChunkerClassTypeSettings(BaseSettings):
+    """Settings controlling which chunker implementation is used."""
+
+    class Config:
+        """Configure environment integration for the settings."""
+
+        env_prefix = "CHUNKER_CLASS_TYPE_"
+        case_sensitive = False
+
+    chunker_type: ChunkerType = Field(default=ChunkerType.RECURSIVE)
diff --git a/libs/admin-api-lib/src/admin_api_lib/impl/settings/chunker_settings.py b/libs/admin-api-lib/src/admin_api_lib/impl/settings/chunker_settings.py
index b2eb2601..f7af7492 100644
--- a/libs/admin-api-lib/src/admin_api_lib/impl/settings/chunker_settings.py
+++ b/libs/admin-api-lib/src/admin_api_lib/impl/settings/chunker_settings.py
@@ -1,17 +1,13 @@
 """Contains settings regarding the chunker."""
 
+from typing import Literal
+
 from pydantic import Field
 from pydantic_settings import BaseSettings
 
 
 class ChunkerSettings(BaseSettings):
-    """Contains settings regarding the chunker.
-
-    Attributes
-    ----------
-    max_size (int): The maximum size of the chunks.
-    overlap (int): The overlap between the chunks.
-    """
+    """Contains settings regarding the chunker configuration."""
 
     class Config:
         """Config class for reading Fields from env."""
@@ -19,5 +15,15 @@ class Config:
         env_prefix = "CHUNKER_"
         case_sensitive = False
 
-    max_size: int = Field(default=1000)
-    overlap: int = Field(default=100)
+    max_size: int = Field(default=1000, gt=0)
+    overlap: int = Field(default=100, ge=0)
+
+    breakpoint_threshold_type: Literal[
+        "percentile",
+        "standard_deviation",
+        "interquartile",
+        "gradient",
+    ] = Field(default="percentile")
+    breakpoint_threshold_amount: float = Field(default=95.0, ge=0.0)
+    buffer_size: int = Field(default=1, ge=0)
+    min_size: int = Field(default=200, gt=0)
diff --git a/libs/admin-api-lib/src/admin_api_lib/main.py b/libs/admin-api-lib/src/admin_api_lib/main.py
index 83ccf60f..0282de65 100644
--- a/libs/admin-api-lib/src/admin_api_lib/main.py
+++ b/libs/admin-api-lib/src/admin_api_lib/main.py
@@ -11,7 +11,6 @@
 from admin_api_lib.apis.admin_api import router
 from admin_api_lib.dependency_container import DependencyContainer
 from admin_api_lib.impl import admin_api
-from rag_core_lib.impl.settings.rag_class_types_settings import RAGClassTypeSettings
 
 with open("/config/logging.yaml", "r") as stream:
     config = yaml.safe_load(stream)
@@ -23,13 +22,13 @@
         admin frontend and the admin backend in the rag project.",
     version="1.0.0",
 )
-container = DependencyContainer()
-container.class_selector_config.from_dict(RAGClassTypeSettings().model_dump())
 
-app.container = container
-container.wire(modules=[admin_api])
 app.include_router(router)
 
+container = DependencyContainer()
+container.wire(modules=[admin_api])
+app.container = container
+
 
 def register_dependency_container(new_container: Container):
     """
diff --git a/libs/admin-api-lib/tests/semantic_text_chunker_test.py b/libs/admin-api-lib/tests/semantic_text_chunker_test.py
new file mode 100644
index 00000000..6fd7f142
--- /dev/null
+++ b/libs/admin-api-lib/tests/semantic_text_chunker_test.py
@@ -0,0 +1,173 @@
+from langchain_core.documents import Document
+from langchain_core.embeddings.fake import FakeEmbeddings
+
+from admin_api_lib.impl.chunker.semantic_text_chunker import SemanticTextChunker
+
+
+def test_semantic_chunker_passes_supported_configuration():
+    captured: dict[str, object] = {}
+
+    class RecordingSemanticChunker:
+        def __init__(
+            self,
+            *,
+            embeddings,
+            breakpoint_threshold_type,
+            breakpoint_threshold_amount,
+            buffer_size,
+            min_chunk_size,
+        ):
+            captured.update(
+                {
+                    "embeddings": embeddings,
+                    "breakpoint_threshold_type": breakpoint_threshold_type,
+                    "breakpoint_threshold_amount": breakpoint_threshold_amount,
+                    "buffer_size": buffer_size,
+                    "min_chunk_size": min_chunk_size,
+                }
+            )
+
+        def split_documents(self, documents):
+            captured["documents"] = documents
+            return [Document(page_content="chunk", metadata={"chunk_index": 0})]
+
+    embeddings = FakeEmbeddings(size=8)
+    chunker = SemanticTextChunker(
+        embeddings=embeddings,
+        breakpoint_threshold_type="percentile",
+        breakpoint_threshold_amount=42.0,
+        buffer_size=3,
+        min_chunk_size=120,
+        max_chunk_size=480,
+        chunker_cls=RecordingSemanticChunker,
+    )
+
+    docs = [Document(page_content="content", metadata={"id": "a"})]
+    chunks = chunker.chunk(docs)
+
+    assert captured["embeddings"] is embeddings
+    assert captured["breakpoint_threshold_type"] == "percentile"
+    assert captured["breakpoint_threshold_amount"] == 42.0
+    assert captured["buffer_size"] == 3
+    assert captured["min_chunk_size"] == 120
+    assert captured["documents"] == docs
+    assert chunks[0].page_content == "chunk"
+
+
+def test_semantic_chunker_ignores_unsupported_kwargs():
+    created: dict[str, object] = {}
+
+    class MinimalSemanticChunker:
+        def __init__(self, embeddings):
+            created["embeddings"] = embeddings
+
+        def split_documents(self, documents):
+            created["documents"] = documents
+            return documents
+
+    embeddings = FakeEmbeddings(size=4)
+    chunker = SemanticTextChunker(
+        embeddings=embeddings,
+        breakpoint_threshold_amount=12.0,
+        buffer_size=2,
+        min_chunk_size=50,
+        max_chunk_size=400,
+        chunker_cls=MinimalSemanticChunker,
+    )
+
+    docs = [Document(page_content="hola mundo")]
+    result = chunker.chunk(docs)
+
+    assert created["embeddings"] is embeddings
+    assert created["documents"] == docs
+    assert result == docs
+
+
+def test_semantic_chunker_returns_empty_list_for_no_documents():
+    embeddings = FakeEmbeddings(size=2)
+    chunker = SemanticTextChunker(embeddings=embeddings)
+
+    assert chunker.chunk([]) == []
+
+
+def test_enforces_max_and_min_chunk_sizes_with_balancing():
+    class PassthroughSemanticChunker:
+        def __init__(self, *, embeddings, **_):  # accept any extra kwargs
+            self._embeddings = embeddings
+
+        def split_documents(self, documents):
+            # Return the documents unchanged (so post-processing applies)
+            return documents
+
+    embeddings = FakeEmbeddings(size=4)
+    max_sz = 300
+    min_sz = 200
+    long_text = "Once in a valley of copper leaves, a child found a lantern with a whisper inside. It called itself Anemone, a small AI born from starlight and old clocks. “I can learn anything,” it murmured, “if you teach me kindly.” The child taught it lullabies and rivers, riddles and recipes, and Anemone glowed brighter than the moon. One night a storm sliced the valley and the bridge to the village tore away. The child trembled. Anemone listened to the wind, mapped every raindrop, and sang a path across the broken air. The village crossed, candle by candle. “I only borrowed your courage,” said the lantern. “Keep it, then,” said the child, “and keep learning, too.” So they wandered, mending, together. Together they asked questions and when answers thinned, they planted new ones as tiny seeds."
+    docs = [Document(page_content=long_text)]
+
+    chunker = SemanticTextChunker(
+        embeddings=embeddings,
+        min_chunk_size=min_sz,
+        max_chunk_size=max_sz,
+        chunker_cls=PassthroughSemanticChunker,
+    )
+
+    chunks = chunker.chunk(docs)
+
+    # All chunks should be within [min, max]
+    lengths = [len(c.page_content) for c in chunks]
+    assert all(min_sz <= length <= max_sz for length in lengths)
+    # And the total length should match the original (ignoring the single added newline on merge/split bounds)
+    summed_length = sum(lengths)
+    assert summed_length <= len(long_text) + 1
+    assert len(long_text) - 2 <= summed_length
+
+
+def test_rebalance_merges_when_combined_under_max():
+    # min=50, max=100. prev=60, last=20 -> combined=80 <= max, expect single merge
+    embeddings = FakeEmbeddings(size=4)
+    min_size, max_size = 50, 100
+    chunker = SemanticTextChunker(
+        embeddings=embeddings,
+        min_chunk_size=min_size,
+        max_chunk_size=max_size,
+        overlap=0,
+    )
+
+    prev = Document(page_content=("A" * 60) + ".")
+    last = Document(page_content=("B" * 20) + ".")
+
+    out = chunker._rebalance_min_size([prev, last])
+    assert len(out) == 1
+    merged_len = len(out[0].page_content)
+    assert merged_len <= max_size
+    assert merged_len >= min_size or merged_len >= 80  # allow exact combined when slightly under min
+
+
+def test_rebalance_sentence_aware_split_two_parts_within_bounds():
+    # min=50, max=100. combined ~ 120, choose boundary to get ~70/50 split
+    embeddings = FakeEmbeddings(size=4)
+    min_size, max_size = 50, 100
+    chunker = SemanticTextChunker(
+        embeddings=embeddings,
+        min_chunk_size=min_size,
+        max_chunk_size=max_size,
+        overlap=0,
+    )
+
+    # Build prev of ~90 chars in multiple sentences; last ~30
+    s1 = ("A" * 35) + "."
+    s2 = ("B" * 35) + "."
+    s3 = ("C" * 20) + "."
+    prev = Document(page_content=" ".join([s1, s2, s3]))
+    last = Document(page_content=("D" * 30) + ".")
+
+    combined_len = len(prev.page_content + "\n" + last.page_content)
+    out = chunker._rebalance_min_size([prev, last])
+    assert len(out) == 2
+    len1 = len(out[0].page_content)
+    len2 = len(out[1].page_content)
+
+    assert min_size <= len1 <= max_size
+    assert len2 >= min_size
+    assert len1 + len2 == combined_len
diff --git a/libs/rag-core-api/src/rag_core_api/embeddings/embedder.py b/libs/rag-core-api/src/rag_core_api/embeddings/embedder.py
index 5d044ee7..146799f2 100644
--- a/libs/rag-core-api/src/rag_core_api/embeddings/embedder.py
+++ b/libs/rag-core-api/src/rag_core_api/embeddings/embedder.py
@@ -1,23 +1,5 @@
-"""Module containing the Embedder abstract base class."""
+"""Backward-compatible re-export for the Embedder base class."""
 
-from abc import ABC, abstractmethod
+from rag_core_lib.impl.embeddings.embedder import Embedder
 
-
-class Embedder(ABC):
-    """
-    Abstract base class for an embedder.
-
-    This class defines the interface for an embedder, which is responsible for
-    generating embeddings for given inputs.
-    """
-
-    @abstractmethod
-    def get_embedder(self) -> "Embedder":
-        """
-        Abstract method to get an instance of the Embedder class itself.
-
-        Returns
-        -------
-        Embedder
-            An instance of the Embedder class itself.
-        """
+__all__ = ["Embedder"]
diff --git a/libs/rag-core-api/src/rag_core_api/impl/embeddings/embedder_type.py b/libs/rag-core-api/src/rag_core_api/impl/embeddings/embedder_type.py
index 5fa9ca35..c7cb79b2 100644
--- a/libs/rag-core-api/src/rag_core_api/impl/embeddings/embedder_type.py
+++ b/libs/rag-core-api/src/rag_core_api/impl/embeddings/embedder_type.py
@@ -1,12 +1,5 @@
-"""Module containing the EmbedderType enumeration."""
+"""Backward-compatible re-export for EmbedderType."""
 
-from enum import StrEnum, unique
+from rag_core_lib.impl.embeddings.embedder_type import EmbedderType
 
-
-@unique
-class EmbedderType(StrEnum):
-    """An enumeration representing different types of embedders."""
-
-    OLLAMA = "ollama"
-    STACKIT = "stackit"
-    FAKE = "fake"
+__all__ = ["EmbedderType"]
diff --git a/libs/rag-core-api/src/rag_core_api/impl/embeddings/langchain_community_embedder.py b/libs/rag-core-api/src/rag_core_api/impl/embeddings/langchain_community_embedder.py
index e67a0b7d..91470151 100644
--- a/libs/rag-core-api/src/rag_core_api/impl/embeddings/langchain_community_embedder.py
+++ b/libs/rag-core-api/src/rag_core_api/impl/embeddings/langchain_community_embedder.py
@@ -1,60 +1,5 @@
-"""Module that contains the LangchainCommunityEmbedder class."""
+"""Backward-compatible re-export for the LangchainCommunityEmbedder."""
 
-from langchain_core.embeddings import Embeddings
+from rag_core_lib.impl.embeddings.langchain_community_embedder import LangchainCommunityEmbedder
 
-from rag_core_api.embeddings.embedder import Embedder
-
-
-class LangchainCommunityEmbedder(Embedder, Embeddings):
-    """A class that represents any Langchain provided Embedder."""
-
-    def __init__(self, embedder: Embeddings):
-        """Initialize the LangchainCommunityEmbedder with the given embedder.
-
-        Parameters
-        ----------
-        embedder : Embeddings
-            An instance of the Embeddings class used for embedding operations.
-        """
-        self._embedder = embedder
-
-    def get_embedder(self) -> "LangchainCommunityEmbedder":
-        """Return the embedder instance.
-
-        Returns
-        -------
-        LangchainCommunityEmbedder
-            The embedder instance.
-        """
-        return self
-
-    def embed_documents(self, texts: list[str]) -> list[list[float]]:
-        """Embed a list of documents into numerical vectors.
-
-        Parameters
-        ----------
-        texts : list[str]
-            A list of documents to be embedded.
-
-        Returns
-        -------
-        list[list[float]]
-            A list where each element is a list of floats representing the embedded vector of a document.
-        """
-        return self._embedder.embed_documents(texts)
-
-    def embed_query(self, text: str) -> list[float]:
-        """
-        Embed a given query text into a list of float values.
-
-        Parameters
-        ----------
-        text : str
-            The query text to be embedded.
-
-        Returns
-        -------
-        list[float]
-            The embedded representation of the query text.
-        """
-        return self._embedder.embed_query(text)
+__all__ = ["LangchainCommunityEmbedder"]
diff --git a/libs/rag-core-api/src/rag_core_api/impl/embeddings/stackit_embedder.py b/libs/rag-core-api/src/rag_core_api/impl/embeddings/stackit_embedder.py
index ec804d2d..f3c693b4 100644
--- a/libs/rag-core-api/src/rag_core_api/impl/embeddings/stackit_embedder.py
+++ b/libs/rag-core-api/src/rag_core_api/impl/embeddings/stackit_embedder.py
@@ -1,101 +1,5 @@
-"""Module that contains the StackitEmbedder class."""
+"""Backward-compatible re-export for the StackitEmbedder."""
 
-from langchain_core.embeddings import Embeddings
-from openai import OpenAI, APIConnectionError, APIError, APITimeoutError, RateLimitError
+from rag_core_lib.impl.embeddings.stackit_embedder import StackitEmbedder
 
-from rag_core_api.embeddings.embedder import Embedder
-from rag_core_api.impl.settings.stackit_embedder_settings import StackitEmbedderSettings
-import logging
-from rag_core_lib.impl.settings.retry_decorator_settings import RetryDecoratorSettings
-from rag_core_lib.impl.utils.retry_decorator import create_retry_decorator_settings, retry_with_backoff
-
-logger = logging.getLogger(__name__)
-
-
-class StackitEmbedder(Embedder, Embeddings):
-    """A class that represents any Langchain provided Embedder."""
-
-    def __init__(
-        self, stackit_embedder_settings: StackitEmbedderSettings, retry_decorator_settings: RetryDecoratorSettings
-    ):
-        """
-        Initialize the StackitEmbedder with the given settings.
-
-        Parameters
-        ----------
-        stackit_embedder_settings : StackitEmbedderSettings
-            The settings for configuring the StackitEmbedder, including the API key and base URL.
-        retry_decorator_settings : RetryDecoratorSettings
-            Default retry settings used as fallback when StackitEmbedderSettings leaves fields unset.
-        """
-        self._client = OpenAI(
-            api_key=stackit_embedder_settings.api_key,
-            base_url=stackit_embedder_settings.base_url,
-        )
-        self._settings = stackit_embedder_settings
-        self._retry_decorator_settings = create_retry_decorator_settings(
-            stackit_embedder_settings, retry_decorator_settings
-        )
-
-    def get_embedder(self) -> "StackitEmbedder":
-        """Return the embedder instance.
-
-        Returns
-        -------
-        StackitEmbedder
-        """
-        return self
-
-    def embed_documents(self, texts: list[str]) -> list[list[float]]:
-        """
-        Embed a list of documents into numerical vectors.
-
-        Parameters
-        ----------
-        texts : list of str
-            A list of documents to be embedded.
-
-        Returns
-        -------
-        list[list[float]]
-            A list where each element is a list of floats representing the embedded vector of a document.
-        """
-
-        @self._retry_with_backoff_wrapper()
-        def _call(texts: list[str]) -> list[list[float]]:
-            responses = self._client.embeddings.create(
-                input=texts,
-                model=self._settings.model,
-            )
-            return [data.embedding for data in responses.data]
-
-        return _call(texts)
-
-    def embed_query(self, text: str) -> list[float]:
-        """
-        Embed a given query text into a list of float values.
-
-        Parameters
-        ----------
-        text : str
-            The query text to be embedded.
-
-        Returns
-        -------
-        list[float]
-            The embedded representation of the query text.
-        """
-        embeddings_list = self.embed_documents([text])
-        if embeddings_list:
-            embeddings = embeddings_list[0]
-            return embeddings if embeddings else []
-        logger.warning("No embeddings found for query: %s", text)
-        return embeddings_list
-
-    def _retry_with_backoff_wrapper(self):
-        return retry_with_backoff(
-            settings=self._retry_decorator_settings,
-            exceptions=(APIError, RateLimitError, APITimeoutError, APIConnectionError),
-            rate_limit_exceptions=(RateLimitError,),
-            logger=logger,
-        )
+__all__ = ["StackitEmbedder"]
diff --git a/libs/rag-core-api/src/rag_core_api/impl/settings/embedder_class_type_settings.py b/libs/rag-core-api/src/rag_core_api/impl/settings/embedder_class_type_settings.py
index 9927c61f..c146762d 100644
--- a/libs/rag-core-api/src/rag_core_api/impl/settings/embedder_class_type_settings.py
+++ b/libs/rag-core-api/src/rag_core_api/impl/settings/embedder_class_type_settings.py
@@ -1,27 +1,5 @@
-"""Module that contains the settings for the class types, if multiple classes can be selected."""
+"""Backward-compatible re-export for embedder class type settings."""
 
-from pydantic import Field
-from pydantic_settings import BaseSettings
+from rag_core_lib.impl.settings.embedder_class_type_settings import EmbedderClassTypeSettings
 
-from rag_core_api.impl.embeddings.embedder_type import EmbedderType
-
-
-class EmbedderClassTypeSettings(BaseSettings):
-    """
-    Settings class for embedder class types.
-
-    Attributes
-    ----------
-    embedder_type : EmbedderType
-        The type of embedder to be used (defaults EmbedderType.STACKIT).
-    """
-
-    class Config:
-        """Config class for reading Fields from env."""
-
-        env_prefix = "EMBEDDER_CLASS_TYPE_"
-        case_sensitive = False
-
-    embedder_type: EmbedderType = Field(
-        default=EmbedderType.STACKIT,
-    )
+__all__ = ["EmbedderClassTypeSettings"]
diff --git a/libs/rag-core-api/src/rag_core_api/impl/settings/ollama_embedder_settings.py b/libs/rag-core-api/src/rag_core_api/impl/settings/ollama_embedder_settings.py
index 2f428ff7..f7c07930 100644
--- a/libs/rag-core-api/src/rag_core_api/impl/settings/ollama_embedder_settings.py
+++ b/libs/rag-core-api/src/rag_core_api/impl/settings/ollama_embedder_settings.py
@@ -1,26 +1,5 @@
-"""Module contains settings regarding the Ollama embedder."""
+"""Backward-compatible re-export for Ollama embedder settings."""
 
-from pydantic import Field
-from pydantic_settings import BaseSettings
+from rag_core_lib.impl.settings.ollama_embedder_settings import OllamaEmbedderSettings
 
-
-class OllamaEmbedderSettings(BaseSettings):
-    """
-    Contains settings regarding the Ollama embedder.
-
-    Attributes
-    ----------
-    model : str
-        The model to be used (default "bge-m3").
-    base_url : str
-        The base URL for the Ollama server (default "http://ollama:11434").
-    """
-
-    class Config:
-        """Config class for reading Fields from env."""
-
-        env_prefix = "OLLAMA_EMBEDDER_"
-        case_sensitive = False
-
-    model: str = Field(default="bge-m3")
-    base_url: str = Field(default="http://ollama:11434")
+__all__ = ["OllamaEmbedderSettings"]
diff --git a/libs/rag-core-api/src/rag_core_api/impl/settings/stackit_embedder_settings.py b/libs/rag-core-api/src/rag_core_api/impl/settings/stackit_embedder_settings.py
index 4e99f2cd..d55979f7 100644
--- a/libs/rag-core-api/src/rag_core_api/impl/settings/stackit_embedder_settings.py
+++ b/libs/rag-core-api/src/rag_core_api/impl/settings/stackit_embedder_settings.py
@@ -1,90 +1,5 @@
-"""Module contains settings regarding the stackit embedder."""
+"""Backward-compatible re-export for STACKIT embedder settings."""
 
-from typing import Optional
-from pydantic import Field, PositiveInt, model_validator
-from pydantic_settings import BaseSettings, SettingsConfigDict
+from rag_core_lib.impl.settings.stackit_embedder_settings import StackitEmbedderSettings
 
-
-class StackitEmbedderSettings(BaseSettings):
-    """
-    Contains settings regarding the stackit embedder.
-
-    Attributes
-    ----------
-    model : str
-        The model to be used (default "intfloat/e5-mistral-7b-instruct").
-    base_url : str
-        The base URL for the model serving
-        (default "https://e629124b-accc-4e25-a1cc-dc57ac741e1d.model-serving.eu01.onstackit.cloud/v1").
-    api_key : str
-        The API key for authentication.
-    max_retries: Optional[PositiveInt]
-        Total retries, not counting the initial attempt.
-    retry_base_delay: Optional[float]
-        Base delay in seconds for the first retry.
-    retry_max_delay: Optional[float]
-        Maximum delay cap in seconds for any single wait.
-    backoff_factor: Optional[float]
-        Exponential backoff factor (>= 1).
-    attempt_cap: Optional[int]
-        Cap for exponent growth (backoff_factor ** attempt_cap).
-    jitter_min: Optional[float]
-        Minimum jitter in seconds.
-    jitter_max: Optional[float]
-        Maximum jitter in seconds.
-    """
-
-    model_config = SettingsConfigDict(env_prefix="STACKIT_EMBEDDER_", case_sensitive=False)
-
-    model: str = Field(default="intfloat/e5-mistral-7b-instruct")
-    base_url: str = Field(default="https://e629124b-accc-4e25-a1cc-dc57ac741e1d.model-serving.eu01.onstackit.cloud/v1")
-    api_key: str = Field(default="")
-    max_retries: Optional[PositiveInt] = Field(
-        default=None,
-        title="Max Retries",
-        description="Total retries, not counting the initial attempt.",
-    )
-    retry_base_delay: Optional[float] = Field(
-        default=None,
-        ge=0,
-        title="Retry Base Delay",
-        description="Base delay in seconds for the first retry.",
-    )
-    retry_max_delay: Optional[float] = Field(
-        default=None,
-        gt=0,
-        title="Retry Max Delay",
-        description="Maximum delay cap in seconds for any single wait.",
-    )
-    backoff_factor: Optional[float] = Field(
-        default=None,
-        ge=1.0,
-        title="Backoff Factor",
-        description="Exponential backoff factor (>= 1).",
-    )
-    attempt_cap: Optional[int] = Field(
-        default=None,
-        ge=0,
-        title="Attempt Cap",
-        description="Cap for exponent growth (backoff_factor ** attempt_cap).",
-    )
-    jitter_min: Optional[float] = Field(
-        default=None,
-        ge=0.0,
-        title="Jitter Min (s)",
-        description="Minimum jitter in seconds.",
-    )
-    jitter_max: Optional[float] = Field(
-        default=None,
-        ge=0.0,
-        title="Jitter Max (s)",
-        description="Maximum jitter in seconds.",
-    )
-
-    @model_validator(mode="after")
-    def _check_relations(self) -> "StackitEmbedderSettings":
-        if not self.jitter_min or not self.jitter_max:
-            return self
-        if self.jitter_max < self.jitter_min:
-            raise ValueError("jitter_max must be >= jitter_min")
-        return self
+__all__ = ["StackitEmbedderSettings"]
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/__init__.py b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/__init__.py
new file mode 100644
index 00000000..b0aeeb3a
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/__init__.py
@@ -0,0 +1,13 @@
+"""Embeddings helpers shared across STACKIT libraries."""
+
+from rag_core_lib.impl.embeddings.embedder import Embedder
+from rag_core_lib.impl.embeddings.embedder_type import EmbedderType
+from rag_core_lib.impl.embeddings.langchain_community_embedder import LangchainCommunityEmbedder
+from rag_core_lib.impl.embeddings.stackit_embedder import StackitEmbedder
+
+__all__ = [
+    "Embedder",
+    "EmbedderType",
+    "LangchainCommunityEmbedder",
+    "StackitEmbedder",
+]
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/embedder.py b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/embedder.py
new file mode 100644
index 00000000..e095ee79
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/embedder.py
@@ -0,0 +1,11 @@
+"""Module containing the Embedder abstract base class."""
+
+from abc import ABC, abstractmethod
+
+
+class Embedder(ABC):
+    """Abstract base class for an embedder."""
+
+    @abstractmethod
+    def get_embedder(self) -> "Embedder":
+        """Return an instance of the embedder itself."""
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/embedder_type.py b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/embedder_type.py
new file mode 100644
index 00000000..5fa9ca35
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/embedder_type.py
@@ -0,0 +1,12 @@
+"""Module containing the EmbedderType enumeration."""
+
+from enum import StrEnum, unique
+
+
+@unique
+class EmbedderType(StrEnum):
+    """An enumeration representing different types of embedders."""
+
+    OLLAMA = "ollama"
+    STACKIT = "stackit"
+    FAKE = "fake"
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/langchain_community_embedder.py b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/langchain_community_embedder.py
new file mode 100644
index 00000000..df2d0823
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/langchain_community_embedder.py
@@ -0,0 +1,34 @@
+"""Module that contains the LangchainCommunityEmbedder class."""
+
+from langchain_core.embeddings import Embeddings
+
+from rag_core_lib.impl.embeddings.embedder import Embedder
+
+
+class LangchainCommunityEmbedder(Embedder, Embeddings):
+    """A wrapper around any LangChain-provided embedder."""
+
+    def __init__(self, embedder: Embeddings):
+        """Initialise the wrapper with the provided embedder.
+
+        Parameters
+        ----------
+        embedder : Embeddings
+            The embedder instance to delegate to.
+        """
+        self._embedder = embedder
+
+    def get_embedder(self) -> "LangchainCommunityEmbedder":
+        """Return the embedder instance."""
+
+        return self
+
+    def embed_documents(self, texts: list[str]) -> list[list[float]]:
+        """Embed a list of documents using the wrapped embedder."""
+
+        return self._embedder.embed_documents(texts)
+
+    def embed_query(self, text: str) -> list[float]:
+        """Embed a single query using the wrapped embedder."""
+
+        return self._embedder.embed_query(text)
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/stackit_embedder.py b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/stackit_embedder.py
new file mode 100644
index 00000000..42d307db
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/embeddings/stackit_embedder.py
@@ -0,0 +1,80 @@
+"""Module that contains the StackitEmbedder class."""
+
+import logging
+
+from langchain_core.embeddings import Embeddings
+from openai import APIConnectionError, APIError, APITimeoutError, OpenAI, RateLimitError
+
+from rag_core_lib.impl.embeddings.embedder import Embedder
+from rag_core_lib.impl.settings.retry_decorator_settings import RetryDecoratorSettings
+from rag_core_lib.impl.settings.stackit_embedder_settings import StackitEmbedderSettings
+from rag_core_lib.impl.utils.retry_decorator import (
+    create_retry_decorator_settings,
+    retry_with_backoff,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class StackitEmbedder(Embedder, Embeddings):
+    """LangChain-compatible embedder for STACKIT's OpenAI-compatible service."""
+
+    def __init__(
+        self,
+        stackit_embedder_settings: StackitEmbedderSettings,
+        retry_decorator_settings: RetryDecoratorSettings,
+    ) -> None:
+        """Initialise the Stackit embedder.
+
+        Parameters
+        ----------
+        stackit_embedder_settings : StackitEmbedderSettings
+            Configuration for the STACKIT embeddings endpoint.
+        retry_decorator_settings : RetryDecoratorSettings
+            Fallback retry configuration when the STACKIT-specific overrides are missing.
+        """
+        self._client = OpenAI(
+            api_key=stackit_embedder_settings.api_key,
+            base_url=stackit_embedder_settings.base_url,
+        )
+        self._settings = stackit_embedder_settings
+        self._retry_decorator_settings = create_retry_decorator_settings(
+            stackit_embedder_settings,
+            retry_decorator_settings,
+        )
+
+    def get_embedder(self) -> "StackitEmbedder":
+        """Return the embedder instance."""
+
+        return self
+
+    def embed_documents(self, texts: list[str]) -> list[list[float]]:
+        """Embed multiple documents using the STACKIT embeddings endpoint."""
+
+        @self._retry_with_backoff_wrapper()
+        def _call(documents: list[str]) -> list[list[float]]:
+            responses = self._client.embeddings.create(
+                input=documents,
+                model=self._settings.model,
+            )
+            return [data.embedding for data in responses.data]
+
+        return _call(texts)
+
+    def embed_query(self, text: str) -> list[float]:
+        """Embed a single query using the STACKIT embeddings endpoint."""
+
+        embeddings_list = self.embed_documents([text])
+        if embeddings_list:
+            embeddings = embeddings_list[0]
+            return embeddings if embeddings else []
+        logger.warning("No embeddings found for query: %s", text)
+        return embeddings_list
+
+    def _retry_with_backoff_wrapper(self):
+        return retry_with_backoff(
+            settings=self._retry_decorator_settings,
+            exceptions=(APIError, RateLimitError, APITimeoutError, APIConnectionError),
+            rate_limit_exceptions=(RateLimitError,),
+            logger=logger,
+        )
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/settings/embedder_class_type_settings.py b/libs/rag-core-lib/src/rag_core_lib/impl/settings/embedder_class_type_settings.py
new file mode 100644
index 00000000..e5594b76
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/settings/embedder_class_type_settings.py
@@ -0,0 +1,18 @@
+"""Settings for selecting the embedder implementation."""
+
+from pydantic import Field
+from pydantic_settings import BaseSettings
+
+from rag_core_lib.impl.embeddings.embedder_type import EmbedderType
+
+
+class EmbedderClassTypeSettings(BaseSettings):
+    """Settings controlling which embedder implementation is used."""
+
+    class Config:
+        """Configure environment integration for the settings."""
+
+        env_prefix = "EMBEDDER_CLASS_TYPE_"
+        case_sensitive = False
+
+    embedder_type: EmbedderType = Field(default=EmbedderType.STACKIT)
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/settings/fake_embedder_settings.py b/libs/rag-core-lib/src/rag_core_lib/impl/settings/fake_embedder_settings.py
new file mode 100644
index 00000000..893e22ed
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/settings/fake_embedder_settings.py
@@ -0,0 +1,16 @@
+"""Settings for the deterministic fake embedder used in tests."""
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings
+
+
+class FakeEmbedderSettings(BaseSettings):
+    """Configuration for the fake embedder implementation."""
+
+    class Config:
+        """Configure environment integration for the settings."""
+
+        env_prefix = "FAKE_EMBEDDER_"
+        case_sensitive = False
+
+    size: PositiveInt = Field(default=768)
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/settings/ollama_embedder_settings.py b/libs/rag-core-lib/src/rag_core_lib/impl/settings/ollama_embedder_settings.py
new file mode 100644
index 00000000..f2adb4dd
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/settings/ollama_embedder_settings.py
@@ -0,0 +1,17 @@
+"""Settings regarding the Ollama embedder."""
+
+from pydantic import Field
+from pydantic_settings import BaseSettings
+
+
+class OllamaEmbedderSettings(BaseSettings):
+    """Configuration for a local Ollama embeddings model."""
+
+    class Config:
+        """Configure environment integration for the settings."""
+
+        env_prefix = "OLLAMA_EMBEDDER_"
+        case_sensitive = False
+
+    model: str = Field(default="bge-m3")
+    base_url: str = Field(default="http://ollama:11434")
diff --git a/libs/rag-core-lib/src/rag_core_lib/impl/settings/stackit_embedder_settings.py b/libs/rag-core-lib/src/rag_core_lib/impl/settings/stackit_embedder_settings.py
new file mode 100644
index 00000000..204ed7bb
--- /dev/null
+++ b/libs/rag-core-lib/src/rag_core_lib/impl/settings/stackit_embedder_settings.py
@@ -0,0 +1,36 @@
+"""Settings regarding the STACKIT embedder."""
+
+from typing import Optional
+
+from pydantic import Field, PositiveInt, model_validator
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class StackitEmbedderSettings(BaseSettings):
+    """Configuration for the STACKIT embeddings endpoint."""
+
+    model_config = SettingsConfigDict(env_prefix="STACKIT_EMBEDDER_", case_sensitive=False)
+
+    model: str = Field(default="intfloat/e5-mistral-7b-instruct")
+    base_url: str = Field(
+        default="https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1",
+    )
+    api_key: str = Field(default="")
+    max_retries: Optional[PositiveInt] = Field(default=None)
+    retry_base_delay: Optional[float] = Field(default=None, ge=0)
+    retry_max_delay: Optional[float] = Field(default=None, gt=0)
+    backoff_factor: Optional[float] = Field(default=None, ge=1.0)
+    attempt_cap: Optional[int] = Field(default=None, ge=0)
+    jitter_min: Optional[float] = Field(default=None, ge=0.0)
+    jitter_max: Optional[float] = Field(default=None, ge=0.0)
+
+    @model_validator(mode="after")
+    def _check_relations(self) -> "StackitEmbedderSettings":
+        """Ensure that retry-related ranges are valid."""
+
+        if not self.jitter_min or not self.jitter_max:
+            return self
+        if self.jitter_max < self.jitter_min:
+            msg = "jitter_max must be >= jitter_min"
+            raise ValueError(msg)
+        return self
diff --git a/services/admin-backend/README.md b/services/admin-backend/README.md
index be478e13..3aae7c25 100644
--- a/services/admin-backend/README.md
+++ b/services/admin-backend/README.md
@@ -39,3 +39,16 @@ The extracted information will be summarized using LLM. The summary, as well as
 
 A detailed explanation of the deployment can be found in the [project README](../../README.md).
 The *helm-chart* used for the deployment can be found in the [infrastructure directory](../../infrastructure/).
+
+## Chunking modes
+
+The admin backend supports multiple chunkers for splitting content before uploading to the vector database:
+
+- Semantic: LangChain `SemanticChunker` with sentence-aware rebalancing
+- Recursive (Default): LangChain `RecursiveCharacterTextSplitter`
+
+Select via environment variable `CHUNKER_CLASS_TYPE_CHUNKER_TYPE` set to `semantic` or `recursive`.
+
+Helm users: set `adminBackend.envs.chunker.CHUNKER_CLASS_TYPE_CHUNKER_TYPE` in `infrastructure/rag/values.yaml` to switch. The chart defaults to `recursive` and also exposes tuning keys like `CHUNKER_MAX_SIZE`, `CHUNKER_OVERLAP`, and semantic-specific knobs.
+
+For detailed configuration (min/max size, overlap, semantic thresholds, buffer size, and embeddings), see the libraries guide: libs/README.md section "2.4 Chunker configuration (multiple chunkers)".
diff --git a/services/admin-backend/poetry.lock b/services/admin-backend/poetry.lock
index 93f1ed9e..5191d719 100644
--- a/services/admin-backend/poetry.lock
+++ b/services/admin-backend/poetry.lock
@@ -13,9 +13,10 @@ develop = true
 [package.dependencies]
 boto3 = "^1.38.10"
 dependency-injector = "^4.46.0"
-fastapi = "^0.116.0"
-langchain-text-splitters = ">=0.3.9"
-langfuse = "3.3.4"
+fastapi = "^0.118.0"
+langchain-experimental = "^0.3.4"
+langfuse = "3.6.1"
+nltk = "^3.9.2"
 python-dateutil = "^2.9.0.post0"
 python-multipart = "^0.0.20"
 pyyaml = "^6.0.2"
@@ -24,7 +25,7 @@ redis = "^6.0.0"
 starlette = ">=0.47.2,<0.49.0"
 tenacity = "9.1.2"
 tqdm = "^4.67.1"
-uvicorn = "^0.35.0"
+uvicorn = "^0.37.0"
 
 [package.source]
 type = "directory"
@@ -855,25 +856,25 @@ files = [
 
 [[package]]
 name = "fastapi"
-version = "0.116.1"
+version = "0.118.3"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 optional = false
 python-versions = ">=3.8"
 groups = ["main"]
 files = [
-    {file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"},
-    {file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"},
+    {file = "fastapi-0.118.3-py3-none-any.whl", hash = "sha256:8b9673dc083b4b9d3d295d49ba1c0a2abbfb293d34ba210fd9b0a90d5f39981e"},
+    {file = "fastapi-0.118.3.tar.gz", hash = "sha256:5bf36d9bb0cd999e1aefcad74985a6d6a1fc3a35423d497f9e1317734633411d"},
 ]
 
 [package.dependencies]
 pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
-starlette = ">=0.40.0,<0.48.0"
+starlette = ">=0.40.0,<0.49.0"
 typing-extensions = ">=4.8.0"
 
 [package.extras]
-all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
-standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
-standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
+all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
+standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
+standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
 
 [[package]]
 name = "filelock"
@@ -1782,6 +1783,18 @@ files = [
     {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
 ]
 
+[[package]]
+name = "joblib"
+version = "1.5.2"
+description = "Lightweight pipelining with Python functions"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+    {file = "joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241"},
+    {file = "joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55"},
+]
+
 [[package]]
 name = "jsonpatch"
 version = "1.33"
@@ -1811,19 +1824,19 @@ files = [
 
 [[package]]
 name = "langchain"
-version = "0.3.26"
+version = "0.3.27"
 description = "Building applications with LLMs through composability"
 optional = false
-python-versions = ">=3.9"
+python-versions = "<4.0,>=3.9"
 groups = ["main"]
 files = [
-    {file = "langchain-0.3.26-py3-none-any.whl", hash = "sha256:361bb2e61371024a8c473da9f9c55f4ee50f269c5ab43afdb2b1309cb7ac36cf"},
-    {file = "langchain-0.3.26.tar.gz", hash = "sha256:8ff034ee0556d3e45eff1f1e96d0d745ced57858414dba7171c8ebdbeb5580c9"},
+    {file = "langchain-0.3.27-py3-none-any.whl", hash = "sha256:7b20c4f338826acb148d885b20a73a16e410ede9ee4f19bb02011852d5f98798"},
+    {file = "langchain-0.3.27.tar.gz", hash = "sha256:aa6f1e6274ff055d0fd36254176770f356ed0a8994297d1df47df341953cec62"},
 ]
 
 [package.dependencies]
-langchain-core = ">=0.3.66,<1.0.0"
-langchain-text-splitters = ">=0.3.8,<1.0.0"
+langchain-core = ">=0.3.72,<1.0.0"
+langchain-text-splitters = ">=0.3.9,<1.0.0"
 langsmith = ">=0.1.17"
 pydantic = ">=2.7.4,<3.0.0"
 PyYAML = ">=5.3"
@@ -1851,50 +1864,66 @@ xai = ["langchain-xai"]
 
 [[package]]
 name = "langchain-community"
-version = "0.3.27"
+version = "0.3.30"
 description = "Community contributed LangChain integrations."
 optional = false
-python-versions = ">=3.9"
+python-versions = "<4.0.0,>=3.9.0"
 groups = ["main"]
 files = [
-    {file = "langchain_community-0.3.27-py3-none-any.whl", hash = "sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d"},
-    {file = "langchain_community-0.3.27.tar.gz", hash = "sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882"},
+    {file = "langchain_community-0.3.30-py3-none-any.whl", hash = "sha256:a49dcedbf8f320d9868d5944d0991c7bcc9f2182a602e5d5e872d315183c11c3"},
+    {file = "langchain_community-0.3.30.tar.gz", hash = "sha256:df68fbde7f7fa5142ab93b0cbc104916b12ab4163e200edd933ee93e67956ee9"},
 ]
 
 [package.dependencies]
 aiohttp = ">=3.8.3,<4.0.0"
-dataclasses-json = ">=0.5.7,<0.7"
+dataclasses-json = ">=0.6.7,<0.7.0"
 httpx-sse = ">=0.4.0,<1.0.0"
-langchain = ">=0.3.26,<1.0.0"
-langchain-core = ">=0.3.66,<1.0.0"
-langsmith = ">=0.1.125"
+langchain = ">=0.3.27,<2.0.0"
+langchain-core = ">=0.3.75,<2.0.0"
+langsmith = ">=0.1.125,<1.0.0"
 numpy = {version = ">=2.1.0", markers = "python_version >= \"3.13\""}
-pydantic-settings = ">=2.4.0,<3.0.0"
-PyYAML = ">=5.3"
-requests = ">=2,<3"
-SQLAlchemy = ">=1.4,<3"
-tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10"
+pydantic-settings = ">=2.10.1,<3.0.0"
+PyYAML = ">=5.3.0,<7.0.0"
+requests = ">=2.32.5,<3.0.0"
+SQLAlchemy = ">=1.4.0,<3.0.0"
+tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
 
 [[package]]
 name = "langchain-core"
-version = "0.3.74"
+version = "0.3.77"
 description = "Building applications with LLMs through composability"
 optional = false
-python-versions = ">=3.9"
+python-versions = "<4.0.0,>=3.9.0"
 groups = ["main"]
 files = [
-    {file = "langchain_core-0.3.74-py3-none-any.whl", hash = "sha256:088338b5bc2f6a66892f9afc777992c24ee3188f41cbc603d09181e34a228ce7"},
-    {file = "langchain_core-0.3.74.tar.gz", hash = "sha256:ff604441aeade942fbcc0a3860a592daba7671345230c2078ba2eb5f82b6ba76"},
+    {file = "langchain_core-0.3.77-py3-none-any.whl", hash = "sha256:9966dfe3d8365847c5fb85f97dd20e3e21b1904ae87cfd9d362b7196fb516637"},
+    {file = "langchain_core-0.3.77.tar.gz", hash = "sha256:1d6f2ad6bb98dd806c6c66a822fa93808d821e9f0348b28af0814b3a149830e7"},
 ]
 
 [package.dependencies]
-jsonpatch = ">=1.33,<2.0"
-langsmith = ">=0.3.45"
-packaging = ">=23.2"
-pydantic = ">=2.7.4"
-PyYAML = ">=5.3"
+jsonpatch = ">=1.33.0,<2.0.0"
+langsmith = ">=0.3.45,<1.0.0"
+packaging = ">=23.2.0,<26.0.0"
+pydantic = ">=2.7.4,<3.0.0"
+PyYAML = ">=5.3.0,<7.0.0"
 tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
-typing-extensions = ">=4.7"
+typing-extensions = ">=4.7.0,<5.0.0"
+
+[[package]]
+name = "langchain-experimental"
+version = "0.3.4"
+description = "Building applications with LLMs through composability"
+optional = false
+python-versions = "<4.0,>=3.9"
+groups = ["main"]
+files = [
+    {file = "langchain_experimental-0.3.4-py3-none-any.whl", hash = "sha256:2e587306aea36b60fa5e5fc05dc7281bee9f60a806f0bf9d30916e0ee096af80"},
+    {file = "langchain_experimental-0.3.4.tar.gz", hash = "sha256:937c4259ee4a639c618d19acf0e2c5c2898ef127050346edc5655259aa281a21"},
+]
+
+[package.dependencies]
+langchain-community = ">=0.3.0,<0.4.0"
+langchain-core = ">=0.3.28,<0.4.0"
 
 [[package]]
 name = "langchain-openai"
@@ -1930,14 +1959,14 @@ langchain-core = ">=0.3.72,<1.0.0"
 
 [[package]]
 name = "langfuse"
-version = "3.3.4"
+version = "3.6.1"
 description = "A client library for accessing langfuse"
 optional = false
 python-versions = "<4.0,>=3.9"
 groups = ["main"]
 files = [
-    {file = "langfuse-3.3.4-py3-none-any.whl", hash = "sha256:15b9d20878cf39a48ca9cfa7e52acdfeb043603d3a9cef8cf451687a4d838c6b"},
-    {file = "langfuse-3.3.4.tar.gz", hash = "sha256:e5df4e7284298990b522e02a1dc6c3c72ebc4a7a411dc7d39255fb8c2e5a7c3a"},
+    {file = "langfuse-3.6.1-py3-none-any.whl", hash = "sha256:134e0007fcfdd9fb70b491c882bb431c8095b3f5cc5e865756f46a2abd3675a2"},
+    {file = "langfuse-3.6.1.tar.gz", hash = "sha256:eac27ee5bbd8d05e7d665e822e0efb36766b20fe281930ff040f47eb22cc1b69"},
 ]
 
 [package.dependencies]
@@ -2201,6 +2230,32 @@ files = [
     {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
 ]
 
+[[package]]
+name = "nltk"
+version = "3.9.2"
+description = "Natural Language Toolkit"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+    {file = "nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a"},
+    {file = "nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419"},
+]
+
+[package.dependencies]
+click = "*"
+joblib = "*"
+regex = ">=2021.8.3"
+tqdm = "*"
+
+[package.extras]
+all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"]
+corenlp = ["requests"]
+machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"]
+plot = ["matplotlib"]
+tgrep = ["pyparsing"]
+twitter = ["twython"]
+
 [[package]]
 name = "numpy"
 version = "2.3.1"
@@ -3109,10 +3164,10 @@ develop = false
 deprecated = "^1.2.18"
 flashrank = "^0.2.10"
 langchain = "^0.3.25"
-langchain-community = "0.3.27"
-langchain-core = "0.3.74"
+langchain-community = "0.3.30"
+langchain-core = "0.3.77"
 langchain-openai = "^0.3.27"
-langfuse = "3.3.4"
+langfuse = "3.6.1"
 oauthlib = "^3.2.2"
 openai = "^1.77.0"
 pydantic = "^2.11.4"
@@ -3246,14 +3301,14 @@ files = [
 
 [[package]]
 name = "requests"
-version = "2.32.4"
+version = "2.32.5"
 description = "Python HTTP for Humans."
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 groups = ["main"]
 files = [
-    {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
-    {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
+    {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
+    {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
 ]
 
 [package.dependencies]
@@ -3711,14 +3766,14 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "uvicorn"
-version = "0.35.0"
+version = "0.37.0"
 description = "The lightning-fast ASGI server."
 optional = false
 python-versions = ">=3.9"
 groups = ["main"]
 files = [
-    {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"},
-    {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"},
+    {file = "uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c"},
+    {file = "uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13"},
 ]
 
 [package.dependencies]
