unique_web_search 2026.30.0.dev2__tar.gz → 2026.30.0.dev3__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 (130) hide show
  1. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/PKG-INFO +4 -7
  2. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/README.md +1 -2
  3. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/pyproject.toml +3 -5
  4. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/config.py +1 -1
  5. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/README.md +58 -0
  6. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/__init__.py +60 -0
  7. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/base.py +62 -0
  8. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/basic.py +54 -51
  9. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/firecrawl.py +49 -0
  10. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/jina.py +61 -51
  11. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/registry.py +50 -0
  12. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/tavily.py +21 -41
  13. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/executor.py +2 -2
  14. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/executor.py +2 -2
  15. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/executor.py +1 -1
  16. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/README.md +1 -1
  17. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/settings.py +1 -1
  18. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/README.md +0 -360
  19. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/__init__.py +0 -107
  20. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/base.py +0 -77
  21. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/crawl4ai.py +0 -326
  22. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/firecrawl.py +0 -70
  23. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/__init__.py +0 -0
  24. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/client_settings.py +0 -0
  25. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/metrics.py +0 -0
  26. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/prompts.py +0 -0
  27. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/schema.py +0 -0
  28. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/service.py +0 -0
  29. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/_registry.py +0 -0
  30. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  31. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/config.py +0 -0
  32. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  33. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  34. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  35. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  36. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  37. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  38. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  39. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/service.py +0 -0
  40. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/client/proxy_config.py +0 -0
  41. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  42. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  43. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  44. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  45. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  46. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/config.py +0 -0
  47. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  48. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  49. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  50. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  51. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  52. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  53. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  54. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  55. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  56. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  57. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  58. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  59. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  60. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  61. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  62. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  63. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/service.py +0 -0
  64. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  65. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/utils.py +0 -0
  66. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/README.md +0 -0
  67. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/__init__.py +0 -0
  68. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/base_config.py +0 -0
  69. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/base_executor.py +0 -0
  70. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/context.py +0 -0
  71. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/modes.py +0 -0
  72. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  73. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/config.py +0 -0
  74. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  75. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  76. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  77. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  78. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  79. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  80. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  81. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/config.py +0 -0
  82. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  83. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  84. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  85. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  86. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  87. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  88. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  89. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/config.py +0 -0
  90. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  91. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  92. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  93. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  94. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  95. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/helpers.py +0 -0
  96. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/message_log.py +0 -0
  97. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/__init__.py +0 -0
  98. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/bridge.py +0 -0
  99. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/mappers.py +0 -0
  100. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/query_elicitation.py +0 -0
  101. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  102. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/base.py +0 -0
  103. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/bing.py +0 -0
  104. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/brave.py +0 -0
  105. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/custom_api.py +0 -0
  106. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/google.py +0 -0
  107. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  108. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/registry.py +0 -0
  109. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/schema.py +0 -0
  110. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  111. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  112. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  113. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  114. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  115. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
  116. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  117. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  118. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  119. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  120. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  121. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  122. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  123. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
  124. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  125. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  126. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  127. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  128. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/structured_llm.py +0 -0
  129. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/skills/web-search-v3.md +0 -0
  130. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-web-search
3
- Version: 2026.30.0.dev2
3
+ Version: 2026.30.0.dev3
4
4
  Summary:
5
5
  Author: Andreas Hauri, Gustav Hartz, Rami Azouz
6
6
  Author-email: Andreas Hauri <andreas@unique.ch>, Gustav Hartz <gustav.hartz.ext@unique.ch>, Rami Azouz <rami.ext@unique.ch>
@@ -11,13 +11,12 @@ Requires-Dist: pydantic-settings>=2.10.1,<3
11
11
  Requires-Dist: timeout-decorator>=0.5.0,<1
12
12
  Requires-Dist: markdownify>=0.14.1,<1
13
13
  Requires-Dist: fake-useragent>=2.2.0,<3
14
- Requires-Dist: crawl4ai>=0.6.3,<1
15
14
  Requires-Dist: firecrawl>=3.3.2,<4
16
15
  Requires-Dist: tavily-python>=0.7.11,<1
17
16
  Requires-Dist: unidecode>=1.4.0,<2
18
17
  Requires-Dist: azure-ai-projects>=1.0.0,<2
19
18
  Requires-Dist: azure-identity>=1.25.0,<2
20
- Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev12,<2026.30.0rc0
19
+ Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev14,<2026.30.0rc0
21
20
  Requires-Dist: azure-core>=1.36.0,<2
22
21
  Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
23
22
  Requires-Dist: google-auth>=2.43.0,<3
@@ -26,8 +25,7 @@ Requires-Dist: rapidfuzz>=3.0.0,<4
26
25
  Requires-Dist: httpx>=0.28.1
27
26
  Requires-Dist: certifi>=2025.11.12
28
27
  Requires-Dist: jinja2>=3.1.6
29
- Requires-Dist: playwright>=1.58.0
30
- Requires-Dist: unique-search-proxy-core>=2026.30.0.dev2,<2026.30.0rc0
28
+ Requires-Dist: unique-search-proxy-core>=2026.30.0.dev3,<2026.30.0rc0
31
29
  Requires-Dist: unique-search-proxy-sdk>=2026.30.0.dev2,<2026.30.0rc0
32
30
  Requires-Python: >=3.12, <4.0
33
31
  Description-Content-Type: text/markdown
@@ -60,7 +58,6 @@ The following diagram illustrates the complete architecture and workflow of the
60
58
 
61
59
  - **Multiple Web Crawlers**:
62
60
  - Basic HTTP Crawler
63
- - Crawl4AI
64
61
  - Jina Reader
65
62
  - Tavily Crawler
66
63
  - Firecrawl Crawler
@@ -85,7 +82,7 @@ The following diagram illustrates the complete architecture and workflow of the
85
82
  For deeper dives into each subsystem, see the dedicated READMEs:
86
83
 
87
84
  - [Search Engines](./unique_web_search/services/search_engine/README.md) &mdash; full catalogue of supported engines, configuration, and usage examples.
88
- - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Crawl4AI, Tavily, Firecrawl, Jina) with setup guides.
85
+ - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Tavily, Firecrawl, Jina) with setup guides.
89
86
  - [Executors](./unique_web_search/services/executors/README.md) &mdash; orchestration layer (V1 & V2) covering query refinement, planning, logging, and best practices.
90
87
 
91
88
  ## Configuration
@@ -26,7 +26,6 @@ The following diagram illustrates the complete architecture and workflow of the
26
26
 
27
27
  - **Multiple Web Crawlers**:
28
28
  - Basic HTTP Crawler
29
- - Crawl4AI
30
29
  - Jina Reader
31
30
  - Tavily Crawler
32
31
  - Firecrawl Crawler
@@ -51,7 +50,7 @@ The following diagram illustrates the complete architecture and workflow of the
51
50
  For deeper dives into each subsystem, see the dedicated READMEs:
52
51
 
53
52
  - [Search Engines](./unique_web_search/services/search_engine/README.md) &mdash; full catalogue of supported engines, configuration, and usage examples.
54
- - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Crawl4AI, Tavily, Firecrawl, Jina) with setup guides.
53
+ - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Tavily, Firecrawl, Jina) with setup guides.
55
54
  - [Executors](./unique_web_search/services/executors/README.md) &mdash; orchestration layer (V1 & V2) covering query refinement, planning, logging, and best practices.
56
55
 
57
56
  ## Configuration
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_web_search"
3
- version = "2026.30.0.dev2"
3
+ version = "2026.30.0.dev3"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -17,13 +17,12 @@ dependencies = [
17
17
  "timeout-decorator>=0.5.0,<1",
18
18
  "markdownify>=0.14.1,<1",
19
19
  "fake-useragent>=2.2.0,<3",
20
- "crawl4ai>=0.6.3,<1",
21
20
  "firecrawl>=3.3.2,<4",
22
21
  "tavily-python>=0.7.11,<1",
23
22
  "unidecode>=1.4.0,<2",
24
23
  "azure-ai-projects>=1.0.0,<2",
25
24
  "azure-identity>=1.25.0,<2",
26
- "unique-toolkit[monitoring]>=2026.30.0.dev12,<2026.30.0rc0",
25
+ "unique-toolkit[monitoring]>=2026.30.0.dev14,<2026.30.0rc0",
27
26
  "azure-core>=1.36.0,<2",
28
27
  "google-cloud-aiplatform>=1.128.0,<2",
29
28
  "google-auth>=2.43.0,<3",
@@ -32,8 +31,7 @@ dependencies = [
32
31
  "httpx>=0.28.1",
33
32
  "certifi>=2025.11.12",
34
33
  "jinja2>=3.1.6",
35
- "playwright>=1.58.0",
36
- "unique-search-proxy-core>=2026.30.0.dev2,<2026.30.0rc0",
34
+ "unique-search-proxy-core>=2026.30.0.dev3,<2026.30.0rc0",
37
35
  "unique-search-proxy-sdk>=2026.30.0.dev2,<2026.30.0rc0",
38
36
  ]
39
37
 
@@ -180,7 +180,7 @@ class WebSearchConfig(BaseToolConfig):
180
180
  default_factory=DefaultCrawler, # type: ignore (This type is computed at runtime so pyright is not able to infer it)
181
181
  title="Web Page Reader",
182
182
  description="Choose and configure how web pages are fetched and converted to readable text.",
183
- discriminator="crawler_type",
183
+ discriminator="crawler",
184
184
  )
185
185
 
186
186
  content_processor_config: ContentProcessorConfig = Field(
@@ -0,0 +1,58 @@
1
+ # Crawlers
2
+
3
+ Web-page readers used by Unique Web Search to fetch and convert URLs into readable text.
4
+
5
+ ## Architecture
6
+
7
+ Crawler configuration lives in **proxy-core** (`unique_search_proxy_core.crawlers`). The tool consumes those configs, registers implementations via `CRAWLER_REGISTRY`, and centralizes proxy dispatch in `BaseCrawler` — the same pattern as search engines.
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ Config["proxy-core *Config\n(crawler discriminator)"]
12
+ Registry["CRAWLER_REGISTRY"]
13
+ Base["BaseCrawler._proxy_crawl"]
14
+ SDK["client.crawl.crawl(crawler, urls, **config)"]
15
+ Legacy["_legacy_crawl\n(direct provider call)"]
16
+
17
+ Config --> Registry
18
+ Registry --> Base
19
+ Base -->|"proxy enabled"| SDK
20
+ Base -->|"proxy disabled"| Legacy
21
+ ```
22
+
23
+ ### Key invariants
24
+
25
+ - **No exposable params.** Crawlers receive URLs only; deployment defaults come from the config. There is no LLM override surface.
26
+ - **Proxy request = config fields + urls.** `BaseCrawler._proxy_crawl` dumps `config.model_dump(exclude={"crawler"})` into the generic SDK call. No merge helper.
27
+ - **Legacy fallback retained.** When the search proxy is disabled, each crawler implements `_legacy_crawl` against the provider SDK / HTTP client.
28
+
29
+ ## Available crawlers
30
+
31
+ | Name | Config | Auth | Notes |
32
+ |------|--------|------|-------|
33
+ | Basic | tool-local `BasicConfig` (extends proxy-core) | None | HTTP fetch + markdownify; content-type toggles; `url_blocked_patterns` (to be decommissioned) |
34
+ | Tavily | `TavilyConfig` | `TAVILY_API_KEY` | Extract API; `extract_depth`, `format`, … |
35
+ | Jina | `JinaConfig` | `JINA_API_KEY` | Reader API; typed `return_format` / `engine` / `do_not_track` |
36
+ | Firecrawl | `FirecrawlConfig` | `FIRECRAWL_API_KEY` | Batch scrape; `only_main_content`, … |
37
+
38
+ ## Configuration
39
+
40
+ `WebSearchConfig.crawler_config` is a discriminated union on `crawler` (`"Basic"` / `"Tavily"` / `"Jina"` / `"Firecrawl"`). Active crawlers are selected via `active_inhouse_crawlers` plus API-key auto-append in settings.
41
+
42
+ ### Example
43
+
44
+ ```python
45
+ from unique_search_proxy_core.crawlers import CrawlerType
46
+ from unique_web_search.services.crawlers import BasicConfig, get_crawler_service
47
+
48
+ config = BasicConfig(crawler=CrawlerType.BASIC, timeout=30)
49
+ crawler = get_crawler_service(config)
50
+ markdowns = await crawler.crawl(["https://example.com"])
51
+ ```
52
+
53
+ ## Adding a crawler
54
+
55
+ 1. Define the deployment config + request model in proxy-core (`unique_search_proxy_core.crawlers.<name>`).
56
+ 2. Add a tool module under `services/crawlers/` that `@register_crawler(...)`s an implementation with `_legacy_crawl` only.
57
+ 3. Modules are auto-discovered — no `__init__.py` factory edit needed.
58
+ 4. Add the config/impl types to the static unions in `__init__.py` (for pyright) and cover them in `tests/test_crawler_registry.py`.
@@ -0,0 +1,60 @@
1
+ from pydantic import BaseModel
2
+ from unique_search_proxy_core.crawlers import CrawlerType
3
+ from unique_search_proxy_core.crawlers.firecrawl.schema import FirecrawlConfig
4
+ from unique_search_proxy_core.crawlers.jina.schema import JinaConfig
5
+ from unique_search_proxy_core.crawlers.tavily.schema import TavilyConfig
6
+
7
+ from unique_web_search.services.crawlers.base import BaseCrawler
8
+ from unique_web_search.services.crawlers.registry import (
9
+ CRAWLER_REGISTRY,
10
+ get_crawler_service,
11
+ )
12
+
13
+ CRAWLER_REGISTRY.autodiscover(
14
+ __path__,
15
+ __name__,
16
+ exclude=frozenset({"url_safety", "utils", "registry"}),
17
+ )
18
+
19
+ from unique_web_search.services.crawlers.basic import ( # noqa: E402
20
+ BasicConfig,
21
+ BasicCrawler,
22
+ )
23
+ from unique_web_search.services.crawlers.firecrawl import ( # noqa: E402
24
+ FirecrawlCrawler,
25
+ )
26
+ from unique_web_search.services.crawlers.jina import JinaCrawler # noqa: E402
27
+ from unique_web_search.services.crawlers.tavily import TavilyCrawler # noqa: E402
28
+
29
+ CrawlerTypes = BasicCrawler | FirecrawlCrawler | JinaCrawler | TavilyCrawler
30
+
31
+ CrawlerConfigTypes = BasicConfig | FirecrawlConfig | JinaConfig | TavilyConfig
32
+
33
+ CRAWLER_NAME_TO_CONFIG = CRAWLER_REGISTRY.name_to_config()
34
+
35
+
36
+ def get_crawler_config_types_from_names(crawler_names: list[str]) -> type[BaseModel]:
37
+ return CRAWLER_REGISTRY.config_types_from_names(crawler_names)
38
+
39
+
40
+ def get_default_crawler_config(crawler_names: list[str]) -> type[BaseModel]:
41
+ return CRAWLER_REGISTRY.default_config(crawler_names)
42
+
43
+
44
+ __all__ = [
45
+ "BasicCrawler",
46
+ "BasicConfig",
47
+ "FirecrawlCrawler",
48
+ "FirecrawlConfig",
49
+ "JinaCrawler",
50
+ "JinaConfig",
51
+ "TavilyCrawler",
52
+ "TavilyConfig",
53
+ "CrawlerType",
54
+ "BaseCrawler",
55
+ "get_crawler_service",
56
+ "CrawlerConfigTypes",
57
+ "CrawlerTypes",
58
+ "get_crawler_config_types_from_names",
59
+ "get_default_crawler_config",
60
+ ]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Generic, TypeVar
5
+
6
+ from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig
7
+ from unique_search_proxy_core.url_safety import (
8
+ CrawlTargetValidationError,
9
+ ResolvedCrawlTarget,
10
+ UrlSafetyService,
11
+ )
12
+
13
+ from unique_web_search.metrics import crawl_blocked
14
+ from unique_web_search.services.proxy.bridge import (
15
+ open_search_proxy_client,
16
+ search_proxy_client_enabled,
17
+ )
18
+ from unique_web_search.services.proxy.mappers import map_crawl_response
19
+
20
+ CrawlerConfig = TypeVar(
21
+ "CrawlerConfig",
22
+ bound=BaseCrawlerConfig,
23
+ )
24
+
25
+
26
+ class BaseCrawler(ABC, Generic[CrawlerConfig]):
27
+ """Base class for web-page crawlers.
28
+
29
+ The search-proxy path is implemented entirely here. Subclasses only provide
30
+ the direct ``_legacy_crawl`` implementation used when the proxy is disabled.
31
+ """
32
+
33
+ def __init__(self, config: CrawlerConfig):
34
+ self.config = config
35
+
36
+ async def crawl(self, urls: list[str]) -> list[str]:
37
+ if search_proxy_client_enabled:
38
+ return await self._proxy_crawl(urls)
39
+
40
+ try:
41
+ targets = await UrlSafetyService.validate_batch_urls(urls)
42
+ except CrawlTargetValidationError as exc:
43
+ for target in exc.blocked_targets:
44
+ crawl_blocked.labels(reason_category=target.category).inc()
45
+ raise
46
+ return await self._legacy_crawl(targets)
47
+
48
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]:
49
+ """Dump deployment config fields into the generic proxy crawl call."""
50
+ params = self.config.model_dump(exclude={"crawler"}, exclude_none=True)
51
+ async with open_search_proxy_client(
52
+ timeout=float(self.config.timeout)
53
+ ) as client:
54
+ response = await client.crawl.crawl(
55
+ urls=urls,
56
+ crawler=self.config.crawler,
57
+ **params,
58
+ )
59
+ return map_crawl_response(response, urls)
60
+
61
+ @abstractmethod
62
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]: ...
@@ -1,84 +1,82 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import re
4
- from typing import Literal
4
+ from typing import Annotated
5
5
 
6
6
  import timeout_decorator
7
7
  from httpx import AsyncClient, Timeout
8
8
  from markdownify import markdownify
9
9
  from pydantic import Field
10
10
  from typing_extensions import override
11
+ from unique_search_proxy_core.crawlers.base import CrawlerType
12
+ from unique_search_proxy_core.crawlers.basic.content_types import (
13
+ CONTENT_TYPE_TOGGLE_TO_MIME,
14
+ )
15
+ from unique_search_proxy_core.crawlers.basic.schema import (
16
+ BasicConfig as CoreBasicConfig,
17
+ )
18
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
11
19
 
12
20
  from unique_web_search.services.client.proxy_config import async_client
13
- from unique_web_search.services.crawlers.base import (
14
- BaseCrawler,
15
- BaseCrawlerConfig,
16
- CrawlerType,
17
- )
21
+ from unique_web_search.services.crawlers.base import BaseCrawler
22
+ from unique_web_search.services.crawlers.registry import register_crawler
18
23
  from unique_web_search.services.crawlers.url_safety import (
19
24
  CrawlTargetValidationError,
20
25
  ResolvedCrawlTarget,
21
26
  )
22
27
  from unique_web_search.services.crawlers.utils import get_random_user_agent
23
- from unique_web_search.services.proxy.bridge import (
24
- open_search_proxy_client,
25
- )
28
+ from unique_web_search.services.proxy.bridge import open_search_proxy_client
26
29
  from unique_web_search.services.proxy.mappers import map_crawl_response
27
30
 
28
31
  _LOGGER = logging.getLogger(__name__)
29
32
 
30
- unwanted_types = {
31
- "application/octet-stream",
32
- "application/pdf",
33
- "application/msword",
34
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
35
- "image/",
36
- "video/",
37
- "audio/",
38
- }
39
-
40
-
41
- class BasicCrawlerConfig(BaseCrawlerConfig[CrawlerType.BASIC]):
42
- crawler_type: Literal[CrawlerType.BASIC] = CrawlerType.BASIC
43
- url_pattern_blacklist: list[str] = Field(
44
- default=[r".*\.pdf$"],
45
- description="List of URL patterns to blacklist",
46
- )
47
- unwanted_content_types: set[str] = Field(
48
- default=unwanted_types,
49
- description="Set of content types to not allow",
50
- )
51
- max_concurrent_requests: int = Field(
52
- default=10,
53
- description="The maximum number of concurrent requests to make to the same domain.",
54
- )
55
33
 
34
+ class BasicConfig(CoreBasicConfig):
35
+ """Tool-local Basic crawler config; extends proxy-core with legacy URL filters."""
36
+
37
+ url_blocked_patterns: Annotated[
38
+ list[str],
39
+ RJSFMetaTag({"ui:options": {"orderable": False}}),
40
+ ] = Field(
41
+ default_factory=lambda: [r".*\.pdf$"],
42
+ title="URL blocked patterns",
43
+ description=(
44
+ "List of URL regex patterns to skip when crawling. "
45
+ "**Note:** This field will be deprecated in the future."
46
+ ),
47
+ )
56
48
 
57
- class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
58
- supports_proxy_crawl = True
59
49
 
60
- def __init__(self, config: BasicCrawlerConfig):
50
+ @register_crawler(
51
+ name="basic",
52
+ key=CrawlerType.BASIC,
53
+ config_cls=BasicConfig,
54
+ config_display_name="Basic",
55
+ )
56
+ class BasicCrawler(BaseCrawler[BasicConfig]):
57
+ def __init__(self, config: BasicConfig):
61
58
  super().__init__(config)
62
59
 
63
60
  self.semaphore: asyncio.Semaphore = asyncio.Semaphore(
64
61
  self.config.max_concurrent_requests
65
62
  )
66
63
 
67
- # TODO: Find a good way for tracking
68
- # @track(
69
- # tags=["basic", "scrape"],
70
- # )
71
64
  @override
72
65
  async def _proxy_crawl(self, urls: list[str]) -> list[str]:
66
+ """Proxy crawl excludes tool-only fields the search-proxy schema forbids."""
67
+ params = self.config.model_dump(
68
+ exclude={"crawler", "url_blocked_patterns"},
69
+ exclude_none=True,
70
+ )
73
71
  async with open_search_proxy_client(
74
- timeout=float(self.config.timeout),
72
+ timeout=float(self.config.timeout)
75
73
  ) as client:
76
- response = await client.crawl.basic(
74
+ response = await client.crawl.crawl(
77
75
  urls=urls,
78
- timeout=int(self.config.timeout),
79
- max_concurrent_requests=self.config.max_concurrent_requests,
76
+ crawler=self.config.crawler,
77
+ **params,
80
78
  )
81
- return map_crawl_response(response, urls)
79
+ return map_crawl_response(response, urls)
82
80
 
83
81
  @override
84
82
  async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
@@ -111,8 +109,8 @@ class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
111
109
  ) -> str:
112
110
  headers = {"User-Agent": get_random_user_agent()}
113
111
 
114
- if self._is_url_blacklisted(target.normalized_url):
115
- return "Unable to crawl URL due to blacklisted pattern"
112
+ if self._is_url_blocked(target.normalized_url):
113
+ return "Unable to crawl URL due to blocked pattern"
116
114
 
117
115
  request_headers = dict(headers)
118
116
  request_extensions: dict[str, str] = {}
@@ -142,13 +140,18 @@ class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
142
140
 
143
141
  return markdown
144
142
 
145
- def _is_url_blacklisted(self, url: str) -> bool:
143
+ def _is_url_blocked(self, url: str) -> bool:
146
144
  return any(
147
- re.match(pattern, url) for pattern in self.config.url_pattern_blacklist
145
+ re.match(pattern, url) for pattern in self.config.url_blocked_patterns
148
146
  )
149
147
 
150
148
  def _content_type_not_allowed(self, content_type: str) -> bool:
151
- return content_type in self.config.unwanted_content_types
149
+ allowed_mimes = {
150
+ mime
151
+ for field_name, mime in CONTENT_TYPE_TOGGLE_TO_MIME.items()
152
+ if getattr(self.config.content_types, field_name)
153
+ }
154
+ return content_type not in allowed_mimes
152
155
 
153
156
 
154
157
  def _markdownify_html_with_timeout(content: str, timeout: float) -> str:
@@ -0,0 +1,49 @@
1
+ from typing import override
2
+
3
+ from firecrawl import AsyncFirecrawl
4
+ from unique_search_proxy_core.crawlers.base import CrawlerType
5
+ from unique_search_proxy_core.crawlers.firecrawl.schema import FirecrawlConfig
6
+
7
+ from unique_web_search.client_settings import (
8
+ get_firecrawl_search_settings,
9
+ )
10
+ from unique_web_search.services.crawlers.base import BaseCrawler
11
+ from unique_web_search.services.crawlers.registry import register_crawler
12
+ from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
13
+
14
+
15
+ @register_crawler(
16
+ name="firecrawl",
17
+ key=CrawlerType.FIRECRAWL,
18
+ config_cls=FirecrawlConfig,
19
+ config_display_name="Firecrawl",
20
+ )
21
+ class FirecrawlCrawler(BaseCrawler[FirecrawlConfig]):
22
+ def __init__(self, config: FirecrawlConfig):
23
+ super().__init__(config)
24
+
25
+ @override
26
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
27
+ urls = [target.normalized_url for target in targets]
28
+
29
+ api_key = get_firecrawl_search_settings().api_key
30
+ assert api_key is not None, "Firecrawl API key is not configured"
31
+
32
+ client = AsyncFirecrawl(api_key=api_key)
33
+
34
+ response = await client.batch_scrape(
35
+ urls=urls,
36
+ formats=["markdown"],
37
+ wait_timeout=self.config.timeout,
38
+ )
39
+
40
+ results = response.data
41
+
42
+ markdowns = [
43
+ result.markdown
44
+ if result.markdown is not None
45
+ else "An error occurred while scraping the URL"
46
+ for result in results
47
+ ]
48
+
49
+ return markdowns
@@ -1,21 +1,15 @@
1
1
  import asyncio
2
- from typing import Any, Literal, cast, override
2
+ from typing import Any, override
3
3
 
4
4
  from httpx import AsyncClient
5
5
  from pydantic import BaseModel, Field, HttpUrl
6
- from unique_search_proxy_core.crawlers.jina.schema import JinaEngine, JinaReturnFormat
6
+ from unique_search_proxy_core.crawlers.base import CrawlerType
7
+ from unique_search_proxy_core.crawlers.jina.schema import JinaConfig
7
8
 
8
9
  from unique_web_search.client_settings import get_jina_search_settings
9
- from unique_web_search.services.crawlers.base import (
10
- BaseCrawler,
11
- BaseCrawlerConfigExperimental,
12
- CrawlerType,
13
- )
10
+ from unique_web_search.services.crawlers.base import BaseCrawler
11
+ from unique_web_search.services.crawlers.registry import register_crawler
14
12
  from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
15
- from unique_web_search.services.proxy.bridge import (
16
- open_search_proxy_client,
17
- )
18
- from unique_web_search.services.proxy.mappers import map_crawl_response
19
13
 
20
14
 
21
15
  class ReaderBody(BaseModel):
@@ -38,46 +32,16 @@ class ReaderResponse(BaseModel):
38
32
  data: ReaderData | None = None
39
33
 
40
34
 
41
- class JinaCrawlerConfig(BaseCrawlerConfigExperimental[CrawlerType.JINA]):
42
- crawler_type: Literal[CrawlerType.JINA] = CrawlerType.JINA
43
- headers: dict[str, str] = Field(
44
- default={
45
- "X-Return-Format": "markdown",
46
- "X-Engine": "browser",
47
- "DNT": "1",
48
- },
49
- description="Headers to send with the request",
50
- )
51
-
52
-
53
- class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
54
- supports_proxy_crawl = True
55
-
56
- def __init__(self, config: JinaCrawlerConfig):
35
+ @register_crawler(
36
+ name="jina",
37
+ key=CrawlerType.JINA,
38
+ config_cls=JinaConfig,
39
+ config_display_name="Jina",
40
+ )
41
+ class JinaCrawler(BaseCrawler[JinaConfig]):
42
+ def __init__(self, config: JinaConfig):
57
43
  super().__init__(config)
58
44
 
59
- # TODO: Find a solution for tracking
60
- # @track(
61
- # tags=["jina", "scrape"],
62
- # )
63
- @override
64
- async def _proxy_crawl(self, urls: list[str]) -> list[str]:
65
- headers = self.config.headers
66
- async with open_search_proxy_client(
67
- timeout=float(self.config.timeout),
68
- ) as client:
69
- response = await client.crawl.jina(
70
- urls=urls,
71
- timeout=int(self.config.timeout),
72
- return_format=cast(
73
- JinaReturnFormat,
74
- headers.get("X-Return-Format", "markdown"),
75
- ),
76
- engine=cast(JinaEngine, headers.get("X-Engine", "browser")),
77
- do_not_track=headers.get("DNT") == "1",
78
- )
79
- return map_crawl_response(response, urls)
80
-
81
45
  @override
82
46
  async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
83
47
  urls = [target.normalized_url for target in targets]
@@ -91,7 +55,7 @@ class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
91
55
  "Authorization": f"Bearer {api_key}",
92
56
  "Content-Type": "application/json",
93
57
  "Accept": "application/json",
94
- } | self.config.headers
58
+ }
95
59
 
96
60
  async with AsyncClient(timeout=self.config.timeout) as client:
97
61
  tasks = [
@@ -116,6 +80,52 @@ class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
116
80
 
117
81
  return markdown_results
118
82
 
83
+ def _build_reader_body(self, url: str) -> dict[str, Any]:
84
+ """Build the Jina Reader POST body applying all deployment config fields.
85
+
86
+ Mirrors ``build_jina_reader_body`` on the search-proxy path so the direct
87
+ (proxy-disabled) crawl honours the same ``JinaConfig`` fields.
88
+ """
89
+ config = self.config
90
+ page_timeout = config.page_timeout
91
+ if page_timeout is None:
92
+ page_timeout = min(max(config.timeout, 1), 180)
93
+
94
+ body: dict[str, Any] = {
95
+ "url": url,
96
+ "respondWith": config.return_format,
97
+ "engine": config.engine,
98
+ "timeout": page_timeout,
99
+ "doNotTrack": config.do_not_track,
100
+ }
101
+
102
+ if config.no_cache:
103
+ body["noCache"] = True
104
+ if config.target_selector is not None:
105
+ body["targetSelector"] = config.target_selector
106
+ if config.wait_for_selector is not None:
107
+ body["waitForSelector"] = config.wait_for_selector
108
+ if config.remove_selector is not None:
109
+ body["removeSelector"] = config.remove_selector
110
+ if config.with_generated_alt:
111
+ body["withGeneratedAlt"] = True
112
+ if config.with_links_summary:
113
+ body["withLinksSummary"] = True
114
+ if config.with_images_summary:
115
+ body["withImagesSummary"] = True
116
+ if config.with_iframe:
117
+ body["withIframe"] = True
118
+ if config.retain_images is not None:
119
+ body["retainImages"] = config.retain_images
120
+ if config.locale is not None:
121
+ body["locale"] = config.locale
122
+ if config.referer is not None:
123
+ body["referer"] = config.referer
124
+ if config.proxy_url is not None:
125
+ body["proxyUrl"] = config.proxy_url
126
+
127
+ return body
128
+
119
129
  async def _crawl_url(
120
130
  self,
121
131
  url: str,
@@ -126,7 +136,7 @@ class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
126
136
  params = {
127
137
  "url": reader_api_endpoint,
128
138
  "headers": headers,
129
- "json": {"url": url},
139
+ "json": self._build_reader_body(url),
130
140
  }
131
141
  response = await client.post(**params)
132
142
  return ReaderResponse.model_validate(response.json())