From 86c673cb064bab6ffc92cf70ce143b02078d723f Mon Sep 17 00:00:00 2001 From: Ramzi Alashmali <186906047+RAMZI0TO99@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:45:00 +0300 Subject: [PATCH] feat: support runtime progress bar overrides --- docs-website/docs/overview/faq.mdx | 14 +++++++ .../version-3.3/overview/faq.mdx | 14 +++++++ haystack/components/converters/markdown.py | 4 +- .../embedders/azure_document_embedder.py | 1 + .../embedders/openai_document_embedder.py | 8 +++- .../evaluators/context_relevance.py | 1 + .../components/evaluators/faithfulness.py | 1 + .../components/evaluators/llm_evaluator.py | 10 ++++- haystack/utils/progress.py | 15 +++++++ ...progress-bar-control-8a89d4a7cee8ad10.yaml | 12 ++++++ .../converters/test_markdown_to_document.py | 21 ++++++++++ .../embedders/test_azure_document_embedder.py | 27 +++++++++++++ .../test_openai_document_embedder.py | 40 +++++++++++++++++++ .../evaluators/test_llm_evaluator.py | 36 +++++++++++++++++ test/utils/test_progress.py | 29 ++++++++++++++ 15 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 haystack/utils/progress.py create mode 100644 releasenotes/notes/runtime-progress-bar-control-8a89d4a7cee8ad10.yaml create mode 100644 test/utils/test_progress.py diff --git a/docs-website/docs/overview/faq.mdx b/docs-website/docs/overview/faq.mdx index 109e64ef731..a7f991eb43f 100644 --- a/docs-website/docs/overview/faq.mdx +++ b/docs-website/docs/overview/faq.mdx @@ -17,6 +17,20 @@ You will want to ensure that a CUDA enabled GPU is being engaged when Haystack i We only collect _anonymous_ usage statistics of Haystack pipeline components. Read more about telemetry in Haystack or how you can opt out on the [Telemetry](telemetry.mdx) page. +### How can I control progress bars? + +Set the `HAYSTACK_PROGRESS_BARS` environment variable to override the `progress_bar` setting for `OpenAIDocumentEmbedder`, `AzureOpenAIDocumentEmbedder`, `MarkdownToDocument`, `LLMEvaluator`, `FaithfulnessEvaluator`, and `ContextRelevanceEvaluator`: + +```python +import os + +os.environ["HAYSTACK_PROGRESS_BARS"] = "false" +``` + +Use `0`, `false`, `no`, or `off` to hide progress bars, and `1`, `true`, `yes`, or `on` to show them. Values are case-insensitive and ignore surrounding whitespace. An unset, empty, or unrecognized value uses the component's configured `progress_bar` setting. + +The variable is read when a component runs, so changes apply to subsequent runs, including asynchronous runs. It overrides the setting without changing the component's stored or serialized configuration. + ### How can I ask my questions around Haystack? For general questions, we recommend joining the [Haystack Discord ](https://discord.com/invite/xYvH6drSmA)or using [GitHub discussions](https://github.com/deepset-ai/haystack/discussions), where the community and maintainers can help. You can also explore [tutorials](https://haystack.deepset.ai/tutorials/40_building_chat_application_with_function_calling) and [examples](https://haystack.deepset.ai/cookbook/tools_support) on website to find more info. diff --git a/docs-website/versioned_docs/version-3.3/overview/faq.mdx b/docs-website/versioned_docs/version-3.3/overview/faq.mdx index 109e64ef731..a7f991eb43f 100644 --- a/docs-website/versioned_docs/version-3.3/overview/faq.mdx +++ b/docs-website/versioned_docs/version-3.3/overview/faq.mdx @@ -17,6 +17,20 @@ You will want to ensure that a CUDA enabled GPU is being engaged when Haystack i We only collect _anonymous_ usage statistics of Haystack pipeline components. Read more about telemetry in Haystack or how you can opt out on the [Telemetry](telemetry.mdx) page. +### How can I control progress bars? + +Set the `HAYSTACK_PROGRESS_BARS` environment variable to override the `progress_bar` setting for `OpenAIDocumentEmbedder`, `AzureOpenAIDocumentEmbedder`, `MarkdownToDocument`, `LLMEvaluator`, `FaithfulnessEvaluator`, and `ContextRelevanceEvaluator`: + +```python +import os + +os.environ["HAYSTACK_PROGRESS_BARS"] = "false" +``` + +Use `0`, `false`, `no`, or `off` to hide progress bars, and `1`, `true`, `yes`, or `on` to show them. Values are case-insensitive and ignore surrounding whitespace. An unset, empty, or unrecognized value uses the component's configured `progress_bar` setting. + +The variable is read when a component runs, so changes apply to subsequent runs, including asynchronous runs. It overrides the setting without changing the component's stored or serialized configuration. + ### How can I ask my questions around Haystack? For general questions, we recommend joining the [Haystack Discord ](https://discord.com/invite/xYvH6drSmA)or using [GitHub discussions](https://github.com/deepset-ai/haystack/discussions), where the community and maintainers can help. You can also explore [tutorials](https://haystack.deepset.ai/tutorials/40_building_chat_application_with_function_calling) and [examples](https://haystack.deepset.ai/cookbook/tools_support) on website to find more info. diff --git a/haystack/components/converters/markdown.py b/haystack/components/converters/markdown.py index 8fd8b21dfad..09ff10d6fe2 100644 --- a/haystack/components/converters/markdown.py +++ b/haystack/components/converters/markdown.py @@ -15,6 +15,7 @@ from haystack.components.converters.utils import get_bytestream_from_source, normalize_metadata from haystack.dataclasses import ByteStream from haystack.lazy_imports import LazyImport +from haystack.utils.progress import _get_progress_bar_setting with LazyImport("Run 'pip install markdown-it-py mdit_plain'") as markdown_conversion_imports: from markdown_it import MarkdownIt @@ -63,6 +64,7 @@ def __init__( If True converts table contents into a single line. :param progress_bar: If True shows a progress bar when running. + The `HAYSTACK_PROGRESS_BARS` environment variable can override this setting at run time. :param store_full_path: If True, the full path of the file is stored in the metadata of the document. If False, only the file name is stored. @@ -113,7 +115,7 @@ def run( zip(sources, meta_list, strict=True), total=len(sources), desc="Converting markdown files to Documents", - disable=not self.progress_bar, + disable=not _get_progress_bar_setting(self.progress_bar), ): try: bytestream = get_bytestream_from_source(source) diff --git a/haystack/components/embedders/azure_document_embedder.py b/haystack/components/embedders/azure_document_embedder.py index f4b8fa455bf..e01c274b068 100644 --- a/haystack/components/embedders/azure_document_embedder.py +++ b/haystack/components/embedders/azure_document_embedder.py @@ -93,6 +93,7 @@ def __init__( # noqa: PLR0913, PLR0917 (too-many-arguments, too-many-positional Number of documents to embed at once. :param progress_bar: If `True`, shows a progress bar when running. + The `HAYSTACK_PROGRESS_BARS` environment variable can override this setting at run time. :param meta_fields_to_embed: List of metadata fields to embed along with the document text. :param embedding_separator: diff --git a/haystack/components/embedders/openai_document_embedder.py b/haystack/components/embedders/openai_document_embedder.py index 649354cfb6c..49f30d8e693 100644 --- a/haystack/components/embedders/openai_document_embedder.py +++ b/haystack/components/embedders/openai_document_embedder.py @@ -14,6 +14,7 @@ from haystack import Document, component, default_from_dict, default_to_dict, logging from haystack.utils import Secret from haystack.utils.http_client import init_http_client +from haystack.utils.progress import _get_progress_bar_setting logger = logging.getLogger(__name__) @@ -89,6 +90,7 @@ def __init__( # noqa: PLR0913, PLR0917 (too-many-arguments, too-many-positional Number of documents to embed at once. :param progress_bar: If `True`, shows a progress bar when running. + The `HAYSTACK_PROGRESS_BARS` environment variable can override this setting at run time. :param meta_fields_to_embed: List of metadata fields to embed along with the document text. :param embedding_separator: @@ -250,7 +252,9 @@ def _embed_batch( doc_ids_to_embeddings: dict[str, list[float]] = {} meta: dict[str, Any] = {} for batch in tqdm( - batched(texts_to_embed.items(), batch_size), disable=not self.progress_bar, desc="Calculating embeddings" + batched(texts_to_embed.items(), batch_size), + disable=not _get_progress_bar_setting(self.progress_bar), + desc="Calculating embeddings", ): args: dict[str, Any] = {"model": self.model, "input": [b[1] for b in batch], "encoding_format": "float"} @@ -293,7 +297,7 @@ async def _embed_batch_async( meta: dict[str, Any] = {} batches = list(batched(texts_to_embed.items(), batch_size)) - if self.progress_bar: + if _get_progress_bar_setting(self.progress_bar): batches = async_tqdm(batches, desc="Calculating embeddings") for batch in batches: diff --git a/haystack/components/evaluators/context_relevance.py b/haystack/components/evaluators/context_relevance.py index e300752629b..e75da77fe30 100644 --- a/haystack/components/evaluators/context_relevance.py +++ b/haystack/components/evaluators/context_relevance.py @@ -134,6 +134,7 @@ def __init__( ``` :param progress_bar: Whether to show a progress bar during the evaluation. + The `HAYSTACK_PROGRESS_BARS` environment variable can override this setting at run time. :param raise_on_failure: Whether to raise an exception if the API call fails. :param chat_generator: diff --git a/haystack/components/evaluators/faithfulness.py b/haystack/components/evaluators/faithfulness.py index eb7bd2f53e4..b049c7e0798 100644 --- a/haystack/components/evaluators/faithfulness.py +++ b/haystack/components/evaluators/faithfulness.py @@ -121,6 +121,7 @@ def __init__( ``` :param progress_bar: Whether to show a progress bar during the evaluation. + The `HAYSTACK_PROGRESS_BARS` environment variable can override this setting at run time. :param raise_on_failure: Whether to raise an exception if the API call fails. :param chat_generator: diff --git a/haystack/components/evaluators/llm_evaluator.py b/haystack/components/evaluators/llm_evaluator.py index 6dd6a5d3595..acffc57b3e0 100644 --- a/haystack/components/evaluators/llm_evaluator.py +++ b/haystack/components/evaluators/llm_evaluator.py @@ -18,6 +18,7 @@ from haystack.dataclasses.chat_message import ChatMessage from haystack.utils import deserialize_chatgenerator_inplace, deserialize_type, serialize_type from haystack.utils.misc import _parse_dict_from_json +from haystack.utils.progress import _get_progress_bar_setting logger = logging.getLogger(__name__) @@ -87,6 +88,7 @@ def __init__( If True, the component will raise an exception on an unsuccessful API call. :param progress_bar: Whether to show a progress bar during the evaluation. + The `HAYSTACK_PROGRESS_BARS` environment variable can override this setting at run time. :param chat_generator: a ChatGenerator instance which represents the LLM. In order for the component to work, the LLM should be configured to return a JSON object. For example, @@ -229,7 +231,9 @@ def run(self, **inputs: Any) -> dict[str, Any]: results: list[dict[str, Any] | None] = [] metadata = [] errors = 0 - for input_names_to_values in tqdm(list_of_input_names_to_values, disable=not self.progress_bar): + for input_names_to_values in tqdm( + list_of_input_names_to_values, disable=not _get_progress_bar_setting(self.progress_bar) + ): prompt = self.builder.run(**input_names_to_values) messages = [ChatMessage.from_user(prompt["prompt"])] try: @@ -300,7 +304,9 @@ async def run_async(self, **inputs: Any) -> dict[str, Any]: errors = 0 generator_has_async = hasattr(self._chat_generator, "run_async") - for input_names_to_values in async_tqdm(list_of_input_names_to_values, disable=not self.progress_bar): + for input_names_to_values in async_tqdm( + list_of_input_names_to_values, disable=not _get_progress_bar_setting(self.progress_bar) + ): prompt = self.builder.run(**input_names_to_values) messages = [ChatMessage.from_user(prompt["prompt"])] try: diff --git a/haystack/utils/progress.py b/haystack/utils/progress.py new file mode 100644 index 00000000000..d4290a4026d --- /dev/null +++ b/haystack/utils/progress.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +import os + + +def _get_progress_bar_setting(default: bool) -> bool: + """Resolve the global override at execution time without changing component configuration.""" + value = os.getenv("HAYSTACK_PROGRESS_BARS", "").strip().lower() + if value in {"1", "true", "yes", "on"}: + return True + if value in {"0", "false", "no", "off"}: + return False + return default diff --git a/releasenotes/notes/runtime-progress-bar-control-8a89d4a7cee8ad10.yaml b/releasenotes/notes/runtime-progress-bar-control-8a89d4a7cee8ad10.yaml new file mode 100644 index 00000000000..a31bbba1a47 --- /dev/null +++ b/releasenotes/notes/runtime-progress-bar-control-8a89d4a7cee8ad10.yaml @@ -0,0 +1,12 @@ +--- +enhancements: + - | + Set ``HAYSTACK_PROGRESS_BARS`` to control progress bars in + ``OpenAIDocumentEmbedder``, ``AzureOpenAIDocumentEmbedder``, + ``MarkdownToDocument``, ``LLMEvaluator``, ``FaithfulnessEvaluator``, and + ``ContextRelevanceEvaluator``. Use ``0``, ``false``, ``no``, or ``off`` to + disable them, or ``1``, ``true``, ``yes``, or ``on`` to enable them. + Values are case-insensitive and ignore surrounding whitespace. An unset, + empty, or unrecognized value uses the component's ``progress_bar`` setting. + The override is read at execution time for synchronous and asynchronous + runs without changing the component's stored or serialized configuration. diff --git a/test/components/converters/test_markdown_to_document.py b/test/components/converters/test_markdown_to_document.py index 0df529e271f..9cc84d540c4 100644 --- a/test/components/converters/test_markdown_to_document.py +++ b/test/components/converters/test_markdown_to_document.py @@ -9,6 +9,7 @@ import pytest from haystack.components.converters.markdown import MarkdownToDocument +from haystack.core.serialization import component_from_dict, component_to_dict from haystack.dataclasses import ByteStream @@ -26,6 +27,26 @@ def test_init_params_custom(self): assert converter.progress_bar is False assert converter.store_full_path is False + @pytest.mark.parametrize("progress_bar", [True, False]) + def test_progress_bar_override_is_resolved_for_each_run(self, monkeypatch, progress_bar): + converter = MarkdownToDocument(progress_bar=progress_bar) + source = ByteStream(data=b"# Example\nDocument text.") + with patch( + "haystack.components.converters.markdown.tqdm", side_effect=lambda iterable, **kwargs: iterable + ) as bar: + for value, expected in [("0", False), ("1", True), (None, progress_bar)]: + if value is None: + monkeypatch.delenv("HAYSTACK_PROGRESS_BARS", raising=False) + else: + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", value) + result = converter.run(sources=[source]) + assert "Document text." in result["documents"][0].content + assert bar.call_args.kwargs["disable"] is not expected + assert converter.progress_bar is progress_bar + data = component_to_dict(obj=converter, name="converter") + restored = component_from_dict(cls=MarkdownToDocument, data=data, name="converter") + assert restored.progress_bar is progress_bar + @pytest.mark.integration def test_run(self, test_files_path): converter = MarkdownToDocument() diff --git a/test/components/embedders/test_azure_document_embedder.py b/test/components/embedders/test_azure_document_embedder.py index 7db21d64161..1b461c9c8ca 100644 --- a/test/components/embedders/test_azure_document_embedder.py +++ b/test/components/embedders/test_azure_document_embedder.py @@ -9,6 +9,7 @@ from openai import APIError, OpenAIError import haystack.components.embedders.azure_document_embedder as azure_document_embedder_module +import haystack.components.embedders.openai_document_embedder as openai_document_embedder_module from haystack import Document from haystack.components.embedders import AzureOpenAIDocumentEmbedder from haystack.utils.auth import Secret @@ -16,6 +17,32 @@ class TestAzureOpenAIDocumentEmbedder: + @pytest.mark.asyncio + @pytest.mark.parametrize("run_async", [False, True]) + async def test_inherited_progress_bar_override(self, monkeypatch, mock_azure_clients, run_async): + embedder = AzureOpenAIDocumentEmbedder(azure_endpoint="https://example-resource.azure.openai.com/") + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", "false") + sync_cls, async_cls = mock_azure_clients + response = Mock( + data=[Mock(embedding=[0.1, 0.2, 0.3])], + model="text-embedding-3-small", + usage={"prompt_tokens": 1, "total_tokens": 1}, + ) + sync_cls.return_value.embeddings.create.return_value = response + async_cls.return_value.embeddings.create = AsyncMock(return_value=response) + progress = Mock(side_effect=lambda iterable, **kwargs: iterable) + monkeypatch.setattr(openai_document_embedder_module, "async_tqdm" if run_async else "tqdm", progress) + + documents = [Document(content="test document")] + result = await embedder.run_async(documents=documents) if run_async else embedder.run(documents=documents) + + assert result["documents"][0].embedding == [0.1, 0.2, 0.3] + if run_async: + progress.assert_not_called() + else: + assert progress.call_args.kwargs["disable"] is True + assert embedder.progress_bar is True + def test_init_default(self, monkeypatch): monkeypatch.setenv("AZURE_OPENAI_API_KEY", "fake-api-key") embedder = AzureOpenAIDocumentEmbedder(azure_endpoint="https://example-resource.azure.openai.com/") diff --git a/test/components/embedders/test_openai_document_embedder.py b/test/components/embedders/test_openai_document_embedder.py index 7a16195b2b9..9ee7121d034 100644 --- a/test/components/embedders/test_openai_document_embedder.py +++ b/test/components/embedders/test_openai_document_embedder.py @@ -214,6 +214,46 @@ def test_run_on_empty_list(self): assert result["documents"] is not None assert not result["documents"] # empty list + @pytest.mark.asyncio + @pytest.mark.parametrize("progress_bar", [False, True]) + @pytest.mark.parametrize("run_async", [False, True]) + async def test_progress_bar_override_is_resolved_each_run( + self, monkeypatch, mock_openai_clients, progress_bar, run_async + ): + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", "false") + embedder = OpenAIDocumentEmbedder(progress_bar=progress_bar) + sync_cls, async_cls = mock_openai_clients + response = Mock( + data=[Mock(embedding=[0.1, 0.2, 0.3])], + model="text-embedding-3-small", + usage={"prompt_tokens": 1, "total_tokens": 1}, + ) + sync_cls.return_value.embeddings.create.return_value = response + async_cls.return_value.embeddings.create = AsyncMock(return_value=response) + progress = Mock(side_effect=lambda iterable, **kwargs: iterable) + monkeypatch.setattr(openai_document_embedder_module, "async_tqdm" if run_async else "tqdm", progress) + + expected_progress_calls = 0 + for override, expected in [("false", False), ("true", True), (None, progress_bar)]: + if override is None: + monkeypatch.delenv("HAYSTACK_PROGRESS_BARS") + else: + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", override) + documents = [Document(content="test document")] + result = await embedder.run_async(documents=documents) if run_async else embedder.run(documents=documents) + + assert result["documents"][0].embedding == [0.1, 0.2, 0.3] + if run_async: + expected_progress_calls += int(expected) + assert progress.call_count == expected_progress_calls + else: + assert progress.call_args.kwargs["disable"] is not expected + assert embedder.progress_bar is progress_bar + + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", "true" if not progress_bar else "false") + restored = OpenAIDocumentEmbedder.from_dict(embedder.to_dict()) + assert restored.progress_bar is progress_bar + def test_embed_batch_handles_exceptions_gracefully(self, caplog): embedder = OpenAIDocumentEmbedder(api_key=Secret.from_token("fake_api_key")) embedder.warm_up() diff --git a/test/components/evaluators/test_llm_evaluator.py b/test/components/evaluators/test_llm_evaluator.py index 35589e8f2ac..03149ece933 100644 --- a/test/components/evaluators/test_llm_evaluator.py +++ b/test/components/evaluators/test_llm_evaluator.py @@ -14,6 +14,42 @@ class TestLLMEvaluator: + @pytest.mark.asyncio + @pytest.mark.parametrize("progress_bar", [False, True]) + @pytest.mark.parametrize("run_async", [False, True]) + async def test_progress_bar_override_is_resolved_each_run(self, monkeypatch, progress_bar, run_async): + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", "false") + evaluator = LLMEvaluator( + instructions="Score the answer.", + inputs=[("predicted_answers", list[str])], + outputs=["score"], + examples=[{"inputs": {"predicted_answers": "Answer"}, "outputs": {"score": 1}}], + chat_generator=MockChatGenerator('{"score": 1}'), + progress_bar=progress_bar, + ) + progress = Mock(side_effect=lambda iterable, **kwargs: iterable) + monkeypatch.setattr( + f"haystack.components.evaluators.llm_evaluator.{'async_tqdm' if run_async else 'tqdm'}", progress + ) + + for override, expected in [("false", False), ("true", True), (None, progress_bar)]: + if override is None: + monkeypatch.delenv("HAYSTACK_PROGRESS_BARS") + else: + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", override) + if run_async: + result = await evaluator.run_async(predicted_answers=["Answer"]) + else: + result = evaluator.run(predicted_answers=["Answer"]) + + assert result["results"] == [{"score": 1}] + assert progress.call_args.kwargs["disable"] is not expected + assert evaluator.progress_bar is progress_bar + + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", "true" if not progress_bar else "false") + restored = LLMEvaluator.from_dict(evaluator.to_dict()) + assert restored.progress_bar is progress_bar + def test_init_default(self, monkeypatch): monkeypatch.setenv("OPENAI_API_KEY", "test-api-key") component = LLMEvaluator( diff --git a/test/utils/test_progress.py b/test/utils/test_progress.py new file mode 100644 index 00000000000..562872be51c --- /dev/null +++ b/test/utils/test_progress.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from haystack.utils.progress import _get_progress_bar_setting + + +@pytest.mark.parametrize("value", ["1", "true", "yes", "on", " TRUE ", "On"]) +def test_environment_can_enable_progress_bars(monkeypatch, value): + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", value) + assert _get_progress_bar_setting(default=False) is True + + +@pytest.mark.parametrize("value", ["0", "false", "no", "off", " FALSE ", "Off"]) +def test_environment_can_disable_progress_bars(monkeypatch, value): + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", value) + assert _get_progress_bar_setting(default=True) is False + + +@pytest.mark.parametrize("default", [True, False]) +@pytest.mark.parametrize("value", [None, "", " ", "invalid"]) +def test_missing_or_unrecognized_override_preserves_component_setting(monkeypatch, default, value): + if value is None: + monkeypatch.delenv("HAYSTACK_PROGRESS_BARS", raising=False) + else: + monkeypatch.setenv("HAYSTACK_PROGRESS_BARS", value) + assert _get_progress_bar_setting(default=default) is default