unique_web_search 2026.30.0.dev3__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 (129) 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.dev3 → unique_web_search-2026.30.0.dev4}/pyproject.toml +4 -4
  4. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/service.py +13 -3
  5. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/README.md +110 -0
  6. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/base.py +10 -2
  7. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/basic.py +10 -3
  8. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/firecrawl.py +0 -3
  9. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/jina.py +0 -3
  10. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/registry.py +10 -2
  11. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/tavily.py +0 -3
  12. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/README.md +5 -2
  13. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/bridge.py +3 -0
  14. unique_web_search-2026.30.0.dev4/src/unique_web_search/services/search_engine/README.md +192 -0
  15. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/base.py +14 -3
  16. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/bing.py +4 -1
  17. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/custom_api.py +8 -2
  18. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/registry.py +12 -2
  19. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/vertexai.py +4 -1
  20. unique_web_search-2026.30.0.dev3/PKG-INFO +0 -146
  21. unique_web_search-2026.30.0.dev3/README.md +0 -114
  22. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/README.md +0 -58
  23. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/search_engine/README.md +0 -715
  24. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/__init__.py +0 -0
  25. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/client_settings.py +0 -0
  26. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/config.py +0 -0
  27. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/metrics.py +0 -0
  28. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/prompts.py +0 -0
  29. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/schema.py +0 -0
  30. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/_registry.py +0 -0
  31. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  32. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/config.py +0 -0
  33. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  34. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  35. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  36. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  37. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  38. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  39. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  40. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/service.py +0 -0
  41. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/client/proxy_config.py +0 -0
  42. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  43. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  44. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  45. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  46. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  47. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/config.py +0 -0
  48. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  49. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  50. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  51. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  52. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  53. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  54. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  55. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  56. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  57. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  58. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  59. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  60. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  61. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  62. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  63. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  64. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/service.py +0 -0
  65. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  66. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  67. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/utils.py +0 -0
  68. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/__init__.py +0 -0
  69. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_config.py +0 -0
  70. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_executor.py +0 -0
  71. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/context.py +0 -0
  72. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/modes.py +0 -0
  73. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  74. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/config.py +0 -0
  75. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/executor.py +0 -0
  76. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  77. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  78. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  79. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  80. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  81. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  82. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  83. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/config.py +0 -0
  84. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/executor.py +0 -0
  85. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  86. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  87. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  88. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  89. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  90. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  91. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  92. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/config.py +0 -0
  93. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  94. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  95. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  96. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  97. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  98. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  99. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/helpers.py +0 -0
  100. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/message_log.py +0 -0
  101. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/__init__.py +0 -0
  102. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/mappers.py +0 -0
  103. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/query_elicitation.py +0 -0
  104. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  105. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/brave.py +0 -0
  106. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/google.py +0 -0
  107. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  108. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/schema.py +0 -0
  109. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  110. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  111. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  112. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  113. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  114. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
  115. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  116. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  117. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  118. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  119. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  120. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  121. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  122. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  123. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  124. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  125. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  126. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/structured_llm.py +0 -0
  127. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/settings.py +0 -0
  128. {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/skills/web-search-v3.md +0 -0
  129. {unique_web_search-2026.30.0.dev3 → 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.dev3"
3
+ version = "2026.30.0.dev4"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "unidecode>=1.4.0,<2",
23
23
  "azure-ai-projects>=1.0.0,<2",
24
24
  "azure-identity>=1.25.0,<2",
25
- "unique-toolkit[monitoring]>=2026.30.0.dev14,<2026.30.0rc0",
25
+ "unique-toolkit[monitoring]>=2026.30.0.dev20,<2026.30.0rc0",
26
26
  "azure-core>=1.36.0,<2",
27
27
  "google-cloud-aiplatform>=1.128.0,<2",
28
28
  "google-auth>=2.43.0,<3",
@@ -31,8 +31,8 @@ dependencies = [
31
31
  "httpx>=0.28.1",
32
32
  "certifi>=2025.11.12",
33
33
  "jinja2>=3.1.6",
34
- "unique-search-proxy-core>=2026.30.0.dev3,<2026.30.0rc0",
35
- "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",
36
36
  ]
37
37
 
38
38
  [build-system]
@@ -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`.
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from abc import ABC, abstractmethod
4
4
  from typing import Generic, TypeVar
5
5
 
6
+ from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
6
7
  from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig
7
8
  from unique_search_proxy_core.url_safety import (
8
9
  CrawlTargetValidationError,
@@ -30,8 +31,14 @@ class BaseCrawler(ABC, Generic[CrawlerConfig]):
30
31
  the direct ``_legacy_crawl`` implementation used when the proxy is disabled.
31
32
  """
32
33
 
33
- def __init__(self, config: CrawlerConfig):
34
+ def __init__(
35
+ self,
36
+ config: CrawlerConfig,
37
+ *,
38
+ request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
39
+ ):
34
40
  self.config = config
41
+ self._request_context = request_context
35
42
 
36
43
  async def crawl(self, urls: list[str]) -> list[str]:
37
44
  if search_proxy_client_enabled:
@@ -49,7 +56,8 @@ class BaseCrawler(ABC, Generic[CrawlerConfig]):
49
56
  """Dump deployment config fields into the generic proxy crawl call."""
50
57
  params = self.config.model_dump(exclude={"crawler"}, exclude_none=True)
51
58
  async with open_search_proxy_client(
52
- timeout=float(self.config.timeout)
59
+ timeout=float(self.config.timeout),
60
+ context=self._request_context,
53
61
  ) as client:
54
62
  response = await client.crawl.crawl(
55
63
  urls=urls,
@@ -8,6 +8,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.context import LOCAL_REQUEST_CONTEXT, RequestContext
11
12
  from unique_search_proxy_core.crawlers.base import CrawlerType
12
13
  from unique_search_proxy_core.crawlers.basic.content_types import (
13
14
  CONTENT_TYPE_TOGGLE_TO_MIME,
@@ -54,8 +55,13 @@ class BasicConfig(CoreBasicConfig):
54
55
  config_display_name="Basic",
55
56
  )
56
57
  class BasicCrawler(BaseCrawler[BasicConfig]):
57
- def __init__(self, config: BasicConfig):
58
- super().__init__(config)
58
+ def __init__(
59
+ self,
60
+ config: BasicConfig,
61
+ *,
62
+ request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
63
+ ):
64
+ super().__init__(config, request_context=request_context)
59
65
 
60
66
  self.semaphore: asyncio.Semaphore = asyncio.Semaphore(
61
67
  self.config.max_concurrent_requests
@@ -69,7 +75,8 @@ class BasicCrawler(BaseCrawler[BasicConfig]):
69
75
  exclude_none=True,
70
76
  )
71
77
  async with open_search_proxy_client(
72
- timeout=float(self.config.timeout)
78
+ timeout=float(self.config.timeout),
79
+ context=self._request_context,
73
80
  ) as client:
74
81
  response = await client.crawl.crawl(
75
82
  urls=urls,
@@ -19,9 +19,6 @@ from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
19
19
  config_display_name="Firecrawl",
20
20
  )
21
21
  class FirecrawlCrawler(BaseCrawler[FirecrawlConfig]):
22
- def __init__(self, config: FirecrawlConfig):
23
- super().__init__(config)
24
-
25
22
  @override
26
23
  async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
27
24
  urls = [target.normalized_url for target in targets]
@@ -39,9 +39,6 @@ class ReaderResponse(BaseModel):
39
39
  config_display_name="Jina",
40
40
  )
41
41
  class JinaCrawler(BaseCrawler[JinaConfig]):
42
- def __init__(self, config: JinaConfig):
43
- super().__init__(config)
44
-
45
42
  @override
46
43
  async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
47
44
  urls = [target.normalized_url for target in targets]
@@ -6,6 +6,7 @@ from collections.abc import Callable
6
6
  from dataclasses import dataclass
7
7
 
8
8
  from pydantic import BaseModel
9
+ from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
9
10
  from unique_search_proxy_core.crawlers.base import CrawlerType
10
11
 
11
12
  from unique_web_search.services._registry import BaseSpec, Registry
@@ -37,9 +38,16 @@ def register_crawler(
37
38
  )
38
39
 
39
40
 
40
- def get_crawler_service(crawler_config: object):
41
+ def get_crawler_service(
42
+ crawler_config: object,
43
+ *,
44
+ request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
45
+ ):
41
46
  crawler = getattr(crawler_config, "crawler")
42
- return CRAWLER_REGISTRY[crawler].impl_cls(crawler_config)
47
+ return CRAWLER_REGISTRY[crawler].impl_cls(
48
+ crawler_config,
49
+ request_context=request_context,
50
+ )
43
51
 
44
52
 
45
53
  __all__ = [
@@ -18,9 +18,6 @@ from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
18
18
  config_display_name="Tavily",
19
19
  )
20
20
  class TavilyCrawler(BaseCrawler[TavilyConfig]):
21
- def __init__(self, config: TavilyConfig):
22
- super().__init__(config)
23
-
24
21
  @override
25
22
  async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
26
23
  urls = [target.normalized_url for target in targets]
@@ -2,12 +2,14 @@
2
2
 
3
3
  Executors orchestrate the **end-to-end research workflow** once a query or plan has been validated. They sit between:
4
4
 
5
- - **Search engines** (Google, Bing, VertexAI, etc.)
6
- - **Crawlers** (Basic, Tavily, Crawl4AI, …)
5
+ - **Search engines** (standard: Google, Brave, Perplexity, Custom API; agent/grounding: Bing, VertexAI)
6
+ - **Crawlers** (Basic, Tavily, Jina, Firecrawl)
7
7
  - **Content processing** (summaries, truncation, relevancy sorters)
8
8
  - **LLM interfaces** (query refinement, structured outputs)
9
9
  - **Progress reporters / logging**
10
10
 
11
+ **Search Proxy is transparent to executors:** when `SEARCH_PROXY_BASE_URL` is set, `SearchEngine.search` / `BaseCrawler.crawl` route via the proxy SDK; otherwise they use legacy direct calls. V1 / V2 / V3 do not branch on proxy mode.
12
+
11
13
  All executors extend the shared base class defined in `base_executor.py`.
12
14
 
13
15
  ```python
@@ -37,6 +39,7 @@ All steps push diagnostics into `WebSearchDebugInfo` for telemetry and debugging
37
39
  |----------|-----------|----------|----------------|-------------|-------|
38
40
  | **WebSearchV1Executor** | `WebSearchToolParameters` (single query + date restrictions) | Traditional search flows | LLM-based query refinement (Basic/Advanced) | Sequential (queries iterated) | Ideal for iterative refinement and single-shot queries |
39
41
  | **WebSearchV2Executor** | `WebSearchPlan` (multi-step plan) | Agentic / multi-step research | Executes explicit steps (search/read) | Parallel (steps scheduled concurrently) | Best for step-based plans generated by planning agents |
42
+ | **WebSearchV3Executor** | Per-call `search` or `read_urls` | Iterative agent research | Model chooses next cheap search vs full read | One command per tool call | Experimental; loop bounded by orchestrator iteration cap |
40
43
 
41
44
  ---
42
45
 
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import AsyncIterator
4
4
  from contextlib import asynccontextmanager
5
5
 
6
+ from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
6
7
  from unique_search_proxy_sdk import UniqueSearchProxyClient
7
8
 
8
9
  from unique_web_search.settings import env_settings
@@ -13,6 +14,7 @@ search_proxy_client_enabled = env_settings.search_proxy_base_url is not None
13
14
  @asynccontextmanager
14
15
  async def open_search_proxy_client(
15
16
  timeout: float,
17
+ context: RequestContext = LOCAL_REQUEST_CONTEXT,
16
18
  ) -> AsyncIterator[UniqueSearchProxyClient]:
17
19
  base_url = env_settings.search_proxy_base_url
18
20
  assert base_url is not None, "Unique Search Proxy base URL is not configured"
@@ -20,6 +22,7 @@ async def open_search_proxy_client(
20
22
  async with UniqueSearchProxyClient(
21
23
  base_url=base_url,
22
24
  timeout=timeout,
25
+ context=context,
23
26
  ) as client:
24
27
  yield client
25
28