haystack-ai 3.3.0rc1__tar.gz → 3.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/PKG-INFO +3 -4
  2. haystack_ai-3.4.0/VERSION.txt +1 -0
  3. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/agents/agent.py +82 -17
  4. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/agents/tool_calling.py +6 -4
  5. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/agents/utils.py +1 -1
  6. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/builders/chat_prompt_builder.py +8 -16
  7. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/builders/prompt_builder.py +2 -2
  8. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/csv.py +1 -2
  9. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/docx.py +41 -2
  10. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/html.py +14 -5
  11. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/image/image_utils.py +41 -42
  12. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/multi_file_converter.py +3 -3
  13. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/azure_document_embedder.py +2 -2
  14. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/azure_text_embedder.py +3 -3
  15. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/openai_document_embedder.py +2 -2
  16. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/openai_text_embedder.py +3 -3
  17. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/answer_exact_match.py +3 -0
  18. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/context_relevance.py +2 -1
  19. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/document_map.py +4 -0
  20. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/document_mrr.py +4 -0
  21. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/document_recall.py +4 -0
  22. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/faithfulness.py +2 -1
  23. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/llm_evaluator.py +46 -13
  24. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/extractors/image/llm_document_content_extractor.py +29 -19
  25. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/extractors/llm_metadata_extractor.py +9 -10
  26. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/azure.py +5 -14
  27. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/llm.py +2 -2
  28. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/openai.py +5 -13
  29. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/openai_responses.py +18 -39
  30. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/joiners/answer_joiner.py +3 -3
  31. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/joiners/branch.py +6 -6
  32. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/joiners/list_joiner.py +3 -3
  33. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/joiners/string_joiner.py +0 -3
  34. haystack_ai-3.4.0/haystack/components/preprocessors/_page_numbers.py +38 -0
  35. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/document_splitter.py +4 -2
  36. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/embedding_based_document_splitter.py +8 -6
  37. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/markdown_header_splitter.py +19 -6
  38. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/recursive_splitter.py +101 -48
  39. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/sentence_tokenizer.py +2 -2
  40. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/query/query_expander.py +77 -27
  41. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/rankers/llm_ranker.py +61 -33
  42. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/rankers/lost_in_the_middle.py +4 -4
  43. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/rankers/meta_field.py +10 -3
  44. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/sentence_window_retriever.py +13 -8
  45. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/routers/conditional_router.py +1 -2
  46. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/validators/json_schema.py +9 -4
  47. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/component/sockets.py +0 -11
  48. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/base.py +4 -6
  49. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/breakpoint.py +4 -2
  50. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/pipeline.py +16 -19
  51. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/super_component/super_component.py +2 -2
  52. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/type_utils.py +2 -3
  53. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/chat_message.py +191 -13
  54. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/in_memory/document_store.py +9 -6
  55. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/types/filter_policy.py +31 -9
  56. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/evaluation/eval_run_result.py +1 -1
  57. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/budget/hooks.py +2 -2
  58. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/__init__.py +2 -0
  59. haystack_ai-3.4.0/haystack/hooks/compaction/tool_result_offloading.py +210 -0
  60. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/tool_result_pruning.py +8 -38
  61. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/utils.py +58 -0
  62. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/from_function.py +6 -2
  63. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/hooks.py +2 -2
  64. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/tool_result_offloading/hooks.py +24 -126
  65. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/tool_result_offloading/types/protocol.py +3 -3
  66. haystack_ai-3.4.0/haystack/hooks/tool_result_offloading/utils.py +194 -0
  67. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/logging.py +85 -11
  68. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/telemetry/_telemetry.py +1 -1
  69. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/token_counters/openai_counter.py +1 -1
  70. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/__init__.py +17 -4
  71. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/component_tool.py +27 -13
  72. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/from_function.py +2 -3
  73. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/parameters_schema_utils.py +14 -1
  74. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/pipeline_tool.py +2 -2
  75. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/searchable_toolset.py +47 -16
  76. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/skills/skill_toolset.py +9 -2
  77. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/tool.py +18 -20
  78. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/toolset.py +5 -37
  79. haystack_ai-3.4.0/haystack/tools/utils.py +116 -0
  80. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/filters.py +4 -2
  81. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/misc.py +5 -2
  82. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/pyproject.toml +5 -6
  83. haystack_ai-3.3.0rc1/VERSION.txt +0 -1
  84. haystack_ai-3.3.0rc1/haystack/tools/utils.py +0 -64
  85. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/.gitignore +0 -0
  86. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/LICENSE +0 -0
  87. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/README.md +0 -0
  88. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/__init__.py +0 -0
  89. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/__init__.py +0 -0
  90. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/agents/__init__.py +0 -0
  91. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/agents/state/__init__.py +0 -0
  92. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/agents/state/state.py +0 -0
  93. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/agents/state/state_utils.py +0 -0
  94. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/builders/__init__.py +0 -0
  95. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/builders/answer_builder.py +0 -0
  96. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/caching/__init__.py +0 -0
  97. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/caching/cache_checker.py +0 -0
  98. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/__init__.py +0 -0
  99. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/file_to_file_content.py +0 -0
  100. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/image/__init__.py +0 -0
  101. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/image/document_to_image.py +0 -0
  102. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/image/file_to_document.py +0 -0
  103. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/image/file_to_image.py +0 -0
  104. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/image/pdf_to_image.py +0 -0
  105. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/json.py +0 -0
  106. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/markdown.py +0 -0
  107. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/msg.py +0 -0
  108. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/output_adapter.py +0 -0
  109. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/pdfminer.py +0 -0
  110. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/pptx.py +0 -0
  111. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/pypdf.py +0 -0
  112. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/txt.py +0 -0
  113. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/utils.py +0 -0
  114. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/converters/xlsx.py +0 -0
  115. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/__init__.py +0 -0
  116. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/mock_document_embedder.py +0 -0
  117. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/mock_text_embedder.py +0 -0
  118. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/mock_utils.py +0 -0
  119. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/types/__init__.py +0 -0
  120. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/embedders/types/protocol.py +0 -0
  121. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/__init__.py +0 -0
  122. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/document_ndcg.py +0 -0
  123. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/evaluators/sas_evaluator.py +0 -0
  124. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/extractors/__init__.py +0 -0
  125. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/extractors/image/__init__.py +0 -0
  126. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/extractors/regex_text_extractor.py +0 -0
  127. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/fetchers/__init__.py +0 -0
  128. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/fetchers/link_content.py +0 -0
  129. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/__init__.py +0 -0
  130. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/__init__.py +0 -0
  131. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/azure_responses.py +0 -0
  132. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/fallback.py +0 -0
  133. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/mock.py +0 -0
  134. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/types/__init__.py +0 -0
  135. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/chat/types/protocol.py +0 -0
  136. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/openai_image_generator.py +0 -0
  137. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/generators/utils.py +0 -0
  138. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/joiners/__init__.py +0 -0
  139. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/joiners/document_joiner.py +0 -0
  140. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/__init__.py +0 -0
  141. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/csv_document_cleaner.py +0 -0
  142. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/csv_document_splitter.py +0 -0
  143. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/document_cleaner.py +0 -0
  144. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/document_preprocessor.py +0 -0
  145. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/hierarchical_document_splitter.py +0 -0
  146. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/python_code_splitter.py +0 -0
  147. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/preprocessors/text_cleaner.py +0 -0
  148. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/query/__init__.py +0 -0
  149. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/rankers/__init__.py +0 -0
  150. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/rankers/meta_field_grouping_ranker.py +0 -0
  151. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/__init__.py +0 -0
  152. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/auto_merging_retriever.py +0 -0
  153. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/filter_retriever.py +0 -0
  154. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/in_memory/__init__.py +0 -0
  155. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/in_memory/bm25_retriever.py +0 -0
  156. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/in_memory/embedding_retriever.py +0 -0
  157. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/multi_query_embedding_retriever.py +0 -0
  158. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/multi_query_text_retriever.py +0 -0
  159. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/multi_retriever.py +0 -0
  160. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/text_embedding_retriever.py +0 -0
  161. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/types/__init__.py +0 -0
  162. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/retrievers/types/protocol.py +0 -0
  163. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/routers/__init__.py +0 -0
  164. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/routers/document_length_router.py +0 -0
  165. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/routers/document_type_router.py +0 -0
  166. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/routers/file_type_router.py +0 -0
  167. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/routers/llm_messages_router.py +0 -0
  168. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/routers/metadata_router.py +0 -0
  169. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/samplers/__init__.py +0 -0
  170. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/samplers/top_p.py +0 -0
  171. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/validators/__init__.py +0 -0
  172. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/writers/__init__.py +0 -0
  173. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/components/writers/document_writer.py +0 -0
  174. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/__init__.py +0 -0
  175. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/component/__init__.py +0 -0
  176. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/component/component.py +0 -0
  177. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/component/types.py +0 -0
  178. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/errors.py +0 -0
  179. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/__init__.py +0 -0
  180. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/component_checks.py +0 -0
  181. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/descriptions.py +0 -0
  182. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/draw.py +0 -0
  183. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/pipeline/utils.py +0 -0
  184. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/serialization.py +0 -0
  185. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/serialization_security.py +0 -0
  186. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/super_component/__init__.py +0 -0
  187. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/core/super_component/utils.py +0 -0
  188. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/data/abbreviations/de.txt +0 -0
  189. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/data/abbreviations/en.txt +0 -0
  190. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/__init__.py +0 -0
  191. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/answer.py +0 -0
  192. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/breakpoints.py +0 -0
  193. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/byte_stream.py +0 -0
  194. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/document.py +0 -0
  195. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/file_content.py +0 -0
  196. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/image_content.py +0 -0
  197. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/skill_info.py +0 -0
  198. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/sparse_embedding.py +0 -0
  199. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/dataclasses/streaming_chunk.py +0 -0
  200. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/__init__.py +0 -0
  201. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/errors/__init__.py +0 -0
  202. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/errors/errors.py +0 -0
  203. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/in_memory/__init__.py +0 -0
  204. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/types/__init__.py +0 -0
  205. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/types/policy.py +0 -0
  206. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/document_stores/types/protocol.py +0 -0
  207. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/errors.py +0 -0
  208. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/evaluation/__init__.py +0 -0
  209. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/__init__.py +0 -0
  210. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/budget/__init__.py +0 -0
  211. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/AGENTS.md +0 -0
  212. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/CLAUDE.md +0 -0
  213. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/hooks.py +0 -0
  214. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/sliding_window.py +0 -0
  215. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/summarization.py +0 -0
  216. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/types/__init__.py +0 -0
  217. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/compaction/types/protocol.py +0 -0
  218. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/__init__.py +0 -0
  219. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/dataclasses.py +0 -0
  220. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/policies.py +0 -0
  221. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/strategies.py +0 -0
  222. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/types/__init__.py +0 -0
  223. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/types/protocol.py +0 -0
  224. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/human_in_the_loop/user_interfaces.py +0 -0
  225. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/invocation.py +0 -0
  226. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/protocol.py +0 -0
  227. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/tool_result_offloading/__init__.py +0 -0
  228. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/tool_result_offloading/policies.py +0 -0
  229. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/tool_result_offloading/stores.py +0 -0
  230. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/tool_result_offloading/types/__init__.py +0 -0
  231. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/hooks/utils.py +0 -0
  232. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/lazy_imports.py +0 -0
  233. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/marshal/__init__.py +0 -0
  234. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/marshal/protocol.py +0 -0
  235. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/marshal/yaml.py +0 -0
  236. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/py.typed +0 -0
  237. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/skill_stores/__init__.py +0 -0
  238. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/skill_stores/file_system/__init__.py +0 -0
  239. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/skill_stores/file_system/skill_store.py +0 -0
  240. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/skill_stores/types/__init__.py +0 -0
  241. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/skill_stores/types/protocol.py +0 -0
  242. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/telemetry/__init__.py +0 -0
  243. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/telemetry/_environment.py +0 -0
  244. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/__init__.py +0 -0
  245. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/callable_serialization/random_callable.py +0 -0
  246. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/document_store.py +0 -0
  247. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/document_store_async.py +0 -0
  248. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/factory.py +0 -0
  249. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/__init__.py +0 -0
  250. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/accumulate.py +0 -0
  251. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/add_value.py +0 -0
  252. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/concatenate.py +0 -0
  253. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/double.py +0 -0
  254. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/fstring.py +0 -0
  255. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/future_annotations.py +0 -0
  256. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/greet.py +0 -0
  257. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/hello.py +0 -0
  258. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/joiner.py +0 -0
  259. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/parity.py +0 -0
  260. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/remainder.py +0 -0
  261. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/repeat.py +0 -0
  262. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/subtract.py +0 -0
  263. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/sum.py +0 -0
  264. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/text_splitter.py +0 -0
  265. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/sample_components/threshold.py +0 -0
  266. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/telemetry.py +0 -0
  267. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/testing/test_utils.py +0 -0
  268. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/token_counters/__init__.py +0 -0
  269. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/token_counters/approximate_counter.py +0 -0
  270. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/token_counters/tiktoken_counter.py +0 -0
  271. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/token_counters/types/__init__.py +0 -0
  272. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/token_counters/types/protocol.py +0 -0
  273. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/token_counters/utils.py +0 -0
  274. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/agent_tool.py +0 -0
  275. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/errors.py +0 -0
  276. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/serde_utils.py +0 -0
  277. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/skills/__init__.py +0 -0
  278. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tools/tool_types.py +0 -0
  279. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tracing/__init__.py +0 -0
  280. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tracing/logging_tracer.py +0 -0
  281. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tracing/tracer.py +0 -0
  282. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/tracing/utils.py +0 -0
  283. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/__init__.py +0 -0
  284. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/async_utils.py +0 -0
  285. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/auth.py +0 -0
  286. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/azure.py +0 -0
  287. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/base_serialization.py +0 -0
  288. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/callable_serialization.py +0 -0
  289. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/dataclasses.py +0 -0
  290. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/deserialization.py +0 -0
  291. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/device.py +0 -0
  292. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/experimental.py +0 -0
  293. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/hf.py +0 -0
  294. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/http_client.py +0 -0
  295. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/jinja2_chat_extension.py +0 -0
  296. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/jinja2_extensions.py +0 -0
  297. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/jinja2_sandbox.py +0 -0
  298. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/jupyter.py +0 -0
  299. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/requests_utils.py +0 -0
  300. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/type_serialization.py +0 -0
  301. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/utils/url_validation.py +0 -0
  302. {haystack_ai-3.3.0rc1 → haystack_ai-3.4.0}/haystack/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: haystack-ai
3
- Version: 3.3.0-rc1
3
+ Version: 3.4.0
4
4
  Summary: LLM framework to build customizable, production-ready LLM applications. Connect components (models, vector DBs, file converters) to pipelines or agents that can interact with your data.
5
5
  Project-URL: CI: GitHub, https://github.com/deepset-ai/haystack/actions
6
6
  Project-URL: Docs: RTD, https://haystack.deepset.ai/overview/intro
@@ -18,13 +18,13 @@ Classifier: License :: OSI Approved :: Apache Software License
18
18
  Classifier: Operating System :: OS Independent
19
19
  Classifier: Programming Language :: Python
20
20
  Classifier: Programming Language :: Python :: 3
21
- Classifier: Programming Language :: Python :: 3.10
22
21
  Classifier: Programming Language :: Python :: 3.11
23
22
  Classifier: Programming Language :: Python :: 3.12
24
23
  Classifier: Programming Language :: Python :: 3.13
25
24
  Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: 3.15
26
26
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
- Requires-Python: >=3.10
27
+ Requires-Python: >=3.11
28
28
  Requires-Dist: anyio>=4.14.2
29
29
  Requires-Dist: docstring-parser
30
30
  Requires-Dist: filetype
@@ -43,7 +43,6 @@ Requires-Dist: python-dateutil
43
43
  Requires-Dist: pyyaml
44
44
  Requires-Dist: tenacity!=8.4.0
45
45
  Requires-Dist: tqdm
46
- Requires-Dist: typing-extensions>=4.7
47
46
  Description-Content-Type: text/markdown
48
47
 
49
48
  <div align="center">
@@ -0,0 +1 @@
1
+ 3.4.0
@@ -3,6 +3,7 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import inspect
6
+ from collections import Counter
6
7
  from dataclasses import dataclass
7
8
  from typing import Any, Literal, cast
8
9
 
@@ -28,7 +29,12 @@ from haystack.components.agents.utils import (
28
29
  )
29
30
  from haystack.components.builders import ChatPromptBuilder
30
31
  from haystack.components.generators.chat.types import ChatGenerator
31
- from haystack.core.serialization import component_to_dict, default_from_dict, default_to_dict
32
+ from haystack.core.serialization import (
33
+ component_to_dict,
34
+ default_from_dict,
35
+ default_to_dict,
36
+ generate_qualified_class_name,
37
+ )
32
38
  from haystack.dataclasses import ChatMessage, ChatRole, StreamingCallbackT, select_streaming_callback
33
39
  from haystack.hooks.invocation import _run_hooks, _run_hooks_async
34
40
  from haystack.hooks.protocol import (
@@ -51,13 +57,18 @@ from haystack.hooks.utils import (
51
57
  warm_up_hooks_async,
52
58
  )
53
59
  from haystack.tools import (
60
+ ComponentTool,
61
+ Tool,
54
62
  Toolset,
55
63
  ToolsType,
56
64
  _check_duplicate_tool_names,
65
+ close_tools,
66
+ close_tools_async,
57
67
  deserialize_tools_or_toolset_inplace,
58
68
  flatten_tools_or_toolsets,
59
69
  serialize_tools_or_toolset,
60
70
  warm_up_tools,
71
+ warm_up_tools_async,
61
72
  )
62
73
  from haystack.utils.async_utils import _execute_component_async
63
74
  from haystack.utils.callable_serialization import deserialize_callable, serialize_callable
@@ -199,6 +210,34 @@ def _pending_tool_call_messages_from_state(state: State) -> list[ChatMessage]:
199
210
  return [last_message] if last_message.tool_calls else []
200
211
 
201
212
 
213
+ def _get_tool_telemetry_data(tool: Tool) -> dict[str, Any]:
214
+ """
215
+ Describe a single tool for telemetry.
216
+
217
+ Descriptions are deliberately left out: they are free text that can hold personal or confidential data.
218
+ """
219
+ data = {"type": generate_qualified_class_name(type(tool)), "name": tool.name}
220
+ if isinstance(tool, ComponentTool):
221
+ data["component"] = generate_qualified_class_name(type(tool._component))
222
+ return data
223
+
224
+
225
+ def _get_tools_telemetry_data(tools: ToolsType) -> dict[str, Any]:
226
+ """
227
+ Summarize the tools for telemetry.
228
+
229
+ Call it only after the tools are warmed up: flattening iterates the Toolsets, and some of them (e.g.
230
+ SearchableToolset) warm up on iteration, while others (e.g. MCPToolset) only load their tools in `warm_up()`.
231
+ """
232
+ toolsets = [tools] if isinstance(tools, Toolset) else [entry for entry in tools if isinstance(entry, Toolset)]
233
+ flat_tools = flatten_tools_or_toolsets(tools=tools)
234
+ return {
235
+ "count": len(flat_tools),
236
+ "tools": [_get_tool_telemetry_data(tool) for tool in flat_tools],
237
+ "toolset_types": dict(Counter(generate_qualified_class_name(type(toolset)) for toolset in toolsets)),
238
+ }
239
+
240
+
202
241
  @dataclass(kw_only=True)
203
242
  class _ExecutionContext:
204
243
  """
@@ -241,7 +280,7 @@ class Agent:
241
280
 
242
281
  ```python
243
282
  from haystack.components.agents import Agent
244
- from haystack.components.generators.chat import OpenAIChatGenerator
283
+ from haystack.components.generators.chat import OpenAIResponsesChatGenerator
245
284
  from haystack.components.generators.utils import print_streaming_chunk
246
285
  from haystack.dataclasses import ChatMessage
247
286
  from haystack.tools import tool
@@ -272,7 +311,7 @@ class Agent:
272
311
  "You are a helpful assistant. Use the 'search' tool to find information "
273
312
  "about a user's question and the 'calculator' tool to perform math."
274
313
  ),
275
- chat_generator=OpenAIChatGenerator(),
314
+ chat_generator=OpenAIResponsesChatGenerator(),
276
315
  tools=[search, calculator],
277
316
  streaming_callback=print_streaming_chunk,
278
317
  )
@@ -293,7 +332,7 @@ class Agent:
293
332
 
294
333
  ```python
295
334
  from haystack.components.agents import Agent
296
- from haystack.components.generators.chat import OpenAIChatGenerator
335
+ from haystack.components.generators.chat import OpenAIResponsesChatGenerator
297
336
  from haystack.tools import tool
298
337
  from typing import Annotated
299
338
 
@@ -308,7 +347,7 @@ class Agent:
308
347
  return f"[Translated '{text}' to {target_language}]"
309
348
 
310
349
  agent = Agent(
311
- chat_generator=OpenAIChatGenerator(),
350
+ chat_generator=OpenAIResponsesChatGenerator(),
312
351
  tools=[translate],
313
352
  system_prompt="You are a helpful translation assistant.",
314
353
  user_prompt=\"\"\"{% message role="user"%}
@@ -341,13 +380,13 @@ class Agent:
341
380
  - `on_exit`: runs when the Agent is about to stop on an exit condition. An `on_exit` hook can keep the Agent
342
381
  running by setting `state.set("continue_run", True)`.
343
382
 
344
- Use the `@hook` decorator to build a hook from a function. This `on_exit` hook keeps the Agent running until a
345
- required tool has been called.
383
+ Use the `@hook` decorator to build a hook from a function. This `on_exit` hook keeps the Agent running when it
384
+ stops with a text reply, until it finishes by calling `save_result`.
346
385
 
347
386
  ```python
348
387
  from haystack.components.agents import Agent
349
388
  from haystack.components.agents.state import State
350
- from haystack.components.generators.chat import OpenAIChatGenerator
389
+ from haystack.components.generators.chat import OpenAIResponsesChatGenerator
351
390
  from haystack.dataclasses import ChatMessage
352
391
  from haystack.hooks import hook
353
392
  from haystack.tools import tool
@@ -363,14 +402,15 @@ class Agent:
363
402
 
364
403
  @hook
365
404
  def require_save(state: State) -> None:
366
- if state.get("tool_call_counts", {}).get("save_result", 0) == 0:
367
- state.set("messages", [ChatMessage.from_system("Call `save_result` before finishing.")])
405
+ if state.data.get("exit_reason") == "text":
406
+ state.set("messages", [ChatMessage.from_user("Call `save_result` before finishing.")])
368
407
  state.set("continue_run", True) # keep the Agent running instead of stopping
369
408
 
370
409
 
371
410
  agent = Agent(
372
- chat_generator=OpenAIChatGenerator(),
411
+ chat_generator=OpenAIResponsesChatGenerator(),
373
412
  tools=[save_result],
413
+ exit_conditions=["save_result"],
374
414
  hooks={"on_exit": [require_save]},
375
415
  )
376
416
  ```
@@ -601,7 +641,7 @@ class Agent:
601
641
 
602
642
  async def warm_up_async(self) -> None:
603
643
  """Warm up the tools, hooks, and the underlying chat generator on the serving event loop."""
604
- warm_up_tools(tools=self.tools)
644
+ await warm_up_tools_async(tools=self.tools)
605
645
  await warm_up_hooks_async(self.hooks)
606
646
  if hasattr(self.chat_generator, "warm_up_async"):
607
647
  await self.chat_generator.warm_up_async()
@@ -609,13 +649,15 @@ class Agent:
609
649
  self.chat_generator.warm_up()
610
650
 
611
651
  def close(self) -> None:
612
- """Release the hooks' and the underlying chat generator's resources."""
652
+ """Release tools, hooks, and chat generator resources."""
653
+ close_tools(tools=self.tools)
613
654
  close_hooks(self.hooks)
614
655
  if hasattr(self.chat_generator, "close"):
615
656
  self.chat_generator.close()
616
657
 
617
658
  async def close_async(self) -> None:
618
- """Release the hooks' and the underlying chat generator's async resources."""
659
+ """Release async tools, hooks, and chat generator resources."""
660
+ await close_tools_async(tools=self.tools)
619
661
  await close_hooks_async(self.hooks)
620
662
  if hasattr(self.chat_generator, "close_async"):
621
663
  await self.chat_generator.close_async()
@@ -633,6 +675,15 @@ class Agent:
633
675
  params: dict[str, Any] = {name: getattr(self, name) for name in init_params if name != "self"}
634
676
  return type(self)(**{**params, **overrides})
635
677
 
678
+ def _get_telemetry_data(self) -> dict[str, Any]:
679
+ """
680
+ Data that is sent to Posthog for usage analytics.
681
+ """
682
+ chat_generator_data: dict[str, Any] = {"type": generate_qualified_class_name(type(self.chat_generator))}
683
+ if hasattr(self.chat_generator, "_get_telemetry_data"):
684
+ chat_generator_data.update(self.chat_generator._get_telemetry_data())
685
+ return {"chat_generator": chat_generator_data, "tools": _get_tools_telemetry_data(self.tools)}
686
+
636
687
  def to_dict(self) -> dict[str, Any]:
637
688
  """
638
689
  Serialize the component to a dictionary.
@@ -817,9 +868,6 @@ class Agent:
817
868
 
818
869
  if isinstance(tools, (Toolset, list)):
819
870
  selected = cast(ToolsType, tools) # mypy can't narrow the Union type from the isinstance checks
820
- # Per-run tools are not covered by the Agent's own warm_up(), so warm them up here.
821
- # warm_up() is expected to be idempotent, so re-warming on every run is cheap.
822
- warm_up_tools(tools=selected)
823
871
  return _spawn_tools(tools=selected)
824
872
 
825
873
  raise TypeError(
@@ -847,6 +895,8 @@ class Agent:
847
895
  precedence, keys set only at initialization are kept.
848
896
  :param tools: Optional list of Tool objects, a Toolset, or list of tool names to use for this run.
849
897
  When passing tool names, tools are selected from the Agent's originally configured tools.
898
+ Tool and Toolset objects passed here are warmed up automatically; the caller is responsible for
899
+ closing them if they hold resources.
850
900
  :param hook_context: Optional dictionary of request-scoped resources made available to hooks via
851
901
  `state.data.get("hook_context")`. Useful in web/server environments to provide per-request objects
852
902
  (e.g., WebSocket connections, async queues, Redis pub/sub clients) that a hook can use, for
@@ -874,6 +924,12 @@ class Agent:
874
924
  """
875
925
  agent_inputs = {"messages": messages, "streaming_callback": streaming_callback, **kwargs}
876
926
  self.warm_up()
927
+ # warm up tools passed at runtime
928
+ if isinstance(tools, Toolset):
929
+ tools_to_warm_up: ToolsType = tools
930
+ else:
931
+ tools_to_warm_up = [tool for tool in tools or [] if not isinstance(tool, str)]
932
+ warm_up_tools(tools=tools_to_warm_up)
877
933
 
878
934
  exe_context = self._initialize_fresh_execution(
879
935
  messages=messages,
@@ -934,6 +990,9 @@ class Agent:
934
990
  with the `generation_kwargs` passed at the chat generator's initialization: keys provided here take
935
991
  precedence, keys set only at initialization are kept.
936
992
  :param tools: Optional list of Tool objects, a Toolset, or list of tool names to use for this run.
993
+ When passing tool names, tools are selected from the Agent's originally configured tools.
994
+ Tool and Toolset objects passed here are warmed up automatically; the caller is responsible for
995
+ closing them if they hold resources.
937
996
  :param hook_context: Optional dictionary of request-scoped resources made available to hooks via
938
997
  `state.data.get("hook_context")`. Useful in web/server environments to provide per-request objects
939
998
  (e.g., WebSocket connections, async queues, Redis pub/sub clients) that a hook can use, for
@@ -961,6 +1020,12 @@ class Agent:
961
1020
  """
962
1021
  agent_inputs = {"messages": messages, "streaming_callback": streaming_callback, **kwargs}
963
1022
  await self.warm_up_async()
1023
+ # warm up tools passed at runtime
1024
+ if isinstance(tools, Toolset):
1025
+ tools_to_warm_up: ToolsType = tools
1026
+ else:
1027
+ tools_to_warm_up = [tool for tool in tools or [] if not isinstance(tool, str)]
1028
+ await warm_up_tools_async(tools=tools_to_warm_up)
964
1029
 
965
1030
  exe_context = self._initialize_fresh_execution(
966
1031
  messages=messages,
@@ -17,7 +17,7 @@ from haystack.dataclasses import ChatMessage, ToolCall
17
17
  from haystack.dataclasses.streaming_chunk import StreamingCallbackT, StreamingChunk, _invoke_streaming_callback
18
18
  from haystack.tools import ComponentTool, Tool, ToolsType, _check_duplicate_tool_names, flatten_tools_or_toolsets
19
19
  from haystack.tools.errors import ToolInvocationError
20
- from haystack.tools.parameters_schema_utils import _unwrap_optional
20
+ from haystack.tools.parameters_schema_utils import _is_state_annotation
21
21
  from haystack.tracing.utils import _serializable_value
22
22
 
23
23
  logger = logging.getLogger(__name__)
@@ -187,7 +187,7 @@ def _create_tool_span(tool: Tool, tool_call: ToolCall, parent_span: tracing.Span
187
187
  "execute tool" span per invocation
188
188
  (https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-agent-spans.md#execute-tool-span),
189
189
  and tracing backends such as Langfuse follow the same model. The span is tagged with the tool's identity; the caller
190
- adds the call arguments and result as content tags.
190
+ adds the call arguments and result as content tags, and sets `haystack.tool.error` to `True` when the call fails.
191
191
 
192
192
  The parent is passed in rather than read from the tracer here: the calls of a step run concurrently, so a tracer
193
193
  that tracks the active span globally would report a sibling tool span as current and the spans would end up chained
@@ -218,6 +218,7 @@ def _make_context_bound_invoke(
218
218
  try:
219
219
  result = tool.invoke(**args)
220
220
  except ToolInvocationError as e:
221
+ span.set_tag("haystack.tool.error", True)
221
222
  span.set_content_tag("haystack.agent.step.tool.output", {"error": str(e)})
222
223
  return e
223
224
  span.set_content_tag("haystack.agent.step.tool.output", result)
@@ -255,6 +256,7 @@ def _make_bounded_invoke_async(
255
256
  try:
256
257
  result = await tool.invoke_async(**args)
257
258
  except ToolInvocationError as e:
259
+ span.set_tag("haystack.tool.error", True)
258
260
  span.set_content_tag("haystack.agent.step.tool.output", {"error": str(e)})
259
261
  return e
260
262
  span.set_content_tag("haystack.agent.step.tool.output", result)
@@ -307,7 +309,7 @@ def _inject_state_args(tool: Tool, llm_args: dict[str, Any], state: State) -> di
307
309
 
308
310
  # We also inject the full State object for any parameter annotated as State
309
311
  for param_name, param_type in func_params.items():
310
- if _unwrap_optional(param_type) is State:
312
+ if _is_state_annotation(param_type):
311
313
  final_args[param_name] = state
312
314
 
313
315
  return final_args
@@ -424,7 +426,7 @@ def _state_io_for_call(tool: Tool, llm_args: dict[str, Any]) -> tuple[_StateKeys
424
426
  """
425
427
  func_params = _get_func_params(tool)
426
428
  # Check if State is in func_params
427
- if any(_unwrap_optional(param_type) is State for param_type in func_params.values()):
429
+ if any(_is_state_annotation(param_type) for param_type in func_params.values()):
428
430
  return _ALL_STATE_KEYS, _ALL_STATE_KEYS
429
431
 
430
432
  # Calculate reads
@@ -115,7 +115,7 @@ def _select_tools_by_name(configured_tools: ToolsType, names: list[str]) -> list
115
115
  Standalone Tools are kept when their name is requested. A Toolset with run-scoped state (one overriding
116
116
  `spawn()`, such as SearchableToolset) is replaced by a per-run copy carrying the requested names, so its
117
117
  dynamic behavior (search/lazy-loading) is preserved without mutating the shared, configured Toolset. Any
118
- other Toolset is warmed up and reduced to the matching Tools.
118
+ other Toolset is reduced to the matching Tools. The caller must warm up the tools before selection.
119
119
 
120
120
  :param configured_tools: The tools configured on the Agent.
121
121
  :param names: The requested tool names.
@@ -68,13 +68,13 @@ class ChatPromptBuilder:
68
68
 
69
69
  ```python
70
70
  from haystack.components.builders import ChatPromptBuilder
71
- from haystack.components.generators.chat import OpenAIChatGenerator
71
+ from haystack.components.generators.chat import OpenAIResponsesChatGenerator
72
72
  from haystack.dataclasses import ChatMessage
73
73
  from haystack import Pipeline
74
74
 
75
75
  # no parameter init, we don't use any runtime template variables
76
76
  prompt_builder = ChatPromptBuilder()
77
- llm = OpenAIChatGenerator(model="gpt-5-mini")
77
+ llm = OpenAIResponsesChatGenerator(model="gpt-5.6-luna")
78
78
 
79
79
  pipe = Pipeline()
80
80
  pipe.add_component("prompt_builder", prompt_builder)
@@ -88,26 +88,18 @@ class ChatPromptBuilder:
88
88
 
89
89
  res = pipe.run(data={"prompt_builder": {"template_variables": {"location": location, "language": language},
90
90
  "template": messages}})
91
- print(res)
92
- # >> {'llm': {'replies': [ChatMessage(_role=<ChatRole.ASSISTANT: 'assistant'>, _content=[TextContent(text=
93
- # "Berlin is the capital city of Germany and one of the most vibrant
94
- # and diverse cities in Europe. Here are some key things to know...Enjoy your time exploring the vibrant and dynamic
95
- # capital of Germany!")], _name=None, _meta={'model': 'gpt-5-mini',
96
- # 'index': 0, 'finish_reason': 'stop', 'usage': {'prompt_tokens': 27, 'completion_tokens': 681, 'total_tokens':
97
- # 708}})]}}
91
+ print(res["llm"]["replies"][0].text)
92
+ # >> Berlin is the capital city of Germany and one of the most vibrant and diverse cities in Europe. Here are some
93
+ # >> key things to know...Enjoy your time exploring the vibrant and dynamic capital of Germany!
98
94
 
99
95
  messages = [system_message, ChatMessage.from_user("What's the weather forecast for {{location}} in the next {{day_count}} days?")]
100
96
 
101
97
  res = pipe.run(data={"prompt_builder": {"template_variables": {"location": location, "day_count": "5"},
102
98
  "template": messages}})
103
99
 
104
- print(res)
105
- # >> {'llm': {'replies': [ChatMessage(_role=<ChatRole.ASSISTANT: 'assistant'>, _content=[TextContent(text=
106
- # "Here is the weather forecast for Berlin in the next 5
107
- # days:\\n\\nDay 1: Mostly cloudy with a high of 22°C (72°F) and...so it's always a good idea to check for updates
108
- # closer to your visit.")], _name=None, _meta={'model': 'gpt-5-mini',
109
- # 'index': 0, 'finish_reason': 'stop', 'usage': {'prompt_tokens': 37, 'completion_tokens': 201,
110
- # 'total_tokens': 238}})]}}
100
+ print(res["llm"]["replies"][0].text)
101
+ # >> Here is the weather forecast for Berlin in the next 5 days: Day 1: Mostly cloudy with a high of 22°C (72°F)
102
+ # >> and...so it's always a good idea to check for updates closer to your visit.
111
103
  ```
112
104
 
113
105
  #### String prompt template
@@ -47,7 +47,7 @@ class PromptBuilder:
47
47
  ```python
48
48
  from haystack import Pipeline, Document
49
49
  from haystack.utils import Secret
50
- from haystack.components.generators.chat import OpenAIChatGenerator
50
+ from haystack.components.generators.chat import OpenAIResponsesChatGenerator
51
51
  from haystack.components.builders.prompt_builder import PromptBuilder
52
52
 
53
53
  # in a real world use case documents could come from a retriever, web, or any other source
@@ -64,7 +64,7 @@ class PromptBuilder:
64
64
  \"\"\"
65
65
  p = Pipeline()
66
66
  p.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder")
67
- p.add_component(instance=OpenAIChatGenerator(api_key=Secret.from_env_var("OPENAI_API_KEY")), name="llm")
67
+ p.add_component(instance=OpenAIResponsesChatGenerator(api_key=Secret.from_env_var("OPENAI_API_KEY")), name="llm")
68
68
  p.connect("prompt_builder", "llm")
69
69
 
70
70
  question = "Where does Joe live?"
@@ -215,7 +215,7 @@ class CSVToDocument:
215
215
  Remaining row columns are added to ``meta`` with collision-safe
216
216
  keys (prefixed with ``csv_`` if needed).
217
217
  """
218
- row_meta = dict(base_meta)
218
+ row_meta = {**base_meta, "row_number": row_index}
219
219
 
220
220
  # content (strict: content_column must exist; validated by caller)
221
221
  content = self._safe_value(row.get(content_column))
@@ -235,5 +235,4 @@ class CSVToDocument:
235
235
  suffix += 1
236
236
  row_meta[key_to_use] = self._safe_value(v)
237
237
 
238
- row_meta["row_number"] = row_index
239
238
  return Document(content=content, meta=row_meta)
@@ -19,6 +19,14 @@ from haystack.lazy_imports import LazyImport
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
+ _WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
23
+ _MARKUP_COMPATIBILITY_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006"
24
+ # A text box keeps its paragraphs here, nested under the drawing inside a run.
25
+ _TEXT_BOX_CONTENT_TAG = f"{{{_WORD_NS}}}txbxContent"
26
+ # Word writes a text box twice inside `mc:AlternateContent`: a `wps:txbx` under
27
+ # `mc:Choice` and the same text as a VML text box under `mc:Fallback`.
28
+ _FALLBACK_TAG = f"{{{_MARKUP_COMPATIBILITY_NS}}}Fallback"
29
+
22
30
  # A Markdown table row ends at a line break and its columns are separated by pipes, so
23
31
  # neither can survive inside a cell.
24
32
  _MARKDOWN_CELL_BREAK_PATTERN = re.compile(r"\s*(?:\r\n|\r|\n)\s*")
@@ -251,6 +259,7 @@ class DOCXToDocument:
251
259
  else:
252
260
  para_text = self._process_links_in_paragraph(paragraph)
253
261
  elements.append(para_text)
262
+ elements.extend(self._extract_text_boxes(element, document))
254
263
  elif element.tag.endswith("tbl"):
255
264
  table = docx.table.Table(element, document)
256
265
  table_str = (
@@ -262,6 +271,34 @@ class DOCXToDocument:
262
271
 
263
272
  return elements
264
273
 
274
+ def _extract_text_boxes(self, element: Any, document: "DocxDocument") -> list[str]:
275
+ """
276
+ Extracts the text of any text box anchored to a paragraph.
277
+
278
+ A text box holds its own paragraphs and tables under `w:txbxContent`, nested in
279
+ the drawing inside a run, so the anchoring paragraph's text never reaches them.
280
+
281
+ :param element: The `w:p` element to look under.
282
+ :param document: The DOCX Document object.
283
+ :returns: List of strings, one per paragraph or table found inside a text box.
284
+ """
285
+ elements = []
286
+ for text_box in element.iter(_TEXT_BOX_CONTENT_TAG):
287
+ # Skip the `mc:Fallback` copy, or the same text is emitted twice.
288
+ if any(ancestor.tag == _FALLBACK_TAG for ancestor in text_box.iterancestors()):
289
+ continue
290
+ for child in text_box:
291
+ if child.tag.endswith("p"):
292
+ elements.append(self._process_links_in_paragraph(Paragraph(child, document)))
293
+ elif child.tag.endswith("tbl"):
294
+ table = docx.table.Table(child, document)
295
+ elements.append(
296
+ self._table_to_markdown(table)
297
+ if self.table_format == DOCXTableFormat.MARKDOWN
298
+ else self._table_to_csv(table)
299
+ )
300
+ return elements
301
+
265
302
  def _process_paragraph_with_page_breaks(self, paragraph: "Paragraph") -> str:
266
303
  """
267
304
  Processes a paragraph with page breaks.
@@ -303,10 +340,12 @@ class DOCXToDocument:
303
340
  if isinstance(content, Run):
304
341
  text += content.text
305
342
  elif isinstance(content, Hyperlink):
343
+ # Use `.url` instead of `.address` so a URL fragment stored
344
+ # separately by Word (in `w:anchor`) is not dropped.
306
345
  if self.link_format == DOCXLinkFormat.MARKDOWN:
307
- formatted_link = f"[{content.text}]({content.address})"
346
+ formatted_link = f"[{content.text}]({content.url})"
308
347
  else: # PLAIN format
309
- formatted_link = f"{content.text} ({content.address})"
348
+ formatted_link = f"{content.text} ({content.url})"
310
349
  text += formatted_link
311
350
 
312
351
  return text
@@ -36,7 +36,10 @@ class HTMLToDocument:
36
36
  """
37
37
 
38
38
  def __init__(
39
- self, extraction_kwargs: dict[str, Any] | None = None, store_full_path: bool = False, encoding: str = "utf-8"
39
+ self,
40
+ extraction_kwargs: dict[str, Any] | None = None,
41
+ store_full_path: bool = False,
42
+ encoding: str | None = None,
40
43
  ) -> None:
41
44
  """
42
45
  Create an HTMLToDocument component.
@@ -48,8 +51,9 @@ class HTMLToDocument:
48
51
  If True, the full path of the file is stored in the metadata of the document.
49
52
  If False, only the file name is stored.
50
53
  :param encoding:
51
- The default encoding to use when converting HTML files. If the encoding is specified in the metadata of a
52
- source ByteStream, it overrides this value.
54
+ The encoding to use when decoding HTML files. If None (the default), Trafilatura detects the encoding.
55
+ If detection produces incorrect characters, install `trafilatura[all]` for additional encoding detection
56
+ support, or set this parameter if you know the encoding.
53
57
  """
54
58
  trafilatura_import.check()
55
59
 
@@ -124,8 +128,9 @@ class HTMLToDocument:
124
128
  continue
125
129
 
126
130
  try:
127
- encoding = bytestream.meta.get("encoding", self.encoding)
128
- text = extract(bytestream.data.decode(encoding), **merged_extraction_kwargs)
131
+ html_bytes = bytestream.data
132
+ html_content = html_bytes.decode(self.encoding) if self.encoding is not None else html_bytes
133
+ text = extract(html_content, **merged_extraction_kwargs)
129
134
  except Exception as conversion_e:
130
135
  logger.warning(
131
136
  "Failed to extract text from {source}. Skipping it. Error: {error}",
@@ -134,6 +139,10 @@ class HTMLToDocument:
134
139
  )
135
140
  continue
136
141
 
142
+ if text is None:
143
+ logger.warning("No text could be extracted from {source}. Skipping it.", source=source)
144
+ continue
145
+
137
146
  merged_metadata = {**bytestream.meta, **metadata}
138
147
 
139
148
  if not self.store_full_path and "file_path" in bytestream.meta:
@@ -7,9 +7,7 @@ import mimetypes
7
7
  from collections import defaultdict
8
8
  from io import BytesIO
9
9
  from pathlib import Path
10
- from typing import TypedDict, Union
11
-
12
- from typing_extensions import NotRequired
10
+ from typing import NotRequired, TypedDict, Union
13
11
 
14
12
  from haystack import logging
15
13
  from haystack.dataclasses import ByteStream, Document
@@ -162,54 +160,55 @@ def _convert_pdf_to_images(
162
160
 
163
161
  resolved_page_range = page_range or range(1, num_pages + 1)
164
162
 
165
- for page_number in resolved_page_range:
166
- if page_number < 1 or page_number > num_pages:
167
- logger.warning(
168
- "Page {page_number} is out of range for the PDF file {file_path}. Skipping it.",
169
- page_number=page_number,
170
- file_path=bytestream.meta.get("file_path"),
171
- )
172
- continue
173
-
174
- # Get dimensions of the page
175
- page = pdf[max(page_number - 1, 0)] # Adjust for 0-based indexing
176
- _, _, width, height = page.get_mediabox()
163
+ try:
164
+ for page_number in resolved_page_range:
165
+ if page_number < 1 or page_number > num_pages:
166
+ logger.warning(
167
+ "Page {page_number} is out of range for the PDF file {file_path}. Skipping it.",
168
+ page_number=page_number,
169
+ file_path=bytestream.meta.get("file_path"),
170
+ )
171
+ continue
177
172
 
178
- target_resolution_dpi = 300.0
173
+ # Get dimensions of the page
174
+ page = pdf[max(page_number - 1, 0)] # Adjust for 0-based indexing
175
+ _, _, width, height = page.get_mediabox()
179
176
 
180
- # From pypdfium2 docs: scale (float) – A factor scaling the number of pixels per PDF canvas unit. This defines
181
- # the resolution of the image. To convert a DPI value to a scale factor, multiply it by the size of 1 canvas
182
- # unit in inches (usually 1/72in).
183
- # https://pypdfium2.readthedocs.io/en/stable/python_api.html#pypdfium2._helpers.page.PdfPage.render
184
- target_scale = target_resolution_dpi / 72.0
177
+ target_resolution_dpi = 300.0
185
178
 
186
- # Calculate potential pixels for target_dpi
187
- pixels_for_target_scale = width * height * target_scale**2
179
+ # From pypdfium2 docs: scale (float) – A factor scaling the number of pixels per PDF canvas unit. This
180
+ # defines the resolution of the image. To convert a DPI value to a scale factor, multiply it by the size of
181
+ # 1 canvas unit in inches (usually 1/72in).
182
+ # https://pypdfium2.readthedocs.io/en/stable/python_api.html#pypdfium2._helpers.page.PdfPage.render
183
+ target_scale = target_resolution_dpi / 72.0
188
184
 
189
- pil_max_pixels = PILImage.MAX_IMAGE_PIXELS or int(1024 * 1024 * 1024 // 4 // 3)
190
- # 90% of PIL's default limit to prevent borderline cases
191
- pixel_limit = pil_max_pixels * 0.9
185
+ # Calculate potential pixels for target_dpi
186
+ pixels_for_target_scale = width * height * target_scale**2
192
187
 
193
- scale = target_scale
194
- if pixels_for_target_scale > pixel_limit:
195
- logger.info(
196
- "Large PDF detected ({pixels:.2f} pixels). Resizing the image to fit the pixel limit.",
197
- pixels=pixels_for_target_scale,
198
- )
199
- scale = (pixel_limit / (width * height)) ** 0.5
188
+ pil_max_pixels = PILImage.MAX_IMAGE_PIXELS or int(1024 * 1024 * 1024 // 4 // 3)
189
+ # 90% of PIL's default limit to prevent borderline cases
190
+ pixel_limit = pil_max_pixels * 0.9
200
191
 
201
- pdf_bitmap = page.render(scale=scale)
192
+ scale = target_scale
193
+ if pixels_for_target_scale > pixel_limit:
194
+ logger.info(
195
+ "Large PDF detected ({pixels:.2f} pixels). Resizing the image to fit the pixel limit.",
196
+ pixels=pixels_for_target_scale,
197
+ )
198
+ scale = (pixel_limit / (width * height)) ** 0.5
202
199
 
203
- image: "Image" = pdf_bitmap.to_pil()
204
- pdf_bitmap.close()
205
- if size is not None:
206
- # Set reducing_gap=None to disable multi-step shrink; better quality.
207
- # https://pillow.readthedocs.io/en/latest/reference/Image.html#PIL.Image.Image.thumbnail
208
- image.thumbnail(size=size, reducing_gap=None)
200
+ pdf_bitmap = page.render(scale=scale)
209
201
 
210
- all_pdf_images.append((page_number, image))
202
+ image: "Image" = pdf_bitmap.to_pil()
203
+ pdf_bitmap.close()
204
+ if size is not None:
205
+ # Set reducing_gap=None to disable multi-step shrink; better quality.
206
+ # https://pillow.readthedocs.io/en/latest/reference/Image.html#PIL.Image.Image.thumbnail
207
+ image.thumbnail(size=size, reducing_gap=None)
211
208
 
212
- pdf.close()
209
+ all_pdf_images.append((page_number, image))
210
+ finally:
211
+ pdf.close()
213
212
 
214
213
  if return_base64:
215
214
  return [