haystack-ai 3.2.0rc1__tar.gz → 3.3.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 (298) hide show
  1. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/PKG-INFO +2 -11
  2. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/README.md +0 -10
  3. haystack_ai-3.3.0/VERSION.txt +1 -0
  4. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/agents/agent.py +8 -6
  5. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/agents/tool_calling.py +4 -4
  6. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/builders/chat_prompt_builder.py +15 -11
  7. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/docx.py +39 -5
  8. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/image/document_to_image.py +2 -2
  9. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/image/image_utils.py +12 -5
  10. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/json.py +3 -0
  11. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/xlsx.py +9 -0
  12. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/mock_document_embedder.py +3 -3
  13. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/mock_text_embedder.py +3 -3
  14. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/extractors/llm_metadata_extractor.py +64 -52
  15. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/fetchers/link_content.py +1 -1
  16. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/joiners/branch.py +4 -2
  17. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/sentence_tokenizer.py +21 -2
  18. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/text_cleaner.py +4 -0
  19. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/rankers/meta_field.py +8 -6
  20. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/rankers/meta_field_grouping_ranker.py +4 -2
  21. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/in_memory/bm25_retriever.py +13 -7
  22. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/in_memory/embedding_retriever.py +13 -7
  23. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/multi_retriever.py +41 -14
  24. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/sentence_window_retriever.py +84 -53
  25. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/routers/conditional_router.py +18 -15
  26. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/base.py +9 -0
  27. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/pipeline.py +12 -0
  28. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/type_utils.py +6 -1
  29. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/chat_message.py +16 -1
  30. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/in_memory/document_store.py +46 -11
  31. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/hooks.py +12 -12
  32. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/strategies.py +4 -17
  33. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/pyproject.toml +2 -2
  34. haystack_ai-3.2.0rc1/VERSION.txt +0 -1
  35. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/.gitignore +0 -0
  36. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/LICENSE +0 -0
  37. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/__init__.py +0 -0
  38. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/__init__.py +0 -0
  39. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/agents/__init__.py +0 -0
  40. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/agents/state/__init__.py +0 -0
  41. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/agents/state/state.py +0 -0
  42. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/agents/state/state_utils.py +0 -0
  43. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/agents/utils.py +0 -0
  44. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/builders/__init__.py +0 -0
  45. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/builders/answer_builder.py +0 -0
  46. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/builders/prompt_builder.py +0 -0
  47. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/caching/__init__.py +0 -0
  48. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/caching/cache_checker.py +0 -0
  49. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/__init__.py +0 -0
  50. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/csv.py +0 -0
  51. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/file_to_file_content.py +0 -0
  52. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/html.py +0 -0
  53. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/image/__init__.py +0 -0
  54. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/image/file_to_document.py +0 -0
  55. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/image/file_to_image.py +0 -0
  56. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/image/pdf_to_image.py +0 -0
  57. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/markdown.py +0 -0
  58. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/msg.py +0 -0
  59. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/multi_file_converter.py +0 -0
  60. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/output_adapter.py +0 -0
  61. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/pdfminer.py +0 -0
  62. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/pptx.py +0 -0
  63. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/pypdf.py +0 -0
  64. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/txt.py +0 -0
  65. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/converters/utils.py +0 -0
  66. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/__init__.py +0 -0
  67. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/azure_document_embedder.py +0 -0
  68. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/azure_text_embedder.py +0 -0
  69. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/mock_utils.py +0 -0
  70. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/openai_document_embedder.py +0 -0
  71. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/openai_text_embedder.py +0 -0
  72. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/types/__init__.py +0 -0
  73. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/embedders/types/protocol.py +0 -0
  74. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/__init__.py +0 -0
  75. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/answer_exact_match.py +0 -0
  76. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/context_relevance.py +0 -0
  77. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/document_map.py +0 -0
  78. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/document_mrr.py +0 -0
  79. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/document_ndcg.py +0 -0
  80. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/document_recall.py +0 -0
  81. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/faithfulness.py +0 -0
  82. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/llm_evaluator.py +0 -0
  83. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/evaluators/sas_evaluator.py +0 -0
  84. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/extractors/__init__.py +0 -0
  85. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/extractors/image/__init__.py +0 -0
  86. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/extractors/image/llm_document_content_extractor.py +0 -0
  87. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/extractors/regex_text_extractor.py +0 -0
  88. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/fetchers/__init__.py +0 -0
  89. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/__init__.py +0 -0
  90. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/__init__.py +0 -0
  91. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/azure.py +0 -0
  92. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/azure_responses.py +0 -0
  93. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/fallback.py +0 -0
  94. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/llm.py +0 -0
  95. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/mock.py +0 -0
  96. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/openai.py +0 -0
  97. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/openai_responses.py +0 -0
  98. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/types/__init__.py +0 -0
  99. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/chat/types/protocol.py +0 -0
  100. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/openai_image_generator.py +0 -0
  101. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/generators/utils.py +0 -0
  102. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/joiners/__init__.py +0 -0
  103. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/joiners/answer_joiner.py +0 -0
  104. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/joiners/document_joiner.py +0 -0
  105. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/joiners/list_joiner.py +0 -0
  106. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/joiners/string_joiner.py +0 -0
  107. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/__init__.py +0 -0
  108. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/csv_document_cleaner.py +0 -0
  109. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/csv_document_splitter.py +0 -0
  110. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/document_cleaner.py +0 -0
  111. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/document_preprocessor.py +0 -0
  112. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/document_splitter.py +0 -0
  113. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/embedding_based_document_splitter.py +0 -0
  114. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/hierarchical_document_splitter.py +0 -0
  115. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/markdown_header_splitter.py +0 -0
  116. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/python_code_splitter.py +0 -0
  117. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/preprocessors/recursive_splitter.py +0 -0
  118. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/query/__init__.py +0 -0
  119. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/query/query_expander.py +0 -0
  120. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/rankers/__init__.py +0 -0
  121. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/rankers/llm_ranker.py +0 -0
  122. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/rankers/lost_in_the_middle.py +0 -0
  123. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/__init__.py +0 -0
  124. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/auto_merging_retriever.py +0 -0
  125. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/filter_retriever.py +0 -0
  126. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/in_memory/__init__.py +0 -0
  127. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/multi_query_embedding_retriever.py +0 -0
  128. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/multi_query_text_retriever.py +0 -0
  129. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/text_embedding_retriever.py +0 -0
  130. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/types/__init__.py +0 -0
  131. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/retrievers/types/protocol.py +0 -0
  132. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/routers/__init__.py +0 -0
  133. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/routers/document_length_router.py +0 -0
  134. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/routers/document_type_router.py +0 -0
  135. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/routers/file_type_router.py +0 -0
  136. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/routers/llm_messages_router.py +0 -0
  137. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/routers/metadata_router.py +0 -0
  138. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/samplers/__init__.py +0 -0
  139. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/samplers/top_p.py +0 -0
  140. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/validators/__init__.py +0 -0
  141. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/validators/json_schema.py +0 -0
  142. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/writers/__init__.py +0 -0
  143. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/components/writers/document_writer.py +0 -0
  144. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/__init__.py +0 -0
  145. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/component/__init__.py +0 -0
  146. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/component/component.py +0 -0
  147. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/component/sockets.py +0 -0
  148. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/component/types.py +0 -0
  149. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/errors.py +0 -0
  150. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/__init__.py +0 -0
  151. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/breakpoint.py +0 -0
  152. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/component_checks.py +0 -0
  153. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/descriptions.py +0 -0
  154. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/draw.py +0 -0
  155. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/pipeline/utils.py +0 -0
  156. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/serialization.py +0 -0
  157. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/serialization_security.py +0 -0
  158. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/super_component/__init__.py +0 -0
  159. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/super_component/super_component.py +0 -0
  160. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/core/super_component/utils.py +0 -0
  161. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/data/abbreviations/de.txt +0 -0
  162. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/data/abbreviations/en.txt +0 -0
  163. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/__init__.py +0 -0
  164. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/answer.py +0 -0
  165. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/breakpoints.py +0 -0
  166. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/byte_stream.py +0 -0
  167. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/document.py +0 -0
  168. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/file_content.py +0 -0
  169. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/image_content.py +0 -0
  170. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/skill_info.py +0 -0
  171. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/sparse_embedding.py +0 -0
  172. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/dataclasses/streaming_chunk.py +0 -0
  173. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/__init__.py +0 -0
  174. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/errors/__init__.py +0 -0
  175. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/errors/errors.py +0 -0
  176. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/in_memory/__init__.py +0 -0
  177. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/types/__init__.py +0 -0
  178. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/types/filter_policy.py +0 -0
  179. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/types/policy.py +0 -0
  180. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/document_stores/types/protocol.py +0 -0
  181. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/errors.py +0 -0
  182. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/evaluation/__init__.py +0 -0
  183. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/evaluation/eval_run_result.py +0 -0
  184. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/__init__.py +0 -0
  185. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/budget/__init__.py +0 -0
  186. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/budget/hooks.py +0 -0
  187. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/AGENTS.md +0 -0
  188. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/CLAUDE.md +0 -0
  189. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/__init__.py +0 -0
  190. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/sliding_window.py +0 -0
  191. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/summarization.py +0 -0
  192. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/tool_result_pruning.py +0 -0
  193. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/types/__init__.py +0 -0
  194. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/types/protocol.py +0 -0
  195. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/compaction/utils.py +0 -0
  196. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/from_function.py +0 -0
  197. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/__init__.py +0 -0
  198. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/dataclasses.py +0 -0
  199. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/hooks.py +0 -0
  200. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/policies.py +0 -0
  201. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/types/__init__.py +0 -0
  202. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/types/protocol.py +0 -0
  203. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/human_in_the_loop/user_interfaces.py +0 -0
  204. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/invocation.py +0 -0
  205. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/protocol.py +0 -0
  206. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/tool_result_offloading/__init__.py +0 -0
  207. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/tool_result_offloading/hooks.py +0 -0
  208. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/tool_result_offloading/policies.py +0 -0
  209. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/tool_result_offloading/stores.py +0 -0
  210. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/tool_result_offloading/types/__init__.py +0 -0
  211. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/tool_result_offloading/types/protocol.py +0 -0
  212. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/hooks/utils.py +0 -0
  213. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/lazy_imports.py +0 -0
  214. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/logging.py +0 -0
  215. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/marshal/__init__.py +0 -0
  216. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/marshal/protocol.py +0 -0
  217. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/marshal/yaml.py +0 -0
  218. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/py.typed +0 -0
  219. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/skill_stores/__init__.py +0 -0
  220. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/skill_stores/file_system/__init__.py +0 -0
  221. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/skill_stores/file_system/skill_store.py +0 -0
  222. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/skill_stores/types/__init__.py +0 -0
  223. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/skill_stores/types/protocol.py +0 -0
  224. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/telemetry/__init__.py +0 -0
  225. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/telemetry/_environment.py +0 -0
  226. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/telemetry/_telemetry.py +0 -0
  227. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/__init__.py +0 -0
  228. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/callable_serialization/random_callable.py +0 -0
  229. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/document_store.py +0 -0
  230. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/document_store_async.py +0 -0
  231. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/factory.py +0 -0
  232. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/__init__.py +0 -0
  233. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/accumulate.py +0 -0
  234. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/add_value.py +0 -0
  235. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/concatenate.py +0 -0
  236. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/double.py +0 -0
  237. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/fstring.py +0 -0
  238. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/future_annotations.py +0 -0
  239. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/greet.py +0 -0
  240. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/hello.py +0 -0
  241. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/joiner.py +0 -0
  242. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/parity.py +0 -0
  243. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/remainder.py +0 -0
  244. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/repeat.py +0 -0
  245. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/subtract.py +0 -0
  246. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/sum.py +0 -0
  247. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/text_splitter.py +0 -0
  248. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/sample_components/threshold.py +0 -0
  249. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/telemetry.py +0 -0
  250. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/testing/test_utils.py +0 -0
  251. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/token_counters/__init__.py +0 -0
  252. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/token_counters/approximate_counter.py +0 -0
  253. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/token_counters/openai_counter.py +0 -0
  254. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/token_counters/tiktoken_counter.py +0 -0
  255. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/token_counters/types/__init__.py +0 -0
  256. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/token_counters/types/protocol.py +0 -0
  257. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/token_counters/utils.py +0 -0
  258. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/__init__.py +0 -0
  259. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/agent_tool.py +0 -0
  260. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/component_tool.py +0 -0
  261. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/errors.py +0 -0
  262. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/from_function.py +0 -0
  263. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/parameters_schema_utils.py +0 -0
  264. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/pipeline_tool.py +0 -0
  265. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/searchable_toolset.py +0 -0
  266. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/serde_utils.py +0 -0
  267. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/skills/__init__.py +0 -0
  268. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/skills/skill_toolset.py +0 -0
  269. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/tool.py +0 -0
  270. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/tool_types.py +0 -0
  271. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/toolset.py +0 -0
  272. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tools/utils.py +0 -0
  273. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tracing/__init__.py +0 -0
  274. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tracing/logging_tracer.py +0 -0
  275. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tracing/tracer.py +0 -0
  276. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/tracing/utils.py +0 -0
  277. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/__init__.py +0 -0
  278. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/async_utils.py +0 -0
  279. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/auth.py +0 -0
  280. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/azure.py +0 -0
  281. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/base_serialization.py +0 -0
  282. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/callable_serialization.py +0 -0
  283. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/dataclasses.py +0 -0
  284. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/deserialization.py +0 -0
  285. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/device.py +0 -0
  286. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/experimental.py +0 -0
  287. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/filters.py +0 -0
  288. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/hf.py +0 -0
  289. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/http_client.py +0 -0
  290. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/jinja2_chat_extension.py +0 -0
  291. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/jinja2_extensions.py +0 -0
  292. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/jinja2_sandbox.py +0 -0
  293. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/jupyter.py +0 -0
  294. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/misc.py +0 -0
  295. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/requests_utils.py +0 -0
  296. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/type_serialization.py +0 -0
  297. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/utils/url_validation.py +0 -0
  298. {haystack_ai-3.2.0rc1 → haystack_ai-3.3.0}/haystack/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: haystack-ai
3
- Version: 3.2.0-rc1
3
+ Version: 3.3.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
@@ -25,6 +25,7 @@ Classifier: Programming Language :: Python :: 3.13
25
25
  Classifier: Programming Language :: Python :: 3.14
26
26
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
27
  Requires-Python: >=3.10
28
+ Requires-Dist: anyio>=4.14.2
28
29
  Requires-Dist: docstring-parser
29
30
  Requires-Dist: filetype
30
31
  Requires-Dist: httpx
@@ -56,16 +57,6 @@ Description-Content-Type: text/markdown
56
57
  | Meta | [![Discord](https://img.shields.io/discord/993534733298450452?logo=discord)](https://discord.com/invite/qZxjM4bAHU) [![Twitter Follow](https://img.shields.io/twitter/follow/haystack_ai)](https://twitter.com/haystack_ai) |
57
58
  </div>
58
59
 
59
- <div align="center">
60
-
61
- # 🎉🎊✨ &nbsp; Haystack 3.0 is out! &nbsp; ✨🎊🎉
62
-
63
- ### Read the announcement [here](https://haystack.deepset.ai/blog/haystack-3-release)!
64
-
65
- ## 🥳 🎈 🎆 🪅 🎇 🍾 🥂 🎁 🌈
66
-
67
- </div>
68
-
69
60
  [Haystack](https://haystack.deepset.ai/) is an open-source AI orchestration framework for building production-ready LLM applications in Python.
70
61
 
71
62
  Design modular pipelines and agent workflows with explicit control over retrieval, routing, memory, and generation. Build scalable RAG systems, multimodal applications, semantic search, question answering, and autonomous agents, all in a transparent architecture that lets you experiment, customize deeply, and deploy with confidence.
@@ -9,16 +9,6 @@
9
9
  | Meta | [![Discord](https://img.shields.io/discord/993534733298450452?logo=discord)](https://discord.com/invite/qZxjM4bAHU) [![Twitter Follow](https://img.shields.io/twitter/follow/haystack_ai)](https://twitter.com/haystack_ai) |
10
10
  </div>
11
11
 
12
- <div align="center">
13
-
14
- # 🎉🎊✨ &nbsp; Haystack 3.0 is out! &nbsp; ✨🎊🎉
15
-
16
- ### Read the announcement [here](https://haystack.deepset.ai/blog/haystack-3-release)!
17
-
18
- ## 🥳 🎈 🎆 🪅 🎇 🍾 🥂 🎁 🌈
19
-
20
- </div>
21
-
22
12
  [Haystack](https://haystack.deepset.ai/) is an open-source AI orchestration framework for building production-ready LLM applications in Python.
23
13
 
24
14
  Design modular pipelines and agent workflows with explicit control over retrieval, routing, memory, and generation. Build scalable RAG systems, multimodal applications, semantic search, question answering, and autonomous agents, all in a transparent architecture that lets you experiment, customize deeply, and deploy with confidence.
@@ -0,0 +1 @@
1
+ 3.3.0
@@ -866,9 +866,10 @@ class Agent:
866
866
  - "exit_reason": Why the Agent stopped, useful for routing the output downstream (e.g. with a
867
867
  `ConditionalRouter`). One of: `"text"` (the model returned a complete reply with no tool calls),
868
868
  `"length"` or `"content_filter"` (the model returned an incomplete reply, which may contain partial
869
- text), the name of the tool that satisfied a tool exit condition (in which case `last_message` is that
870
- tool's result), or `"max_agent_steps"` (the Agent hit `max_agent_steps` before meeting an exit
871
- condition), or a custom reason a hook supplied through the `stop_run` state key.
869
+ text), the name of the tool that satisfied a tool exit condition (its result is the last tool message in
870
+ `messages` whose `tool_call_result.origin.tool_name` matches it, which may not be `last_message` when
871
+ the model called several tools at once), or `"max_agent_steps"` (the Agent hit `max_agent_steps` before
872
+ meeting an exit condition), or a custom reason a hook supplied through the `stop_run` state key.
872
873
  - Any additional keys defined in the `state_schema`.
873
874
  """
874
875
  agent_inputs = {"messages": messages, "streaming_callback": streaming_callback, **kwargs}
@@ -952,9 +953,10 @@ class Agent:
952
953
  - "exit_reason": Why the Agent stopped, useful for routing the output downstream (e.g. with a
953
954
  `ConditionalRouter`). One of: `"text"` (the model returned a complete reply with no tool calls),
954
955
  `"length"` or `"content_filter"` (the model returned an incomplete reply, which may contain partial
955
- text), the name of the tool that satisfied a tool exit condition (in which case `last_message` is that
956
- tool's result), or `"max_agent_steps"` (the Agent hit `max_agent_steps` before meeting an exit
957
- condition), or a custom reason a hook supplied through the `stop_run` state key.
956
+ text), the name of the tool that satisfied a tool exit condition (its result is the last tool message in
957
+ `messages` whose `tool_call_result.origin.tool_name` matches it, which may not be `last_message` when
958
+ the model called several tools at once), or `"max_agent_steps"` (the Agent hit `max_agent_steps` before
959
+ meeting an exit condition), or a custom reason a hook supplied through the `stop_run` state key.
958
960
  - Any additional keys defined in the `state_schema`.
959
961
  """
960
962
  agent_inputs = {"messages": messages, "streaming_callback": streaming_callback, **kwargs}
@@ -449,8 +449,9 @@ def _schedule_tool_calls(tool_calls: list[ToolCall], tools: list[Tool]) -> list[
449
449
  once — cannot be ordered by the read-after-write rule alone, so they are broken deterministically by call order
450
450
  (the lowest-index remaining call runs next, on its own).
451
451
 
452
- Pure write-write overlaps create no dependency: nobody reads the contended key, and outputs are merged into State
453
- sequentially in call order afterward, so the result stays deterministic without serializing execution.
452
+ Write-write overlaps alone create no dependency. Outputs are merged into State after each batch, in the
453
+ LLM's requested order within that batch, so subsequent batches see the updated values. This keeps merging
454
+ deterministic, but does not guarantee that writes follow the LLM's requested order across batches.
454
455
 
455
456
  :param tool_calls: The tool calls to schedule, in call order.
456
457
  :param tools: The resolved Tool for each entry in `tool_calls` (parallel list).
@@ -498,8 +499,7 @@ def _finalize_tool_result(
498
499
  Turn a single tool invocation result into a tool-result ChatMessage, merging outputs into State.
499
500
 
500
501
  On a `ToolInvocationError`, either re-raise (when `raise_on_failure`) or return an error message. Otherwise
501
- merge the tool's outputs into State (in call order, so write-write merges stay deterministic) and build the
502
- result message.
502
+ merge the tool's outputs into State and build the result message.
503
503
  """
504
504
  if isinstance(result, ToolInvocationError):
505
505
  if raise_on_failure:
@@ -176,12 +176,13 @@ class ChatPromptBuilder:
176
176
  # infer variables from template
177
177
  if message.text is None:
178
178
  raise ValueError(NO_TEXT_ERROR_MESSAGE.format(role=message.role.value, message=message))
179
- if message.text and "templatize_part" in message.text:
180
- raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
181
- assigned_variables, template_variables = _extract_template_variables_and_assignments(
182
- env=self._env, template=message.text
183
- )
184
- extracted_variables += list(template_variables - assigned_variables)
179
+ for text in message.texts:
180
+ if "templatize_part" in text:
181
+ raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
182
+ assigned_variables, template_variables = _extract_template_variables_and_assignments(
183
+ env=self._env, template=text
184
+ )
185
+ extracted_variables += list(template_variables - assigned_variables)
185
186
  elif isinstance(template, str):
186
187
  assigned_variables, template_variables = _extract_template_variables_and_assignments(
187
188
  env=self._env, template=template
@@ -263,12 +264,15 @@ class ChatPromptBuilder:
263
264
  self._validate_variables(set(template_variables_combined.keys()))
264
265
  if message.text is None:
265
266
  raise ValueError(NO_TEXT_ERROR_MESSAGE.format(role=message.role.value, message=message))
266
- if message.text and "templatize_part" in message.text:
267
- raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
268
- compiled_template = self._env.from_string(message.text)
269
- rendered_text = compiled_template.render(template_variables_combined)
267
+ rendered_content = list(message._content)
268
+ for index, part in enumerate(rendered_content):
269
+ if isinstance(part, TextContent):
270
+ if "templatize_part" in part.text:
271
+ raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
272
+ rendered_text = self._env.from_string(part.text).render(template_variables_combined)
273
+ rendered_content[index] = TextContent(text=rendered_text)
270
274
  # use dataclasses.replace to avoid in-place mutation of the original message
271
- rendered_message: ChatMessage = replace(message, _content=[TextContent(text=rendered_text)])
275
+ rendered_message: ChatMessage = replace(message, _content=rendered_content)
272
276
  processed_messages.append(rendered_message)
273
277
  else:
274
278
  processed_messages.append(message)
@@ -5,6 +5,7 @@
5
5
  import csv
6
6
  import io
7
7
  import os
8
+ import re
8
9
  from dataclasses import asdict, dataclass
9
10
  from enum import Enum
10
11
  from io import StringIO
@@ -18,10 +19,15 @@ from haystack.lazy_imports import LazyImport
18
19
 
19
20
  logger = logging.getLogger(__name__)
20
21
 
22
+ # A Markdown table row ends at a line break and its columns are separated by pipes, so
23
+ # neither can survive inside a cell.
24
+ _MARKDOWN_CELL_BREAK_PATTERN = re.compile(r"\s*(?:\r\n|\r|\n)\s*")
25
+ _MARKDOWN_CELL_PIPE_PATTERN = re.compile(r"(?<!\\)(\\*)\|")
26
+
21
27
  with LazyImport("Run 'pip install python-docx'") as docx_import:
22
28
  import docx
23
29
  from docx.document import Document as DocxDocument
24
- from docx.table import Table
30
+ from docx.table import Table, _Cell
25
31
  from docx.text.hyperlink import Hyperlink
26
32
  from docx.text.paragraph import Paragraph
27
33
  from docx.text.run import Run
@@ -305,6 +311,31 @@ class DOCXToDocument:
305
311
 
306
312
  return text
307
313
 
314
+ @staticmethod
315
+ def _escape_markdown_cell(text: str) -> str:
316
+ """
317
+ Makes a cell's text safe to put between the pipes of a Markdown table row.
318
+
319
+ A cell spanning several paragraphs arrives with newlines in it, which would end
320
+ the row in the middle, and a pipe in a cell would be read as a column separator.
321
+
322
+ :param text: The cell text.
323
+ :returns: The text with line breaks collapsed and pipes escaped.
324
+ """
325
+ text = _MARKDOWN_CELL_BREAK_PATTERN.sub(" ", text)
326
+ # The backslash run in front of the pipe is doubled first, so a backslash the
327
+ # cell already contains cannot consume the escape.
328
+ return _MARKDOWN_CELL_PIPE_PATTERN.sub(lambda match: match.group(1) * 2 + r"\|", text)
329
+
330
+ def _cell_text(self, cell: "_Cell") -> str:
331
+ """
332
+ Returns a table cell's text with links formatted like links in body paragraphs.
333
+
334
+ :param cell: The DOCX table cell.
335
+ :returns: The cell's paragraphs joined by newlines, as `cell.text` joins them.
336
+ """
337
+ return "\n".join(self._process_links_in_paragraph(paragraph) for paragraph in cell.paragraphs)
338
+
308
339
  def _table_to_markdown(self, table: "Table") -> str:
309
340
  """
310
341
  Converts a DOCX table to a Markdown string.
@@ -315,10 +346,10 @@ class DOCXToDocument:
315
346
  markdown: list[str] = []
316
347
  max_col_widths: list[int] = []
317
348
 
318
- # Calculate max width for each column
349
+ # Calculate max width for each column, on the escaped text that is written out
319
350
  for row in table.rows:
320
351
  for i, cell in enumerate(row.cells):
321
- cell_text = cell.text.strip()
352
+ cell_text = self._escape_markdown_cell(self._cell_text(cell).strip())
322
353
  if i >= len(max_col_widths):
323
354
  max_col_widths.append(len(cell_text))
324
355
  else:
@@ -326,7 +357,10 @@ class DOCXToDocument:
326
357
 
327
358
  # Process rows
328
359
  for i, row in enumerate(table.rows):
329
- md_row = [cell.text.strip().ljust(max_col_widths[j]) for j, cell in enumerate(row.cells)]
360
+ md_row = [
361
+ self._escape_markdown_cell(self._cell_text(cell).strip()).ljust(max_col_widths[j])
362
+ for j, cell in enumerate(row.cells)
363
+ ]
330
364
  markdown.append("| " + " | ".join(md_row) + " |")
331
365
 
332
366
  # Add separator after header row
@@ -348,7 +382,7 @@ class DOCXToDocument:
348
382
 
349
383
  # Process rows
350
384
  for row in table.rows:
351
- csv_row = [cell.text.strip() for cell in row.cells]
385
+ csv_row = [self._cell_text(cell).strip() for cell in row.cells]
352
386
  csv_writer.writerow(csv_row)
353
387
 
354
388
  # Get the CSV as a string and strip any trailing newlines
@@ -114,8 +114,8 @@ class DocumentToImageContent:
114
114
  Dictionary containing one key:
115
115
  - "image_contents": ImageContents created from the processed documents. These contain base64-encoded image
116
116
  data and metadata. The order corresponds to the order of the input documents. A document that is
117
- missing the required metadata keys, has an invalid file path, or has an unsupported MIME type gets
118
- None in its position and a logged warning with the reason.
117
+ missing the required metadata keys, has an invalid file path, has an unsupported MIME type, or points
118
+ to a PDF page that cannot be converted gets None in its position and a logged warning with the reason.
119
119
  """
120
120
  if not documents:
121
121
  return {"image_contents": []}
@@ -164,7 +164,11 @@ def _convert_pdf_to_images(
164
164
 
165
165
  for page_number in resolved_page_range:
166
166
  if page_number < 1 or page_number > num_pages:
167
- logger.warning("Page {page_number} is out of range for the PDF file. Skipping it.", page_number=page_number)
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
+ )
168
172
  continue
169
173
 
170
174
  # Get dimensions of the page
@@ -312,7 +316,8 @@ def _batch_convert_pdf_pages_to_images(
312
316
  :param size: Optional tuple of width and height to resize the images to.
313
317
  :param return_base64: If True, return base64 encoded images instead of PIL images.
314
318
 
315
- :returns: Dictionary mapping document indices to images (PIL.Image or base64 string).
319
+ :returns: Dictionary mapping document indices to images (PIL.Image or base64 string). Documents whose page could
320
+ not be converted are not included.
316
321
  """
317
322
  if not pdf_page_infos:
318
323
  return {}
@@ -325,16 +330,18 @@ def _batch_convert_pdf_pages_to_images(
325
330
 
326
331
  for pdf_path, page_infos_for_pdf in page_infos_by_pdf_path.items():
327
332
  page_numbers_to_convert = [info["page_number"] for info in page_infos_for_pdf]
328
- bytestream = ByteStream.from_file_path(pdf_path)
333
+ bytestream = ByteStream.from_file_path(pdf_path, meta={"file_path": str(pdf_path)})
329
334
 
330
335
  converted_pages = _convert_pdf_to_images(
331
336
  bytestream=bytestream, return_base64=return_base64, page_range=page_numbers_to_convert, size=size
332
337
  )
333
338
 
334
- # Map results back to document indices
339
+ # Map results back to document indices. Pages that could not be converted (out of range, or the PDF
340
+ # could not be read) are missing from the results, so their documents are left out.
335
341
  page_number_to_image = dict(converted_pages)
336
342
  for page_info in page_infos_for_pdf:
337
- converted_images_by_doc_index[page_info["doc_idx"]] = page_number_to_image[page_info["page_number"]]
343
+ if page_info["page_number"] in page_number_to_image:
344
+ converted_images_by_doc_index[page_info["doc_idx"]] = page_number_to_image[page_info["page_number"]]
338
345
 
339
346
  # mypy is not able to infer that we match the declared return type
340
347
  return converted_images_by_doc_index # type: ignore[return-value]
@@ -238,6 +238,9 @@ class JSONConverter:
238
238
  logger.warning("Expected a scalar value but got {obj}. Skipping it.", obj=obj)
239
239
  continue
240
240
 
241
+ if text is not None and not isinstance(text, str):
242
+ text = str(text)
243
+
241
244
  meta = {}
242
245
  if meta_fields == "*":
243
246
  meta = {k: v for k, v in obj.items() if k != self._content_key}
@@ -234,6 +234,15 @@ class XLSXToDocument:
234
234
  "missingval": "",
235
235
  **self.table_format_kwargs,
236
236
  }
237
+ if resolved_kwargs["tablefmt"] == "pipe":
238
+ value = value.replace(
239
+ {
240
+ r"\r\n|\r|\n": " ", # keep in-cell line breaks from creating extra Markdown rows
241
+ r"(\\*)\|": r"\1\1\\|", # escape pipes but preserve any preceding literal backslashes
242
+ },
243
+ regex=True,
244
+ )
245
+
237
246
  # to_markdown uses tabulate, whose missingval only covers None: a NaN
238
247
  # reaches the formatter as a number and is written out as "nan". Replace
239
248
  # the empty cells with None so an empty cell reads as empty, the way
@@ -81,13 +81,13 @@ class MockDocumentEmbedder:
81
81
  :param meta_fields_to_embed: List of metadata fields to embed along with the document text.
82
82
  :param embedding_separator: Separator used to concatenate the metadata fields to the document text.
83
83
  :param progress_bar: Accepted for interface compatibility with real Document Embedders and ignored.
84
- :raises ValueError: If both `embedding` and `embedding_fn` are provided, if `dimension` is not positive, or
85
- if `embedding` is an empty list.
84
+ :raises ValueError: If both `embedding` and `embedding_fn` are provided, if `embedding` is an empty list,
85
+ or if neither is provided and `dimension` is not positive.
86
86
  :raises TypeError: If `embedding` is not a sequence of numbers.
87
87
  """
88
88
  if embedding is not None and embedding_fn is not None:
89
89
  raise ValueError("Pass either 'embedding' or 'embedding_fn', not both.")
90
- if dimension <= 0:
90
+ if embedding is None and embedding_fn is None and dimension <= 0:
91
91
  raise ValueError("'dimension' must be a positive integer.")
92
92
 
93
93
  self.embedding = _coerce_embedding(embedding, name="'embedding'") if embedding is not None else None
@@ -70,13 +70,13 @@ class MockTextEmbedder:
70
70
  :param meta: Additional metadata merged into the output `meta`.
71
71
  :param prefix: A string to add at the beginning of the text before embedding.
72
72
  :param suffix: A string to add at the end of the text before embedding.
73
- :raises ValueError: If both `embedding` and `embedding_fn` are provided, if `dimension` is not positive, or
74
- if `embedding` is an empty list.
73
+ :raises ValueError: If both `embedding` and `embedding_fn` are provided, if `embedding` is an empty list,
74
+ or if neither is provided and `dimension` is not positive.
75
75
  :raises TypeError: If `embedding` is not a sequence of numbers.
76
76
  """
77
77
  if embedding is not None and embedding_fn is not None:
78
78
  raise ValueError("Pass either 'embedding' or 'embedding_fn', not both.")
79
- if dimension <= 0:
79
+ if embedding is None and embedding_fn is None and dimension <= 0:
80
80
  raise ValueError("'dimension' must be a positive integer.")
81
81
 
82
82
  self.embedding = _coerce_embedding(embedding, name="'embedding'") if embedding is not None else None
@@ -5,10 +5,8 @@
5
5
  import copy
6
6
  import json
7
7
  from asyncio import Semaphore, gather
8
- from collections.abc import Iterable
9
8
  from concurrent.futures import ThreadPoolExecutor
10
9
  from dataclasses import replace
11
- from functools import partial
12
10
  from typing import Any
13
11
 
14
12
  from jinja2 import meta
@@ -297,10 +295,17 @@ class LLMMetadataExtractor:
297
295
 
298
296
  return all_prompts
299
297
 
300
- def _run_on_thread(self, prompt: ChatMessage | None, parent_span: tracing.Span | None = None) -> dict[str, Any]:
301
- # If prompt is None, return an error dictionary
298
+ @staticmethod
299
+ def _fail(document: Document, error: str, response: ChatMessage | None = None) -> tuple[Document, bool]:
300
+ """Return a copy of ``document`` with the failure metadata set, flagged as failed."""
301
+ new_meta = {**document.meta, "metadata_extraction_error": error, "metadata_extraction_response": response}
302
+ return replace(document, meta=new_meta), False
303
+
304
+ def _run_on_thread(
305
+ self, document: Document, prompt: ChatMessage | None, parent_span: tracing.Span | None = None
306
+ ) -> tuple[Document, bool]:
302
307
  if prompt is None:
303
- return {"error": "Document has no content, skipping LLM call."}
308
+ return self._fail(document, "Document has no content, skipping LLM call.")
304
309
 
305
310
  try:
306
311
  with _trace_chat_generator_run(
@@ -316,13 +321,14 @@ class LLMMetadataExtractor:
316
321
  class_name=self._chat_generator.__class__.__name__,
317
322
  error=e,
318
323
  )
319
- result = {"error": "LLM failed with exception: " + str(e)}
320
- return result
324
+ return self._fail(document, "LLM failed with exception: " + str(e))
325
+ return self._process_reply(document, result["replies"][0])
321
326
 
322
- async def _run_async(self, prompt: ChatMessage | None, parent_span: tracing.Span | None = None) -> dict[str, Any]:
323
- # If prompt is None, return an error dictionary
327
+ async def _run_async(
328
+ self, document: Document, prompt: ChatMessage | None, parent_span: tracing.Span | None = None
329
+ ) -> tuple[Document, bool]:
324
330
  if prompt is None:
325
- return {"error": "Document has no content, skipping LLM call."}
331
+ return self._fail(document, "Document has no content, skipping LLM call.")
326
332
 
327
333
  try:
328
334
  with _trace_chat_generator_run(
@@ -338,45 +344,33 @@ class LLMMetadataExtractor:
338
344
  class_name=self._chat_generator.__class__.__name__,
339
345
  error=e,
340
346
  )
341
- result = {"error": "LLM failed with exception: " + str(e)}
342
- return result
347
+ return self._fail(document, "LLM failed with exception: " + str(e))
348
+ return self._process_reply(document, result["replies"][0])
343
349
 
344
- def _process_results(
345
- self, documents: list[Document], results: Iterable[dict[str, Any]]
346
- ) -> tuple[list[Document], list[Document]]:
347
- successful_documents = []
348
- failed_documents = []
349
- for document, result in zip(documents, results, strict=True):
350
- new_meta = {**document.meta}
351
- if "error" in result:
352
- new_meta["metadata_extraction_error"] = result["error"]
353
- new_meta["metadata_extraction_response"] = None
354
- failed_documents.append(replace(document, meta=new_meta))
355
- continue
350
+ def _process_reply(self, document: Document, reply: ChatMessage) -> tuple[Document, bool]:
351
+ """
352
+ Parse one document's LLM reply into metadata.
356
353
 
357
- reply = result["replies"][0]
358
- try:
359
- parsed_metadata = _parse_dict_from_json(
360
- reply.text, expected_keys=self.expected_keys, raise_on_failure=True
361
- )
362
- except (ValueError, json.JSONDecodeError) as e:
363
- logger.warning(
364
- "Response from the LLM is not valid JSON or missing expected keys. Received output: {response}",
365
- response=reply.text,
366
- )
367
- if self.raise_on_failure:
368
- raise
369
- new_meta["metadata_extraction_error"] = "Response is not valid JSON or missing keys. Error: " + str(e)
370
- new_meta["metadata_extraction_response"] = reply
371
- failed_documents.append(replace(document, meta=new_meta))
372
- continue
354
+ Returns (updated_document, True if success else False).
355
+ """
356
+ try:
357
+ parsed_metadata = _parse_dict_from_json(
358
+ reply.text or "", expected_keys=self.expected_keys, raise_on_failure=True
359
+ )
360
+ except (ValueError, json.JSONDecodeError) as e:
361
+ logger.warning(
362
+ "Response from the LLM is not valid JSON or missing expected keys. Received output: {response}",
363
+ response=reply.text,
364
+ )
365
+ if self.raise_on_failure:
366
+ raise
367
+ return self._fail(document, "Response is not valid JSON or missing keys. Error: " + str(e), response=reply)
373
368
 
374
- new_meta.update(parsed_metadata)
375
- # Remove metadata_extraction_error and metadata_extraction_response if present from previous runs.
376
- new_meta.pop("metadata_extraction_error", None)
377
- new_meta.pop("metadata_extraction_response", None)
378
- successful_documents.append(replace(document, meta=new_meta))
379
- return successful_documents, failed_documents
369
+ new_meta = {**document.meta, **parsed_metadata}
370
+ # Remove metadata_extraction_error and metadata_extraction_response if present from previous runs.
371
+ new_meta.pop("metadata_extraction_error", None)
372
+ new_meta.pop("metadata_extraction_response", None)
373
+ return replace(document, meta=new_meta), True
380
374
 
381
375
  @component.output_types(documents=list[Document], failed_documents=list[Document])
382
376
  def run(self, documents: list[Document], page_range: list[str | int] | None = None) -> dict[str, Any]:
@@ -421,9 +415,19 @@ class LLMMetadataExtractor:
421
415
 
422
416
  # Run the LLM on each prompt
423
417
  with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
424
- results = executor.map(partial(self._run_on_thread, parent_span=parent_span), all_prompts)
418
+ results = executor.map(
419
+ lambda document, prompt: self._run_on_thread(document, prompt, parent_span=parent_span),
420
+ documents,
421
+ all_prompts,
422
+ )
425
423
 
426
- successful_documents, failed_documents = self._process_results(documents, results)
424
+ successful_documents = []
425
+ failed_documents = []
426
+ for doc, success in results:
427
+ if success:
428
+ successful_documents.append(doc)
429
+ else:
430
+ failed_documents.append(doc)
427
431
 
428
432
  return {"documents": successful_documents, "failed_documents": failed_documents}
429
433
 
@@ -474,12 +478,20 @@ class LLMMetadataExtractor:
474
478
  # Run the LLM on each prompt, bounding concurrency per task so max_workers is enforced.
475
479
  sem = Semaphore(max(1, self.max_workers))
476
480
 
477
- async def _bounded_run(prompt: ChatMessage | None) -> dict[str, Any]:
481
+ async def _bounded_run(document: Document, prompt: ChatMessage | None) -> tuple[Document, bool]:
478
482
  async with sem:
479
- return await self._run_async(prompt, parent_span=parent_span)
483
+ return await self._run_async(document, prompt, parent_span=parent_span)
480
484
 
481
- results = await gather(*[_bounded_run(prompt) for prompt in all_prompts])
485
+ results = await gather(
486
+ *[_bounded_run(document, prompt) for document, prompt in zip(documents, all_prompts, strict=True)]
487
+ )
482
488
 
483
- successful_documents, failed_documents = self._process_results(documents, results)
489
+ successful_documents = []
490
+ failed_documents = []
491
+ for doc, success in results:
492
+ if success:
493
+ successful_documents.append(doc)
494
+ else:
495
+ failed_documents.append(doc)
484
496
 
485
497
  return {"documents": successful_documents, "failed_documents": failed_documents}
@@ -143,7 +143,7 @@ class LinkContentFetcher:
143
143
  self.retry_attempts = retry_attempts
144
144
  self.timeout = timeout
145
145
  self.http2 = http2
146
- self.client_kwargs = client_kwargs or {}
146
+ self.client_kwargs = dict(client_kwargs or {})
147
147
  self.request_headers = request_headers or {}
148
148
 
149
149
  # Configure default client settings
@@ -113,8 +113,10 @@ class BranchJoiner:
113
113
  :returns:
114
114
  A deserialized `BranchJoiner` instance.
115
115
  """
116
- data["init_parameters"]["type_"] = deserialize_type(data["init_parameters"]["type_"])
117
- return default_from_dict(cls, data)
116
+ # Copy so the caller's data keeps the serialized type and can be deserialized again
117
+ init_parameters = dict(data["init_parameters"])
118
+ init_parameters["type_"] = deserialize_type(init_parameters["type_"])
119
+ return default_from_dict(cls, {**data, "init_parameters": init_parameters})
118
120
 
119
121
  def run(self, **kwargs: Any) -> dict[str, Any]:
120
122
  """
@@ -42,6 +42,9 @@ ISO639_TO_NLTK = {
42
42
 
43
43
  QUOTE_SPANS_RE = re.compile(r'"[^"]*"|\'[^\']*\'')
44
44
 
45
+ # Closing brackets and quotes that can trail a sentence ending, as in `He said "Hi." Bye.` or `(Hi.) Bye.`
46
+ SENTENCE_CLOSING_CHARS_RE = r"[\)\]}\"'’”»]*"
47
+
45
48
  if nltk_imports.is_successful():
46
49
 
47
50
  def load_sentence_tokenizer(
@@ -105,8 +108,10 @@ if nltk_imports.is_successful():
105
108
  self._period_context_fmt
106
109
  % {
107
110
  "NonWord": self._re_non_word_chars,
108
- # SentEndChars might be followed by closing brackets, so we match them here.
109
- "SentEndChars": self._re_sent_end_chars + r"[\)\]}]*",
111
+ # SentEndChars might be followed by closing brackets or quotes, so we match them here.
112
+ # If we don't, the whitespace after e.g. `."` is behind the closing quote, the pattern
113
+ # above can't reach it and it ends up in none of the sentences we return.
114
+ "SentEndChars": self._re_sent_end_chars + SENTENCE_CLOSING_CHARS_RE,
110
115
  },
111
116
  re.UNICODE | re.VERBOSE,
112
117
  )
@@ -201,6 +206,10 @@ class SentenceSplitter:
201
206
  start, end = span
202
207
  next_start, next_end = next_span
203
208
 
209
+ # with keep_white_spaces=True a span also covers the whitespace up to the next sentence, the rules below
210
+ # look at where the sentence itself ends
211
+ end = start + len(text[start:end].rstrip())
212
+
204
213
  # sentence. sentence"\nsentence -> no split (end << quote_end)
205
214
  # sentence.", sentence -> no split (end < quote_end)
206
215
  # sentence?", sentence -> no split (end < quote_end)
@@ -214,6 +223,16 @@ class SentenceSplitter:
214
223
  # question is cited
215
224
  return True
216
225
 
226
+ # sentence.", sentence -> no split (end == quote_end): widening the closing-char class (see
227
+ # period_context_re) moves the boundary just past the closing quote instead of leaving it inside,
228
+ # so anything other than whitespace directly after the quote continues the sentence.
229
+ if any(
230
+ quote_start < end == quote_end and quote_end < len(text) and not text[quote_end].isspace()
231
+ for quote_start, quote_end in quote_spans
232
+ ):
233
+ # e.g. `He said "Hi.", then left.` -> the punctuation continues the sentence
234
+ return True
235
+
217
236
  if re.search(r"(^|\n)\s*\d{1,2}\.$", text[start:end]) is not None:
218
237
  # sentence ends with a numeration
219
238
  return True
@@ -84,7 +84,11 @@ class TextCleaner:
84
84
  :param texts: List of strings to clean.
85
85
  :returns: A dictionary with the following key:
86
86
  - `texts`: the cleaned list of strings.
87
+ :raises TypeError:
88
+ If `texts` is not a list or any element is not a string.
87
89
  """
90
+ if not isinstance(texts, list) or not all(isinstance(text, str) for text in texts):
91
+ raise TypeError("`texts` must be a list of strings.")
88
92
 
89
93
  if self._regex:
90
94
  texts = [self._regex.sub("", text) for text in texts]