unique-web-search 2026.26.0.dev7__tar.gz → 2026.26.0.dev8__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 (129) hide show
  1. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/PKG-INFO +2 -3
  2. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/pyproject.toml +4 -4
  3. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/__init__.py +1 -16
  4. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/base.py +12 -6
  5. unique_web_search-2026.26.0.dev8/src/unique_web_search/services/crawlers/url_safety/__init__.py +21 -0
  6. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/basic_proxy.py +0 -79
  7. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -15
  8. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/dns.py +0 -53
  9. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/models.py +0 -77
  10. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/netloc.py +0 -62
  11. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/policy.py +0 -52
  12. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/redirect.py +0 -81
  13. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/resolver.py +0 -103
  14. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/service.py +0 -64
  15. unique_web_search-2026.26.0.dev7/src/unique_web_search/services/crawlers/url_safety/settings.py +0 -60
  16. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/README.md +0 -0
  17. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/__init__.py +0 -0
  18. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/client_settings.py +0 -0
  19. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/config.py +0 -0
  20. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/metrics.py +0 -0
  21. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/prompts.py +0 -0
  22. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/schema.py +0 -0
  23. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/service.py +0 -0
  24. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  25. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/config.py +0 -0
  26. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  27. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  28. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  29. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  30. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  31. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  32. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/argument_screening/service.py +0 -0
  33. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/client/proxy_config.py +0 -0
  34. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  35. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  36. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  37. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  38. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  39. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/config.py +0 -0
  40. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  41. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  42. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  43. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  44. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  45. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  46. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  47. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  48. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  49. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  50. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  51. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  52. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  53. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  54. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  55. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  56. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/content_processing/service.py +0 -0
  57. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/README.md +0 -0
  58. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/basic.py +0 -0
  59. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/crawl4ai.py +0 -0
  60. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  61. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/jina.py +0 -0
  62. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  63. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/crawlers/utils.py +0 -0
  64. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/README.md +0 -0
  65. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/__init__.py +0 -0
  66. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/base_config.py +0 -0
  67. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/base_executor.py +0 -0
  68. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/context.py +0 -0
  69. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  70. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/config.py +0 -0
  71. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/executor.py +0 -0
  72. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  73. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  74. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/prompts/restrict_date_description.j2 +0 -0
  75. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  76. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  77. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  78. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  79. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/config.py +0 -0
  80. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/executor.py +0 -0
  81. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  82. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  83. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  84. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  85. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  86. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  87. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v3/config.py +0 -0
  88. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  89. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  90. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  91. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  92. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  93. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/helpers.py +0 -0
  94. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/message_log.py +0 -0
  95. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/query_elicitation.py +0 -0
  96. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/README.md +0 -0
  97. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  98. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/base.py +0 -0
  99. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/bing.py +0 -0
  100. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/brave.py +0 -0
  101. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/custom_api.py +0 -0
  102. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/firecrawl.py +0 -0
  103. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/google.py +0 -0
  104. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/jina.py +0 -0
  105. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  106. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/schema.py +0 -0
  107. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/tavily.py +0 -0
  108. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  109. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  110. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  111. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  112. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  113. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
  114. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  115. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  116. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  117. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  118. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  119. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  120. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  121. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
  122. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  123. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  124. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  125. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  126. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/services/structured_llm.py +0 -0
  127. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/settings.py +0 -0
  128. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/src/unique_web_search/skills/web-search-v3.md +0 -0
  129. {unique_web_search-2026.26.0.dev7 → unique_web_search-2026.26.0.dev8}/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.26.0.dev7
3
+ Version: 2026.26.0.dev8
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>
@@ -28,8 +28,7 @@ Requires-Dist: certifi>=2025.11.12
28
28
  Requires-Dist: jinja2>=3.1.6
29
29
  Requires-Dist: playwright>=1.58.0
30
30
  Requires-Dist: perplexityai>=0.34.0
31
- Requires-Dist: unique-search-proxy-core>=2026.26.0.dev5,<2026.26.0rc0
32
- Requires-Dist: unique-search-proxy-sdk>=2026.26.0.dev5,<2026.26.0rc0
31
+ Requires-Dist: unique-search-proxy-core>=2026.26.0.dev6,<2026.26.0rc0
33
32
  Requires-Python: >=3.12, <4.0
34
33
  Description-Content-Type: text/markdown
35
34
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_web_search"
3
- version = "2026.26.0.dev7"
3
+ version = "2026.26.0.dev8"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -34,8 +34,8 @@ dependencies = [
34
34
  "jinja2>=3.1.6",
35
35
  "playwright>=1.58.0",
36
36
  "perplexityai>=0.34.0",
37
- "unique-search-proxy-core>=2026.26.0.dev5,<2026.26.0rc0",
38
- "unique-search-proxy-sdk>=2026.26.0.dev5,<2026.26.0rc0",
37
+ "unique-search-proxy-core>=2026.26.0.dev6,<2026.26.0rc0",
38
+ # "unique-search-proxy-sdk>=2026.24.0.dev0",
39
39
  ]
40
40
 
41
41
  [build-system]
@@ -93,5 +93,5 @@ extend-select = ["I"]
93
93
 
94
94
  [tool.uv.sources]
95
95
  unique-toolkit = { workspace = true }
96
- unique-search-proxy-sdk = { workspace = true }
96
+ # unique-search-proxy-sdk = { workspace = true }
97
97
  unique-search-proxy-core = { workspace = true }
@@ -9,10 +9,6 @@ from unique_web_search.services.crawlers.basic import (
9
9
  BasicCrawler,
10
10
  BasicCrawlerConfig,
11
11
  )
12
- from unique_web_search.services.crawlers.basic_proxy import (
13
- BasicCrawlerProxyConfig,
14
- BasicProxyCrawler,
15
- )
16
12
  from unique_web_search.services.crawlers.crawl4ai import (
17
13
  Crawl4AiCrawler,
18
14
  Crawl4AiCrawlerConfig,
@@ -31,17 +27,11 @@ from unique_web_search.services.crawlers.tavily import (
31
27
  )
32
28
 
33
29
  CrawlerTypes = (
34
- BasicCrawler
35
- | BasicProxyCrawler
36
- | Crawl4AiCrawler
37
- | FirecrawlCrawler
38
- | JinaCrawler
39
- | TavilyCrawler
30
+ BasicCrawler | Crawl4AiCrawler | FirecrawlCrawler | JinaCrawler | TavilyCrawler
40
31
  )
41
32
 
42
33
  CrawlerConfigTypes = (
43
34
  BasicCrawlerConfig
44
- | BasicCrawlerProxyConfig
45
35
  | Crawl4AiCrawlerConfig
46
36
  | FirecrawlCrawlerConfig
47
37
  | JinaCrawlerConfig
@@ -54,7 +44,6 @@ CRAWLER_NAME_TO_CONFIG = {
54
44
  "firecrawl": FirecrawlCrawlerConfig,
55
45
  "jina": JinaCrawlerConfig,
56
46
  "tavily": TavilyCrawlerConfig,
57
- "basic_proxy": BasicCrawlerProxyConfig,
58
47
  }
59
48
 
60
49
 
@@ -72,8 +61,6 @@ def get_crawler_service(
72
61
  return FirecrawlCrawler(crawler_config)
73
62
  case CrawlerType.JINA:
74
63
  return JinaCrawler(crawler_config)
75
- case CrawlerType.BASIC_PROXY:
76
- return BasicProxyCrawler(crawler_config)
77
64
 
78
65
 
79
66
  def get_crawler_config_types_from_names(crawler_names: list[str]) -> TypeAlias:
@@ -103,8 +90,6 @@ def get_default_crawler_config(
103
90
  __all__ = [
104
91
  "BasicCrawler",
105
92
  "BasicCrawlerConfig",
106
- "BasicProxyCrawler",
107
- "BasicCrawlerProxyConfig",
108
93
  "Crawl4AiCrawler",
109
94
  "Crawl4AiCrawlerConfig",
110
95
  "FirecrawlCrawler",
@@ -3,14 +3,16 @@ from enum import StrEnum
3
3
  from typing import Generic, TypeVar
4
4
 
5
5
  from pydantic import BaseModel
6
+ from unique_search_proxy_core.url_safety import (
7
+ CrawlTargetValidationError,
8
+ ResolvedCrawlTarget,
9
+ UrlSafetyService,
10
+ )
6
11
  from unique_toolkit.agentic.tools.config import (
7
12
  get_configuration_dict,
8
13
  )
9
14
 
10
- from unique_web_search.services.crawlers.url_safety import (
11
- ResolvedCrawlTarget,
12
- UrlSafetyService,
13
- )
15
+ from unique_web_search.metrics import crawl_blocked
14
16
  from unique_web_search.services.helpers import (
15
17
  clean_model_title_generator,
16
18
  experimental_model_title_generator,
@@ -20,7 +22,6 @@ from unique_web_search.services.helpers import (
20
22
  class CrawlerType(StrEnum):
21
23
  CRAWL4AI = "Crawl4AiCrawler"
22
24
  BASIC = "BasicCrawler"
23
- BASIC_PROXY = "BasicProxyCrawler"
24
25
  NO_CRAWLER = "NoCrawler"
25
26
  TAVILY = "TavilyCrawler"
26
27
  FIRECRAWL = "FirecrawlCrawler"
@@ -55,7 +56,12 @@ class BaseCrawler(ABC, Generic[CrawlerConfig]):
55
56
  self.config = config
56
57
 
57
58
  async def crawl(self, urls: list[str]) -> list[str]:
58
- targets = await UrlSafetyService.validate_batch_urls(urls)
59
+ try:
60
+ targets = await UrlSafetyService.validate_batch_urls(urls)
61
+ except CrawlTargetValidationError as exc:
62
+ for target in exc.blocked_targets:
63
+ crawl_blocked.labels(reason_category=target.category).inc()
64
+ raise
59
65
  return await self._crawl(targets)
60
66
 
61
67
  @abstractmethod
@@ -0,0 +1,21 @@
1
+ """Deprecated re-exports — use ``unique_search_proxy_core.url_safety`` directly."""
2
+
3
+ from unique_search_proxy_core.url_safety import (
4
+ BlockedCrawlTarget,
5
+ CrawlTargetValidationError,
6
+ ResolvedCrawlTarget,
7
+ UrlSafetyOutcome,
8
+ UrlSafetyService,
9
+ UrlSafetySettings,
10
+ url_safety_settings,
11
+ )
12
+
13
+ __all__ = [
14
+ "BlockedCrawlTarget",
15
+ "CrawlTargetValidationError",
16
+ "ResolvedCrawlTarget",
17
+ "UrlSafetyOutcome",
18
+ "UrlSafetyService",
19
+ "UrlSafetySettings",
20
+ "url_safety_settings",
21
+ ]
@@ -1,79 +0,0 @@
1
- import logging
2
- from typing import Literal
3
-
4
- from unique_search_proxy_core.crawlers import (
5
- BasicConfig as BasicCrawlerProxyCoreConfig,
6
- )
7
- from unique_search_proxy_core.crawlers.base import CrawlerType as ProxyCrawlerType
8
- from unique_search_proxy_sdk import UniqueSearchProxyClient
9
- from unique_search_proxy_sdk._generated.models.crawl_url_result import CrawlUrlResult
10
- from unique_search_proxy_sdk._generated.models.per_url_error import PerUrlError
11
-
12
- from unique_web_search.client_settings import get_search_proxy_settings
13
- from unique_web_search.services.crawlers.base import BaseCrawlerConfig, CrawlerType
14
-
15
- _LOGGER = logging.getLogger(__name__)
16
-
17
-
18
- class BasicCrawlerProxyConfig(
19
- BaseCrawlerConfig[CrawlerType.BASIC_PROXY], BasicCrawlerProxyCoreConfig
20
- ):
21
- crawler_type: Literal[CrawlerType.BASIC_PROXY] = CrawlerType.BASIC_PROXY
22
-
23
-
24
- class BasicProxyCrawler:
25
- """Crawler that delegates fetching to the remote Unique Search Proxy service.
26
-
27
- Intermediate transition crawler: instead of fetching URLs in-process (see
28
- :class:`BasicCrawler`), it forwards the request to the search-proxy service
29
- via :class:`UniqueSearchProxyClient` and maps the per-URL results back to the
30
- markdown list contract shared by the other crawlers.
31
- """
32
-
33
- def __init__(self, config: BasicCrawlerProxyConfig):
34
- self.config: BasicCrawlerProxyConfig = config
35
-
36
- async def crawl(self, urls: list[str]) -> list[str]:
37
- return await self._crawl(urls)
38
-
39
- async def _crawl(self, urls: list[str]) -> list[str]:
40
- base_url = get_search_proxy_settings().base_url
41
- assert base_url is not None, "Unique Search Proxy base URL is not configured"
42
-
43
- try:
44
- async with UniqueSearchProxyClient(
45
- base_url=base_url,
46
- timeout=float(self.config.timeout),
47
- ) as client:
48
- response = await client.crawl.crawl(
49
- urls,
50
- crawler=ProxyCrawlerType.BASIC,
51
- content_types=self.config.content_types,
52
- timeout=self.config.timeout,
53
- max_concurrent_requests=self.config.max_concurrent_requests,
54
- )
55
- except Exception as error:
56
- _LOGGER.exception("Unique Search Proxy crawl request failed")
57
- return [f"Unable to crawl URL via search proxy: {error}" for _ in urls]
58
-
59
- url_to_markdown = {
60
- result.url: _result_to_markdown(result) for result in response.results
61
- }
62
-
63
- return [
64
- url_to_markdown.get(url, "Error: URL not found in search proxy response")
65
- for url in urls
66
- ]
67
-
68
-
69
- def _result_to_markdown(result: CrawlUrlResult) -> str:
70
- if isinstance(result.content, str):
71
- return result.content
72
-
73
- if isinstance(result.error, PerUrlError):
74
- return f"Error: {result.error.message}"
75
-
76
- if isinstance(result.raw, str):
77
- return result.raw
78
-
79
- return "Error: No content returned by search proxy"
@@ -1,15 +0,0 @@
1
- from unique_web_search.services.crawlers.url_safety.models import (
2
- BlockedCrawlTarget,
3
- CrawlTargetValidationError,
4
- ResolvedCrawlTarget,
5
- )
6
- from unique_web_search.services.crawlers.url_safety.service import UrlSafetyService
7
- from unique_web_search.services.crawlers.url_safety.settings import url_safety_settings
8
-
9
- __all__ = [
10
- "BlockedCrawlTarget",
11
- "url_safety_settings",
12
- "CrawlTargetValidationError",
13
- "ResolvedCrawlTarget",
14
- "UrlSafetyService",
15
- ]
@@ -1,53 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import socket
5
- from ipaddress import ip_address
6
-
7
-
8
- async def resolve_host_addresses(host: str) -> tuple[str, ...]:
9
- loop = asyncio.get_running_loop()
10
- resolved = await loop.run_in_executor(
11
- None, lambda: socket.getaddrinfo(host, None, type=socket.SOCK_STREAM)
12
- )
13
- return tuple(
14
- dict.fromkeys(str(ip_address(sockaddr[0])) for _, _, _, _, sockaddr in resolved)
15
- )
16
-
17
-
18
- def block_reason_for_resolved_addresses(
19
- resolved_addresses: tuple[str, ...],
20
- ) -> tuple[str, str] | None:
21
- if not resolved_addresses:
22
- return (
23
- "dns",
24
- "Target host could not be resolved during safety validation",
25
- )
26
-
27
- for resolved_address in resolved_addresses:
28
- if not ip_address(resolved_address).is_global:
29
- return (
30
- "private",
31
- "Target host resolves to a private or special-use IP address",
32
- )
33
-
34
- return None
35
-
36
-
37
- async def resolve_and_validate_host(
38
- host: str,
39
- ) -> tuple[tuple[str, ...], tuple[str, str] | None]:
40
- try:
41
- resolved_addresses = await resolve_host_addresses(host)
42
- except socket.gaierror:
43
- return (), (
44
- "dns",
45
- "Target host could not be resolved during safety validation",
46
- )
47
-
48
- return resolved_addresses, block_reason_for_resolved_addresses(resolved_addresses)
49
-
50
-
51
- async def validate_resolved_host(host: str) -> tuple[str, str] | None:
52
- _, validation_error = await resolve_and_validate_host(host)
53
- return validation_error
@@ -1,77 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from urllib.parse import urlsplit
5
-
6
- from unique_web_search.metrics import crawl_blocked
7
- from unique_web_search.services.crawlers.url_safety.netloc import (
8
- build_netloc,
9
- replace_url_host,
10
- )
11
-
12
-
13
- @dataclass(frozen=True)
14
- class BlockedCrawlTarget:
15
- hostname: str | None
16
- category: str
17
- reason: str
18
-
19
- @property
20
- def display_target(self) -> str:
21
- return self.hostname or "<unknown-host>"
22
-
23
-
24
- @dataclass(frozen=True)
25
- class ResolvedCrawlTarget:
26
- """Carries the validated host/IP values reused when issuing crawl requests."""
27
-
28
- normalized_url: str
29
- hostname: str
30
- resolved_ip: str
31
- used_dns_resolution: bool
32
-
33
- @property
34
- def request_url(self) -> str:
35
- if not self.used_dns_resolution and not self.resolved_ip:
36
- return self.normalized_url
37
- return replace_url_host(urlsplit(self.normalized_url), host=self.resolved_ip)
38
-
39
- @property
40
- def host_header(self) -> str | None:
41
- if not self.used_dns_resolution:
42
- return None
43
-
44
- return build_netloc(host=self.hostname, port=urlsplit(self.normalized_url).port)
45
-
46
- @property
47
- def sni_hostname(self) -> str | None:
48
- if not self.used_dns_resolution:
49
- return None
50
-
51
- if urlsplit(self.normalized_url).scheme.lower() != "https":
52
- return None
53
-
54
- return self.hostname
55
-
56
-
57
- class CrawlTargetValidationError(ValueError):
58
- def __init__(self, blocked_targets: list[BlockedCrawlTarget]):
59
- self.blocked_targets: list[BlockedCrawlTarget] = blocked_targets
60
-
61
- for target in blocked_targets:
62
- crawl_blocked.labels(reason_category=target.category).inc()
63
-
64
- details = "; ".join(
65
- f"{target.display_target} ({target.reason})"
66
- for target in self.blocked_targets
67
- )
68
- super().__init__(f"Blocked crawl target(s) due to URL safety policy: {details}")
69
-
70
-
71
- def bypass_crawl_target(url: str) -> ResolvedCrawlTarget:
72
- return ResolvedCrawlTarget(
73
- normalized_url=url.strip(),
74
- hostname="",
75
- resolved_ip="",
76
- used_dns_resolution=False,
77
- )
@@ -1,62 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from ipaddress import ip_address
4
- from urllib.parse import SplitResult, urlsplit, urlunsplit
5
-
6
-
7
- def extract_hostname(url: str) -> str | None:
8
- normalized_url = url.strip()
9
- if not normalized_url:
10
- return None
11
-
12
- hostname = urlsplit(normalized_url).hostname
13
- if hostname is None:
14
- return None
15
-
16
- return hostname.rstrip(".").lower()
17
-
18
-
19
- def replace_url_host(parsed_url: SplitResult, *, host: str) -> str:
20
- netloc = build_netloc(
21
- host=host,
22
- port=parsed_url.port,
23
- username=parsed_url.username,
24
- password=parsed_url.password,
25
- )
26
- return urlunsplit(
27
- (
28
- parsed_url.scheme,
29
- netloc,
30
- parsed_url.path,
31
- parsed_url.query,
32
- "",
33
- )
34
- )
35
-
36
-
37
- def build_netloc(
38
- host: str,
39
- port: int | None,
40
- username: str | None = None,
41
- password: str | None = None,
42
- ) -> str:
43
- credential_prefix = ""
44
- if username is not None:
45
- credential_prefix = username
46
- if password is not None:
47
- credential_prefix = f"{credential_prefix}:{password}"
48
- credential_prefix = f"{credential_prefix}@"
49
-
50
- try:
51
- host_ip = ip_address(host)
52
- except ValueError:
53
- normalized_host = host
54
- else:
55
- normalized_host = (
56
- f"[{host_ip.compressed}]" if host_ip.version == 6 else host_ip.compressed
57
- )
58
-
59
- if port is None:
60
- return f"{credential_prefix}{normalized_host}"
61
-
62
- return f"{credential_prefix}{normalized_host}:{port}"
@@ -1,52 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from ipaddress import ip_address
4
- from urllib.parse import urlsplit
5
-
6
- from unique_web_search.services.crawlers.url_safety.settings import url_safety_settings
7
-
8
-
9
- def validate_target_cheap(url: str) -> tuple[str, str] | None:
10
- """Static validation only — no DNS. Returns (category, reason) if blocked, None otherwise."""
11
- if not url:
12
- return "empty", "URL is empty"
13
-
14
- parsed_url = urlsplit(url)
15
- if parsed_url.scheme.lower() not in url_safety_settings.allowed_schemes:
16
- return "scheme", "URL scheme must be http or https"
17
-
18
- hostname = parsed_url.hostname
19
- if not hostname:
20
- return "host", "URL host is missing or malformed"
21
-
22
- normalized_host = hostname.rstrip(".").lower()
23
-
24
- if normalized_host in url_safety_settings.metadata_hosts:
25
- return "metadata", "Target points to a metadata endpoint"
26
-
27
- if (
28
- normalized_host in url_safety_settings.localhost_hosts
29
- or normalized_host.endswith(".localhost")
30
- ):
31
- return "localhost", "Target points to a localhost host"
32
-
33
- if normalized_host.endswith(url_safety_settings.service_suffix):
34
- return "cluster", "Target points to an internal service host"
35
-
36
- if normalized_host.endswith(url_safety_settings.cluster_local_suffix):
37
- return "cluster", "Target points to an internal cluster-local host"
38
-
39
- try:
40
- target_ip = ip_address(normalized_host)
41
- except ValueError:
42
- pass
43
- else:
44
- if not target_ip.is_global:
45
- return "private", "Target points to a private or special-use IP address"
46
-
47
- return None
48
-
49
- if "." not in normalized_host:
50
- return "cluster", "Target points to a single-label internal host"
51
-
52
- return None
@@ -1,81 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from collections.abc import Awaitable, Callable
5
- from urllib.parse import urljoin
6
-
7
- import httpx
8
-
9
- from unique_web_search.services.crawlers.url_safety.models import (
10
- BlockedCrawlTarget,
11
- CrawlTargetValidationError,
12
- )
13
- from unique_web_search.services.crawlers.url_safety.netloc import extract_hostname
14
- from unique_web_search.services.crawlers.url_safety.settings import url_safety_settings
15
-
16
- _LOGGER = logging.getLogger(__name__)
17
-
18
- _REDIRECT_STATUS_CODES = frozenset({301, 302, 303, 307, 308})
19
-
20
- ValidateUrlFn = Callable[[str], Awaitable[tuple[str, str] | None]]
21
-
22
-
23
- async def resolve_redirect_chain(
24
- url: str,
25
- validate_url: ValidateUrlFn,
26
- ) -> str:
27
- """Follow HTTP 3xx redirects hop-by-hop, validating each destination.
28
-
29
- Returns the final validated URL.
30
- Raises CrawlTargetValidationError if any hop is blocked.
31
- """
32
- current = url
33
- timeout = url_safety_settings.redirect_timeout_seconds
34
- async with httpx.AsyncClient(follow_redirects=False, timeout=timeout) as client:
35
- for _ in range(url_safety_settings.max_redirect_hops):
36
- error = await validate_url(current)
37
- if error is not None:
38
- category, reason = error
39
- raise CrawlTargetValidationError(
40
- [
41
- BlockedCrawlTarget(
42
- hostname=extract_hostname(current),
43
- category=category,
44
- reason=reason,
45
- )
46
- ]
47
- )
48
-
49
- try:
50
- resp = await client.head(current)
51
- except Exception as exc:
52
- _LOGGER.debug(
53
- "Redirect resolution stopped at %s due to network error: %s",
54
- current,
55
- exc,
56
- )
57
- break
58
-
59
- if resp.status_code not in _REDIRECT_STATUS_CODES:
60
- break
61
-
62
- location = resp.headers.get("location")
63
- if not location:
64
- break
65
-
66
- current = urljoin(current, location)
67
-
68
- error = await validate_url(current)
69
- if error is not None:
70
- category, reason = error
71
- raise CrawlTargetValidationError(
72
- [
73
- BlockedCrawlTarget(
74
- hostname=extract_hostname(current),
75
- category=category,
76
- reason=reason,
77
- )
78
- ]
79
- )
80
-
81
- return current
@@ -1,103 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- from ipaddress import ip_address
5
- from urllib.parse import urlsplit
6
-
7
- from unique_web_search.services.crawlers.url_safety import dns
8
- from unique_web_search.services.crawlers.url_safety.models import (
9
- BlockedCrawlTarget,
10
- CrawlTargetValidationError,
11
- ResolvedCrawlTarget,
12
- )
13
- from unique_web_search.services.crawlers.url_safety.netloc import extract_hostname
14
- from unique_web_search.services.crawlers.url_safety.policy import validate_target_cheap
15
-
16
-
17
- async def resolve_crawl_target(url: str) -> ResolvedCrawlTarget:
18
- """Validate and resolve a single URL, performing DNS exactly once."""
19
- normalized_url = url.strip()
20
-
21
- validation_error = validate_target_cheap(normalized_url)
22
- if validation_error is not None:
23
- category, reason = validation_error
24
- raise CrawlTargetValidationError(
25
- [
26
- BlockedCrawlTarget(
27
- hostname=extract_hostname(normalized_url),
28
- category=category,
29
- reason=reason,
30
- )
31
- ]
32
- )
33
-
34
- parsed_url = urlsplit(normalized_url)
35
- hostname = parsed_url.hostname
36
- if hostname is None:
37
- raise CrawlTargetValidationError(
38
- [
39
- BlockedCrawlTarget(
40
- hostname=None,
41
- category="host",
42
- reason="URL host is missing or malformed",
43
- )
44
- ]
45
- )
46
-
47
- normalized_host = hostname.rstrip(".").lower()
48
-
49
- try:
50
- target_ip = ip_address(normalized_host)
51
- except ValueError:
52
- resolved_addresses, validation_error = await dns.resolve_and_validate_host(
53
- normalized_host
54
- )
55
- if validation_error is not None:
56
- category, reason = validation_error
57
- raise CrawlTargetValidationError(
58
- [
59
- BlockedCrawlTarget(
60
- hostname=normalized_host,
61
- category=category,
62
- reason=reason,
63
- )
64
- ]
65
- )
66
- return ResolvedCrawlTarget(
67
- normalized_url=normalized_url,
68
- hostname=normalized_host,
69
- resolved_ip=resolved_addresses[0],
70
- used_dns_resolution=True,
71
- )
72
-
73
- return ResolvedCrawlTarget(
74
- normalized_url=normalized_url,
75
- hostname=normalized_host,
76
- resolved_ip=str(target_ip),
77
- used_dns_resolution=False,
78
- )
79
-
80
-
81
- async def resolve_crawl_targets_batch(urls: list[str]) -> list[ResolvedCrawlTarget]:
82
- """Resolve and validate each URL, collecting all policy violations before raising."""
83
- results: list[ResolvedCrawlTarget | BaseException] = list(
84
- await asyncio.gather(
85
- *[resolve_crawl_target(url) for url in urls],
86
- return_exceptions=True,
87
- )
88
- )
89
-
90
- blocked: list[BlockedCrawlTarget] = []
91
- resolved: list[ResolvedCrawlTarget] = []
92
- for result in results:
93
- if isinstance(result, CrawlTargetValidationError):
94
- blocked.extend(result.blocked_targets)
95
- elif isinstance(result, BaseException):
96
- raise result
97
- else:
98
- resolved.append(result)
99
-
100
- if blocked:
101
- raise CrawlTargetValidationError(blocked)
102
-
103
- return resolved