unique_web_search 2026.30.0.dev2__tar.gz → 2026.30.0.dev4__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 (133) hide show
  1. unique_web_search-2026.30.0.dev4/PKG-INFO +146 -0
  2. unique_web_search-2026.30.0.dev4/README.md +114 -0
  3. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/pyproject.toml +4 -6
  4. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/config.py +1 -1
  5. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/service.py +13 -3
  6. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/README.md +110 -0
  7. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/__init__.py +60 -0
  8. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/base.py +70 -0
  9. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/basic.py +61 -51
  10. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/firecrawl.py +46 -0
  11. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/jina.py +60 -53
  12. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/registry.py +58 -0
  13. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/tavily.py +20 -43
  14. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/README.md +5 -2
  15. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/executor.py +2 -2
  16. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/executor.py +2 -2
  17. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/executor.py +1 -1
  18. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/bridge.py +3 -0
  19. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/search_engine/README.md +192 -0
  20. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/base.py +14 -3
  21. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/bing.py +4 -1
  22. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/custom_api.py +8 -2
  23. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/registry.py +12 -2
  24. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/vertexai.py +4 -1
  25. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/settings.py +1 -1
  26. unique_web_search-2026.30.0.dev2/PKG-INFO +0 -149
  27. unique_web_search-2026.30.0.dev2/README.md +0 -115
  28. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/README.md +0 -360
  29. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/__init__.py +0 -107
  30. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/base.py +0 -77
  31. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/crawl4ai.py +0 -326
  32. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/firecrawl.py +0 -70
  33. unique_web_search-2026.30.0.dev2/src/unique_web_search/services/search_engine/README.md +0 -715
  34. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/__init__.py +0 -0
  35. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/client_settings.py +0 -0
  36. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/metrics.py +0 -0
  37. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/prompts.py +0 -0
  38. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/schema.py +0 -0
  39. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/_registry.py +0 -0
  40. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  41. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/config.py +0 -0
  42. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  43. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  44. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  45. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  46. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  47. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  48. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  49. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/service.py +0 -0
  50. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/client/proxy_config.py +0 -0
  51. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  52. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  53. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  54. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  55. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  56. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/config.py +0 -0
  57. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  58. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  59. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  60. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  61. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  62. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  63. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  64. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  65. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  66. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  67. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  68. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  69. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  70. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  71. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  72. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  73. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/service.py +0 -0
  74. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  75. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/utils.py +0 -0
  76. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/__init__.py +0 -0
  77. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_config.py +0 -0
  78. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_executor.py +0 -0
  79. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/context.py +0 -0
  80. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/modes.py +0 -0
  81. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  82. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/config.py +0 -0
  83. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  84. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  85. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  86. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  87. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  88. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  89. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  90. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/config.py +0 -0
  91. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  92. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  93. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  94. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  95. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  96. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  97. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  98. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/config.py +0 -0
  99. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  100. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  101. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  102. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  103. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  104. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/helpers.py +0 -0
  105. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/message_log.py +0 -0
  106. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/__init__.py +0 -0
  107. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/mappers.py +0 -0
  108. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/query_elicitation.py +0 -0
  109. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  110. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/brave.py +0 -0
  111. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/google.py +0 -0
  112. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  113. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/schema.py +0 -0
  114. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  115. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  116. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  117. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  118. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  119. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
  120. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  121. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  122. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  123. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  124. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  125. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  126. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  127. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  128. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  129. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  130. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  131. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/structured_llm.py +0 -0
  132. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/skills/web-search-v3.md +0 -0
  133. {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/utils.py +0 -0
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.3
2
+ Name: unique-web-search
3
+ Version: 2026.30.0.dev4
4
+ Summary:
5
+ Author: Andreas Hauri, Gustav Hartz, Rami Azouz
6
+ Author-email: Andreas Hauri <andreas@unique.ch>, Gustav Hartz <gustav.hartz.ext@unique.ch>, Rami Azouz <rami.ext@unique.ch>
7
+ License: Proprietary
8
+ Requires-Dist: typing-extensions>=4.14.1,<5
9
+ Requires-Dist: pydantic>=2.12.3,<3
10
+ Requires-Dist: pydantic-settings>=2.10.1,<3
11
+ Requires-Dist: timeout-decorator>=0.5.0,<1
12
+ Requires-Dist: markdownify>=0.14.1,<1
13
+ Requires-Dist: fake-useragent>=2.2.0,<3
14
+ Requires-Dist: firecrawl>=3.3.2,<4
15
+ Requires-Dist: tavily-python>=0.7.11,<1
16
+ Requires-Dist: unidecode>=1.4.0,<2
17
+ Requires-Dist: azure-ai-projects>=1.0.0,<2
18
+ Requires-Dist: azure-identity>=1.25.0,<2
19
+ Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev20,<2026.30.0rc0
20
+ Requires-Dist: azure-core>=1.36.0,<2
21
+ Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
22
+ Requires-Dist: google-auth>=2.43.0,<3
23
+ Requires-Dist: google-generativeai>=0.8.5,<0.9
24
+ Requires-Dist: rapidfuzz>=3.0.0,<4
25
+ Requires-Dist: httpx>=0.28.1
26
+ Requires-Dist: certifi>=2025.11.12
27
+ Requires-Dist: jinja2>=3.1.6
28
+ Requires-Dist: unique-search-proxy-core>=2026.30.0.dev5,<2026.30.0rc0
29
+ Requires-Dist: unique-search-proxy-sdk>=2026.30.0.dev3,<2026.30.0rc0
30
+ Requires-Python: >=3.12, <4.0
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Unique Web Search
34
+
35
+ Configurable web search tool for Unique assistants: search the web, read pages, and return citation-ready content chunks.
36
+
37
+ ## Search Proxy (recommended)
38
+
39
+ Outbound web access should go through **Unique Search Proxy** (`connectors/unique_search_proxy/`). When `SEARCH_PROXY_BASE_URL` is set, search and crawl calls use `unique-search-proxy-sdk`; credentials and provider egress stay on the proxy pod.
40
+
41
+ | Mode | When | Behaviour |
42
+ |------|------|-----------|
43
+ | **Proxy (recommended)** | `SEARCH_PROXY_BASE_URL` set | `_proxy_search` / `_proxy_crawl` via the SDK |
44
+ | **Legacy** | URL unset | Direct provider calls from this process (`_legacy_search` / `_legacy_crawl`) |
45
+
46
+ - **New standard engines (Brave, Perplexity) are proxy-only** — legacy raises.
47
+ - **Custom API** always runs locally (never routed through the proxy).
48
+ - Future provider work targets Search Proxy only; legacy is preserved for migrated engines.
49
+
50
+ Packages involved: [`unique-search-proxy-core`](../../connectors/unique_search_proxy/unique_search_proxy_core/README.md) (schemas), [`unique-search-proxy-sdk`](../../connectors/unique_search_proxy/unique_search_proxy_sdk/README.md) (HTTP), [`unique-search-proxy`](../../connectors/unique_search_proxy/unique_search_proxy_client/README.md) (server). System overview: [`connectors/unique_search_proxy/README.md`](../../connectors/unique_search_proxy/README.md).
51
+
52
+ ## Architecture
53
+
54
+ ![Web Search Tool Architecture](docs/images/architecture-diagram.svg)
55
+
56
+ ## Search engine kinds
57
+
58
+ ### Standard search
59
+
60
+ Query → normalised results (URL / title / snippet / optional content). Optional page crawl via the configured crawler.
61
+
62
+ | Engine | Proxy | Notes |
63
+ |--------|-------|-------|
64
+ | Google | legacy + proxy | Requires scraping by default |
65
+ | Brave | **proxy-only** | Rich snippets; `ExposableParam` knobs |
66
+ | Perplexity | **proxy-only** | Content extraction knobs; `ExposableParam` |
67
+ | Custom API | always local | Your REST endpoint |
68
+
69
+ Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `value`) for optional provider knobs. See [Search Engines README](./src/unique_web_search/services/search_engine/README.md).
70
+
71
+ ### Agent search (grounding)
72
+
73
+ An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s.
74
+
75
+ | Engine | Proxy | Notes |
76
+ |--------|-------|-------|
77
+ | Bing (Azure AI Foundry grounding) | legacy + proxy | Configurable `requires_scraping` |
78
+ | VertexAI (Gemini grounding) | legacy + proxy | Configurable `requires_scraping` |
79
+
80
+ Agent engines do **not** use `ExposableParam`.
81
+
82
+ ## Web crawlers (page readers)
83
+
84
+ | Crawler | Auth | Notes |
85
+ |---------|------|-------|
86
+ | Basic | none | HTTP + content-type toggles; tool-local `url_blocked_patterns` (legacy) |
87
+ | Tavily | `TAVILY_API_KEY` | Extract API |
88
+ | Jina | `JINA_API_KEY` | Reader API |
89
+ | Firecrawl | `FIRECRAWL_API_KEY` | Batch scrape |
90
+
91
+ No Crawl4AI. Crawler configs have **no exposable LLM knobs**.
92
+
93
+ ## Execution modes
94
+
95
+ | Mode | Status | Behaviour |
96
+ |------|--------|-----------|
97
+ | **V1** | Stable | Refine query → search → crawl → process |
98
+ | **V2** | Stable | Parallel plan steps (search / read URL) |
99
+ | **V3** | Experimental | Agent loop: snippet `search` vs on-demand `read_urls` |
100
+
101
+ Proxy vs legacy is transparent to executors.
102
+
103
+ ## Detailed subsystem docs
104
+
105
+ - [Search Engines](./src/unique_web_search/services/search_engine/README.md)
106
+ - [Crawlers](./src/unique_web_search/services/crawlers/README.md)
107
+ - [Executors](./src/unique_web_search/services/executors/README.md)
108
+
109
+ ## Configuration
110
+
111
+ - `SEARCH_PROXY_BASE_URL` — enables the Search Proxy client
112
+ - Provider API keys (on the proxy pod when proxy is enabled; otherwise on assistants-core for legacy)
113
+ - Search / crawler / content-processing settings via `WebSearchConfig`
114
+
115
+ ## Dependency management (uv.lock + min/latest testing)
116
+
117
+ This package is a **library** and uses `uv` for dependency management.
118
+
119
+ We run tests additionally with minimal dependencies to ensure that the listed ranges are valid. NOTE: We use lowest-direct, not lowest.
120
+ Lowest attempts to use the lowest possible dependency versions _transitively_ causing issues if a dependency has incorrect metadata. Example:
121
+ - google-cloud-aiplatform says it works with shapely<3.0.0.
122
+ - The lowest resolver assumes 1.0 which needs python 2 -> breaks
123
+ Therefore we use lowest-direct which only sets our direct dependencies to lowest. However, this only correctly verifies our min dependencies
124
+ if our code correctly lists all the required dependencies and never imports a transitive dependency. We therefore use deptry to ensure we
125
+ don't use transitive dependencies and that we have no unused dependencies.
126
+
127
+ ### Test locally
128
+
129
+ - **Latest deps and deptry**:
130
+
131
+ ```bash
132
+ cd tool_packages/unique_web_search
133
+ uv sync
134
+ uv run pytest
135
+ uv run deptry
136
+ ```
137
+
138
+ - **Min deps**:
139
+
140
+ ```bash
141
+ cd tool_packages/unique_web_search
142
+ uv venv
143
+ uv pip install -e . --resolution=lowest-direct
144
+ uv export --only-group dev --no-hashes | uv pip install -r -
145
+ uv run --no-sync pytest
146
+ ```
@@ -0,0 +1,114 @@
1
+ # Unique Web Search
2
+
3
+ Configurable web search tool for Unique assistants: search the web, read pages, and return citation-ready content chunks.
4
+
5
+ ## Search Proxy (recommended)
6
+
7
+ Outbound web access should go through **Unique Search Proxy** (`connectors/unique_search_proxy/`). When `SEARCH_PROXY_BASE_URL` is set, search and crawl calls use `unique-search-proxy-sdk`; credentials and provider egress stay on the proxy pod.
8
+
9
+ | Mode | When | Behaviour |
10
+ |------|------|-----------|
11
+ | **Proxy (recommended)** | `SEARCH_PROXY_BASE_URL` set | `_proxy_search` / `_proxy_crawl` via the SDK |
12
+ | **Legacy** | URL unset | Direct provider calls from this process (`_legacy_search` / `_legacy_crawl`) |
13
+
14
+ - **New standard engines (Brave, Perplexity) are proxy-only** — legacy raises.
15
+ - **Custom API** always runs locally (never routed through the proxy).
16
+ - Future provider work targets Search Proxy only; legacy is preserved for migrated engines.
17
+
18
+ Packages involved: [`unique-search-proxy-core`](../../connectors/unique_search_proxy/unique_search_proxy_core/README.md) (schemas), [`unique-search-proxy-sdk`](../../connectors/unique_search_proxy/unique_search_proxy_sdk/README.md) (HTTP), [`unique-search-proxy`](../../connectors/unique_search_proxy/unique_search_proxy_client/README.md) (server). System overview: [`connectors/unique_search_proxy/README.md`](../../connectors/unique_search_proxy/README.md).
19
+
20
+ ## Architecture
21
+
22
+ ![Web Search Tool Architecture](docs/images/architecture-diagram.svg)
23
+
24
+ ## Search engine kinds
25
+
26
+ ### Standard search
27
+
28
+ Query → normalised results (URL / title / snippet / optional content). Optional page crawl via the configured crawler.
29
+
30
+ | Engine | Proxy | Notes |
31
+ |--------|-------|-------|
32
+ | Google | legacy + proxy | Requires scraping by default |
33
+ | Brave | **proxy-only** | Rich snippets; `ExposableParam` knobs |
34
+ | Perplexity | **proxy-only** | Content extraction knobs; `ExposableParam` |
35
+ | Custom API | always local | Your REST endpoint |
36
+
37
+ Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `value`) for optional provider knobs. See [Search Engines README](./src/unique_web_search/services/search_engine/README.md).
38
+
39
+ ### Agent search (grounding)
40
+
41
+ An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s.
42
+
43
+ | Engine | Proxy | Notes |
44
+ |--------|-------|-------|
45
+ | Bing (Azure AI Foundry grounding) | legacy + proxy | Configurable `requires_scraping` |
46
+ | VertexAI (Gemini grounding) | legacy + proxy | Configurable `requires_scraping` |
47
+
48
+ Agent engines do **not** use `ExposableParam`.
49
+
50
+ ## Web crawlers (page readers)
51
+
52
+ | Crawler | Auth | Notes |
53
+ |---------|------|-------|
54
+ | Basic | none | HTTP + content-type toggles; tool-local `url_blocked_patterns` (legacy) |
55
+ | Tavily | `TAVILY_API_KEY` | Extract API |
56
+ | Jina | `JINA_API_KEY` | Reader API |
57
+ | Firecrawl | `FIRECRAWL_API_KEY` | Batch scrape |
58
+
59
+ No Crawl4AI. Crawler configs have **no exposable LLM knobs**.
60
+
61
+ ## Execution modes
62
+
63
+ | Mode | Status | Behaviour |
64
+ |------|--------|-----------|
65
+ | **V1** | Stable | Refine query → search → crawl → process |
66
+ | **V2** | Stable | Parallel plan steps (search / read URL) |
67
+ | **V3** | Experimental | Agent loop: snippet `search` vs on-demand `read_urls` |
68
+
69
+ Proxy vs legacy is transparent to executors.
70
+
71
+ ## Detailed subsystem docs
72
+
73
+ - [Search Engines](./src/unique_web_search/services/search_engine/README.md)
74
+ - [Crawlers](./src/unique_web_search/services/crawlers/README.md)
75
+ - [Executors](./src/unique_web_search/services/executors/README.md)
76
+
77
+ ## Configuration
78
+
79
+ - `SEARCH_PROXY_BASE_URL` — enables the Search Proxy client
80
+ - Provider API keys (on the proxy pod when proxy is enabled; otherwise on assistants-core for legacy)
81
+ - Search / crawler / content-processing settings via `WebSearchConfig`
82
+
83
+ ## Dependency management (uv.lock + min/latest testing)
84
+
85
+ This package is a **library** and uses `uv` for dependency management.
86
+
87
+ We run tests additionally with minimal dependencies to ensure that the listed ranges are valid. NOTE: We use lowest-direct, not lowest.
88
+ Lowest attempts to use the lowest possible dependency versions _transitively_ causing issues if a dependency has incorrect metadata. Example:
89
+ - google-cloud-aiplatform says it works with shapely<3.0.0.
90
+ - The lowest resolver assumes 1.0 which needs python 2 -> breaks
91
+ Therefore we use lowest-direct which only sets our direct dependencies to lowest. However, this only correctly verifies our min dependencies
92
+ if our code correctly lists all the required dependencies and never imports a transitive dependency. We therefore use deptry to ensure we
93
+ don't use transitive dependencies and that we have no unused dependencies.
94
+
95
+ ### Test locally
96
+
97
+ - **Latest deps and deptry**:
98
+
99
+ ```bash
100
+ cd tool_packages/unique_web_search
101
+ uv sync
102
+ uv run pytest
103
+ uv run deptry
104
+ ```
105
+
106
+ - **Min deps**:
107
+
108
+ ```bash
109
+ cd tool_packages/unique_web_search
110
+ uv venv
111
+ uv pip install -e . --resolution=lowest-direct
112
+ uv export --only-group dev --no-hashes | uv pip install -r -
113
+ uv run --no-sync pytest
114
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_web_search"
3
- version = "2026.30.0.dev2"
3
+ version = "2026.30.0.dev4"
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.dev20,<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,9 +31,8 @@ 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",
37
- "unique-search-proxy-sdk>=2026.30.0.dev2,<2026.30.0rc0",
34
+ "unique-search-proxy-core>=2026.30.0.dev5,<2026.30.0rc0",
35
+ "unique-search-proxy-sdk>=2026.30.0.dev3,<2026.30.0rc0",
38
36
  ]
39
37
 
40
38
  [build-system]
@@ -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(
@@ -3,6 +3,7 @@ from datetime import datetime
3
3
  from time import time
4
4
 
5
5
  from typing_extensions import override
6
+ from unique_search_proxy_core.context import RequestContext
6
7
  from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
7
8
  from unique_search_proxy_core.search_engines.base import BaseSearchEngineConfig
8
9
  from unique_toolkit._common.chunk_relevancy_sorter.service import ChunkRelevancySorter
@@ -72,13 +73,22 @@ class WebSearchTool(Tool[WebSearchConfig]):
72
73
  language_model_orchestrator or configuration.token_counting_language_model
73
74
  )
74
75
 
76
+ self.chunk_relevancy_sorter = ChunkRelevancySorter(self.event)
77
+ self.company_id = self.event.company_id
78
+ self.request_context = RequestContext(
79
+ company_id=self.event.company_id,
80
+ user_id=self.event.user_id,
81
+ chat_id=self.event.payload.chat_id,
82
+ )
75
83
  self.search_engine_service = get_search_engine_service(
76
84
  self.config.search_engine_config,
77
85
  self.language_model_service,
86
+ request_context=self.request_context,
87
+ )
88
+ self.crawler_service = get_crawler_service(
89
+ self.config.crawler_config,
90
+ request_context=self.request_context,
78
91
  )
79
- self.crawler_service = get_crawler_service(self.config.crawler_config)
80
- self.chunk_relevancy_sorter = ChunkRelevancySorter(self.event)
81
- self.company_id = self.event.company_id
82
92
  self.chat_history_token_length = 0
83
93
  self.chat_history_chat_messages = self._chat_service.get_full_history()
84
94
 
@@ -0,0 +1,110 @@
1
+ # Crawlers
2
+
3
+ Web-page readers used by Unique Web Search to fetch URLs and convert them to text.
4
+
5
+ ## Architecture
6
+
7
+ Crawler configuration lives in **`unique_search_proxy_core.crawlers`**. The tool registers implementations via `CRAWLER_REGISTRY` and dispatches through `BaseCrawler`.
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ Config["proxy-core *Config"]
12
+ Registry["CRAWLER_REGISTRY"]
13
+ Base["BaseCrawler"]
14
+ SDK["SDK crawl"]
15
+ Legacy["_legacy_crawl"]
16
+
17
+ Config --> Registry
18
+ Registry --> Base
19
+ Base -->|"SEARCH_PROXY_BASE_URL set"| SDK
20
+ Base -->|"unset"| Legacy
21
+ ```
22
+
23
+ ### Key invariants
24
+
25
+ - **No exposable params.** Crawlers have no LLM override surface — only deployment defaults + `urls`.
26
+ - **Proxy body = config fields + urls.** `BaseCrawler._proxy_crawl` dumps `config.model_dump(exclude={"crawler", ...})` into the SDK call.
27
+ - **Legacy retained** when the search proxy is disabled.
28
+
29
+ ## Available crawlers
30
+
31
+ | Name | Config source | Auth | Notes |
32
+ |------|---------------|------|-------|
33
+ | Basic | tool-local `BasicConfig` extends proxy-core | none | HTTP fetch + processors; `content_types` toggles; `url_blocked_patterns` (tool-local, stripped before proxy) |
34
+ | Tavily | `TavilyConfig` | `TAVILY_API_KEY` | Extract API |
35
+ | Jina | `JinaConfig` | `JINA_API_KEY` | Reader API |
36
+ | Firecrawl | `FirecrawlConfig` | `FIRECRAWL_API_KEY` | Batch scrape |
37
+
38
+ There is **no Crawl4AI** crawler.
39
+
40
+ `WebSearchConfig.crawler_config` is a discriminated union on `crawler` (`Basic` / `Tavily` / `Jina` / `Firecrawl`).
41
+
42
+ ---
43
+
44
+ ## Basic
45
+
46
+ | Field | Default | Notes |
47
+ |-------|---------|-------|
48
+ | `timeout` | from base | Per-request timeout (seconds) |
49
+ | `content_types` | html/xhtml/plain/markdown on; pdf off | Which MIME types to process |
50
+ | `max_concurrent_requests` | `10` | Parallelism |
51
+ | `url_blocked_patterns` | tool-local | Regex URL blocklist; **not** sent to the proxy |
52
+
53
+ ```python
54
+ from unique_search_proxy_core.crawlers import CrawlerType
55
+ from unique_web_search.services.crawlers import BasicConfig, get_crawler_service
56
+
57
+ config = BasicConfig(crawler=CrawlerType.BASIC, timeout=30)
58
+ markdowns = await get_crawler_service(config).crawl(["https://example.com"])
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Tavily
64
+
65
+ | Field | Default | Notes |
66
+ |-------|---------|-------|
67
+ | `extract_depth` | `advanced` | `basic` / `advanced` |
68
+ | `format` | `markdown` | `markdown` / `text` |
69
+ | `query` | unset | Optional rerank intent |
70
+ | `chunks_per_source` | unset | Requires `query` |
71
+ | `include_images` / `include_favicon` / `include_usage` | `false` | Response extras |
72
+
73
+ ---
74
+
75
+ ## Jina
76
+
77
+ | Field | Default | Notes |
78
+ |-------|---------|-------|
79
+ | `return_format` | `markdown` | `markdown` / `html` / `text` / … |
80
+ | `engine` | `browser` | `auto` / `browser` / `direct` / `cf-browser-rendering` |
81
+ | `page_timeout` | unset | Seconds; falls back to crawl `timeout` |
82
+ | `max_concurrent_requests` | `10` | |
83
+ | `no_cache` | `false` | |
84
+ | `do_not_track` | `true` | |
85
+ | selectors / summaries | unset / false | `target_selector`, `wait_for_selector`, `remove_selector`, image/link options, `locale`, `referer`, `proxy_url` |
86
+
87
+ ---
88
+
89
+ ## Firecrawl
90
+
91
+ | Field | Default | Notes |
92
+ |-------|---------|-------|
93
+ | `only_main_content` | `true` | Strip chrome before markdown |
94
+ | `only_clean_content` | `false` | Extra LLM clean pass |
95
+ | `max_concurrency` | unset | Batch parallelism |
96
+ | `ignore_invalid_urls` | `true` | |
97
+ | `wait_for` | `0` | ms delay before fetch |
98
+ | `mobile` | `false` | |
99
+ | `block_ads` | `true` | |
100
+ | `remove_base64_images` | `true` | |
101
+ | `proxy` | `auto` | Firecrawl proxy tier |
102
+ | `include_tags` / `exclude_tags` / `scrape_headers` / `max_age` | unset | Optional |
103
+
104
+ ---
105
+
106
+ ## Adding a crawler
107
+
108
+ 1. Define config (+ request model) in `unique_search_proxy_core.crawlers.<name>`.
109
+ 2. Add a tool module with `@register_crawler(...)` implementing `_legacy_crawl`.
110
+ 3. Modules are auto-discovered; keep static unions in `__init__.py` and cover with `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,70 @@
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.context import LOCAL_REQUEST_CONTEXT, RequestContext
7
+ from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig
8
+ from unique_search_proxy_core.url_safety import (
9
+ CrawlTargetValidationError,
10
+ ResolvedCrawlTarget,
11
+ UrlSafetyService,
12
+ )
13
+
14
+ from unique_web_search.metrics import crawl_blocked
15
+ from unique_web_search.services.proxy.bridge import (
16
+ open_search_proxy_client,
17
+ search_proxy_client_enabled,
18
+ )
19
+ from unique_web_search.services.proxy.mappers import map_crawl_response
20
+
21
+ CrawlerConfig = TypeVar(
22
+ "CrawlerConfig",
23
+ bound=BaseCrawlerConfig,
24
+ )
25
+
26
+
27
+ class BaseCrawler(ABC, Generic[CrawlerConfig]):
28
+ """Base class for web-page crawlers.
29
+
30
+ The search-proxy path is implemented entirely here. Subclasses only provide
31
+ the direct ``_legacy_crawl`` implementation used when the proxy is disabled.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ config: CrawlerConfig,
37
+ *,
38
+ request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
39
+ ):
40
+ self.config = config
41
+ self._request_context = request_context
42
+
43
+ async def crawl(self, urls: list[str]) -> list[str]:
44
+ if search_proxy_client_enabled:
45
+ return await self._proxy_crawl(urls)
46
+
47
+ try:
48
+ targets = await UrlSafetyService.validate_batch_urls(urls)
49
+ except CrawlTargetValidationError as exc:
50
+ for target in exc.blocked_targets:
51
+ crawl_blocked.labels(reason_category=target.category).inc()
52
+ raise
53
+ return await self._legacy_crawl(targets)
54
+
55
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]:
56
+ """Dump deployment config fields into the generic proxy crawl call."""
57
+ params = self.config.model_dump(exclude={"crawler"}, exclude_none=True)
58
+ async with open_search_proxy_client(
59
+ timeout=float(self.config.timeout),
60
+ context=self._request_context,
61
+ ) as client:
62
+ response = await client.crawl.crawl(
63
+ urls=urls,
64
+ crawler=self.config.crawler,
65
+ **params,
66
+ )
67
+ return map_crawl_response(response, urls)
68
+
69
+ @abstractmethod
70
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]: ...