unique-web-search 2026.28.0.dev2__tar.gz → 2026.28.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.28.0.dev2 → unique_web_search-2026.28.0.dev4}/PKG-INFO +4 -5
  2. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/pyproject.toml +4 -5
  3. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/config.py +1 -1
  4. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/service.py +1 -1
  5. unique_web_search-2026.28.0.dev4/src/unique_web_search/services/_registry.py +115 -0
  6. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/executor.py +4 -6
  7. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/executor.py +9 -10
  8. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v3/executor.py +7 -7
  9. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/README.md +47 -39
  10. unique_web_search-2026.28.0.dev4/src/unique_web_search/services/search_engine/__init__.py +85 -0
  11. unique_web_search-2026.28.0.dev4/src/unique_web_search/services/search_engine/base.py +62 -0
  12. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/bing.py +25 -43
  13. unique_web_search-2026.28.0.dev4/src/unique_web_search/services/search_engine/brave.py +64 -0
  14. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/custom_api.py +17 -11
  15. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/google.py +53 -29
  16. unique_web_search-2026.28.0.dev4/src/unique_web_search/services/search_engine/perplexity.py +62 -0
  17. unique_web_search-2026.28.0.dev4/src/unique_web_search/services/search_engine/registry.py +62 -0
  18. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/schema.py +6 -2
  19. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/vertexai.py +22 -38
  20. unique_web_search-2026.28.0.dev2/src/unique_web_search/services/search_engine/__init__.py +0 -161
  21. unique_web_search-2026.28.0.dev2/src/unique_web_search/services/search_engine/base.py +0 -138
  22. unique_web_search-2026.28.0.dev2/src/unique_web_search/services/search_engine/brave.py +0 -140
  23. unique_web_search-2026.28.0.dev2/src/unique_web_search/services/search_engine/firecrawl.py +0 -149
  24. unique_web_search-2026.28.0.dev2/src/unique_web_search/services/search_engine/jina.py +0 -288
  25. unique_web_search-2026.28.0.dev2/src/unique_web_search/services/search_engine/perplexity.py +0 -150
  26. unique_web_search-2026.28.0.dev2/src/unique_web_search/services/search_engine/tavily.py +0 -157
  27. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/README.md +0 -0
  28. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/__init__.py +0 -0
  29. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/client_settings.py +0 -0
  30. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/metrics.py +0 -0
  31. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/prompts.py +0 -0
  32. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/schema.py +0 -0
  33. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  34. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/config.py +0 -0
  35. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  36. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  37. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  38. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  39. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  40. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  41. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  42. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/argument_screening/service.py +0 -0
  43. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/client/proxy_config.py +0 -0
  44. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  45. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  46. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  47. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  48. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  49. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/config.py +0 -0
  50. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  51. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  52. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  53. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  54. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  55. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  56. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  57. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  58. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  59. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  60. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  61. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  62. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  63. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  64. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  65. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  66. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/content_processing/service.py +0 -0
  67. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/README.md +0 -0
  68. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  69. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/base.py +0 -0
  70. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/basic.py +0 -0
  71. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/crawl4ai.py +0 -0
  72. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  73. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/jina.py +0 -0
  74. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  75. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  76. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/crawlers/utils.py +0 -0
  77. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/README.md +0 -0
  78. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/__init__.py +0 -0
  79. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/base_config.py +0 -0
  80. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/base_executor.py +0 -0
  81. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/context.py +0 -0
  82. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  83. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/config.py +0 -0
  84. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  85. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  86. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/prompts/restrict_date_description.j2 +0 -0
  87. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  88. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  89. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  90. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  91. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/config.py +0 -0
  92. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  93. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  94. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  95. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  96. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  97. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  98. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v3/config.py +0 -0
  99. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  100. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  101. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  102. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  103. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/helpers.py +0 -0
  104. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/message_log.py +0 -0
  105. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/proxy/__init__.py +0 -0
  106. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/proxy/bridge.py +0 -0
  107. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/proxy/mappers.py +0 -0
  108. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/query_elicitation.py +0 -0
  109. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  110. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  111. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  112. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  113. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  114. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
  115. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  116. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  117. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  118. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  119. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  120. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  121. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  122. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  123. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  124. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  125. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  126. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/services/structured_llm.py +0 -0
  127. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/settings.py +0 -0
  128. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/src/unique_web_search/skills/web-search-v3.md +0 -0
  129. {unique_web_search-2026.28.0.dev2 → unique_web_search-2026.28.0.dev4}/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.28.0.dev2
3
+ Version: 2026.28.0.dev4
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>
@@ -17,7 +17,7 @@ Requires-Dist: tavily-python>=0.7.11,<1
17
17
  Requires-Dist: unidecode>=1.4.0,<2
18
18
  Requires-Dist: azure-ai-projects>=1.0.0,<2
19
19
  Requires-Dist: azure-identity>=1.25.0,<2
20
- Requires-Dist: unique-toolkit[monitoring]>=2026.28.0.dev5,<2026.28.0rc0
20
+ Requires-Dist: unique-toolkit[monitoring]>=2026.28.0.dev12,<2026.28.0rc0
21
21
  Requires-Dist: azure-core>=1.36.0,<2
22
22
  Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
23
23
  Requires-Dist: google-auth>=2.43.0,<3
@@ -27,9 +27,8 @@ Requires-Dist: httpx>=0.28.1
27
27
  Requires-Dist: certifi>=2025.11.12
28
28
  Requires-Dist: jinja2>=3.1.6
29
29
  Requires-Dist: playwright>=1.58.0
30
- Requires-Dist: perplexityai>=0.34.0
31
- Requires-Dist: unique-search-proxy-core>=2026.28.0.dev2,<2026.28.0rc0
32
- Requires-Dist: unique-search-proxy-sdk>=2026.28.0.dev2,<2026.28.0rc0
30
+ Requires-Dist: unique-search-proxy-core>=2026.28.0.dev3,<2026.28.0rc0
31
+ Requires-Dist: unique-search-proxy-sdk>=2026.28.0.dev3,<2026.28.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.28.0.dev2"
3
+ version = "2026.28.0.dev4"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -23,7 +23,7 @@ dependencies = [
23
23
  "unidecode>=1.4.0,<2",
24
24
  "azure-ai-projects>=1.0.0,<2",
25
25
  "azure-identity>=1.25.0,<2",
26
- "unique-toolkit[monitoring]>=2026.28.0.dev5,<2026.28.0rc0",
26
+ "unique-toolkit[monitoring]>=2026.28.0.dev12,<2026.28.0rc0",
27
27
  "azure-core>=1.36.0,<2",
28
28
  "google-cloud-aiplatform>=1.128.0,<2",
29
29
  "google-auth>=2.43.0,<3",
@@ -33,9 +33,8 @@ dependencies = [
33
33
  "certifi>=2025.11.12",
34
34
  "jinja2>=3.1.6",
35
35
  "playwright>=1.58.0",
36
- "perplexityai>=0.34.0",
37
- "unique-search-proxy-core>=2026.28.0.dev2,<2026.28.0rc0",
38
- "unique-search-proxy-sdk>=2026.28.0.dev2,<2026.28.0rc0",
36
+ "unique-search-proxy-core>=2026.28.0.dev3,<2026.28.0rc0",
37
+ "unique-search-proxy-sdk>=2026.28.0.dev3,<2026.28.0rc0",
39
38
  ]
40
39
 
41
40
  [build-system]
@@ -173,7 +173,7 @@ class WebSearchConfig(BaseToolConfig):
173
173
  default_factory=DefaultSearchEngine, # type: ignore (This type is computed at runtime so pyright is not able to infer it)
174
174
  title="Search Engine",
175
175
  description="Choose and configure which search engine to use for web searches (e.g. Google, Bing, Brave).",
176
- discriminator="search_engine_name",
176
+ discriminator="engine",
177
177
  )
178
178
 
179
179
  crawler_config: ActivatedCrawler = Field( # type: ignore (This type is computed at runtime so pyright is not able to infer it)
@@ -115,7 +115,7 @@ class WebSearchTool(Tool[WebSearchConfig]):
115
115
  """Derive the search-engine mode, respecting CustomAPI overrides."""
116
116
  cfg = self.search_engine_service.config
117
117
  override = cfg.search_engine_mode if isinstance(cfg, CustomAPIConfig) else None
118
- return get_search_engine_mode(cfg.search_engine_name, override=override)
118
+ return get_search_engine_mode(cfg.engine, override=override)
119
119
 
120
120
  @override
121
121
  def tool_description(self) -> LanguageModelToolDescription:
@@ -0,0 +1,115 @@
1
+ """Generic self-registration registry for pluggable service implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import operator
7
+ import pkgutil
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from functools import reduce
11
+ from typing import Any, Callable, Generic, Iterable, TypeVar
12
+
13
+ from pydantic import BaseModel
14
+
15
+ K = TypeVar("K", bound=Enum)
16
+ S = TypeVar("S", bound="BaseSpec[Any]")
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class BaseSpec(Generic[K]):
21
+ """Metadata shared by every registered implementation."""
22
+
23
+ name: str
24
+ key: K
25
+ config_cls: type[BaseModel]
26
+ impl_cls: type
27
+
28
+
29
+ class Registry(Generic[K, S]):
30
+ """Collects implementation specs keyed by a discriminator enum value."""
31
+
32
+ def __init__(self, spec_cls: type[S]) -> None:
33
+ self._spec_cls = spec_cls
34
+ self._by_key: dict[K, S] = {}
35
+
36
+ def register(
37
+ self,
38
+ *,
39
+ name: str,
40
+ key: K,
41
+ config_cls: type[BaseModel],
42
+ **extra: Any,
43
+ ) -> Callable[[type], type]:
44
+ def deco(impl_cls: type) -> type:
45
+ if key in self._by_key:
46
+ raise ValueError(f"{key!r} already registered")
47
+ self._by_key[key] = self._spec_cls(
48
+ name=name,
49
+ key=key,
50
+ config_cls=config_cls,
51
+ impl_cls=impl_cls,
52
+ **extra,
53
+ )
54
+ return impl_cls
55
+
56
+ return deco
57
+
58
+ @property
59
+ def specs(self) -> tuple[S, ...]:
60
+ return tuple(self._by_key.values())
61
+
62
+ def __getitem__(self, key: K) -> S:
63
+ return self._by_key[key]
64
+
65
+ def by_name(self) -> dict[str, S]:
66
+ return {spec.name: spec for spec in self._by_key.values()}
67
+
68
+ def name_to_config(self) -> dict[str, type[BaseModel]]:
69
+ return {spec.name: spec.config_cls for spec in self._by_key.values()}
70
+
71
+ def config_types_from_names(self, names: list[str]) -> type[BaseModel]:
72
+ assert len(names) >= 1, "At least one name must be provided"
73
+
74
+ by_name = self.by_name()
75
+ selected_types = [
76
+ by_name[name.lower()].config_cls
77
+ for name in names
78
+ if name.lower() in by_name
79
+ ]
80
+ if not selected_types:
81
+ raise ValueError(f"No config found for names: {names}")
82
+ if len(selected_types) == 1:
83
+ return selected_types[0]
84
+ return reduce(operator.or_, selected_types)
85
+
86
+ def default_config(self, names: list[str]) -> type[BaseModel]:
87
+ assert len(names) >= 1, "At least one name must be provided"
88
+ return self.by_name()[names[0].lower()].config_cls
89
+
90
+ def autodiscover(
91
+ self,
92
+ path: Iterable[str],
93
+ package: str,
94
+ *,
95
+ exclude: frozenset[str] | set[str] = frozenset(),
96
+ ) -> None:
97
+ for info in pkgutil.iter_modules(path):
98
+ if info.ispkg or info.name.startswith("_") or info.name in exclude:
99
+ continue
100
+ importlib.import_module(f"{package}.{info.name}")
101
+
102
+ def assert_enum_coverage(
103
+ self,
104
+ *enums: type[Enum],
105
+ exempt: frozenset[Enum] | set[Enum] = frozenset(),
106
+ ) -> None:
107
+ registered = set(self._by_key.keys())
108
+ for enum_cls in enums:
109
+ for member in enum_cls:
110
+ if member in exempt:
111
+ continue
112
+ if member not in registered:
113
+ raise AssertionError(
114
+ f"{enum_cls.__name__}.{member.name} has no registry entry"
115
+ )
@@ -272,9 +272,9 @@ class WebSearchV1Executor(BaseWebSearchExecutor[WebSearchToolParameters]):
272
272
  async def _search(
273
273
  self, query: str, date_restrict: str | None
274
274
  ) -> list[WebSearchResult]:
275
- engine = self.search_service.config.search_engine_name.value
275
+ engine = self.search_service.config.engine.value
276
276
  start_time = time()
277
- _LOGGER.info(f"Company {self.company_id} Searching with {self.search_service}")
277
+ _LOGGER.info(f"Company {self.company_id} Searching with {engine}")
278
278
  with metric_scope(search_duration, search_errors, engine=engine):
279
279
  search_total.labels(engine=engine).inc()
280
280
  search_results = await self.search_service.search(
@@ -282,14 +282,12 @@ class WebSearchV1Executor(BaseWebSearchExecutor[WebSearchToolParameters]):
282
282
  )
283
283
  end_time = time()
284
284
  delta_time = end_time - start_time
285
- _LOGGER.info(
286
- f"Searched with {self.search_service} completed in {delta_time} seconds"
287
- )
285
+ _LOGGER.info(f"Searched with {engine} completed in {delta_time} seconds")
288
286
  self.debug_info.steps.append(
289
287
  StepDebugInfo(
290
288
  step_name="search",
291
289
  execution_time=delta_time,
292
- config=self.search_service.config.search_engine_name.name,
290
+ config=self.search_service.config.engine.name,
293
291
  extra={
294
292
  "query": query,
295
293
  "date_restrict": date_restrict,
@@ -119,14 +119,15 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
119
119
  )
120
120
  )
121
121
 
122
- engine = self.search_service.config.search_engine_name.value
122
+ engine = self.search_service.config.engine.value
123
123
  time_start = time()
124
- _LOGGER.info(f"Company {self.company_id} Searching with {self.search_service}")
124
+ _LOGGER.info(f"Company {self.company_id} Searching with {engine}")
125
125
 
126
126
  await self._message_log_callback.log_queries([step.query_or_url])
127
127
  with metric_scope(search_duration, search_errors, engine=engine):
128
128
  search_total.labels(engine=engine).inc()
129
129
  results = await self.search_service.search(step.query_or_url)
130
+
130
131
  await self._message_log_callback.log_web_search_results(results)
131
132
 
132
133
  delta_time = time() - time_start
@@ -135,7 +136,7 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
135
136
  StepDebugInfo(
136
137
  step_name=f"{step_name}.search",
137
138
  execution_time=delta_time,
138
- config=self.search_service.config.search_engine_name.name,
139
+ config=engine,
139
140
  extra={
140
141
  "query": step.query_or_url,
141
142
  "number_of_results": len(results),
@@ -144,9 +145,7 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
144
145
  )
145
146
  )
146
147
 
147
- _LOGGER.info(
148
- f"Searched with {self.search_service} completed in {delta_time} seconds"
149
- )
148
+ _LOGGER.info(f"Searched with {engine} completed in {delta_time} seconds")
150
149
 
151
150
  if self.search_service.requires_scraping and results:
152
151
  results = await self._crawl_search_results(step_name, results)
@@ -159,7 +158,7 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
159
158
  """Crawl URLs for the given search results and fill content. Call when results non-empty."""
160
159
  crawler = self.crawler_service.config.crawler_type.value
161
160
  time_start = time()
162
- _LOGGER.info(f"Company {self.company_id} Crawling with {self.crawler_service}")
161
+ _LOGGER.info(f"Company {self.company_id} Crawling with {crawler}")
163
162
  with metric_scope(crawl_duration, crawl_errors, crawler=crawler):
164
163
  crawl_results = await self.crawler_service.crawl(
165
164
  [result.url for result in results]
@@ -172,7 +171,7 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
172
171
  StepDebugInfo(
173
172
  step_name=f"{step_name}.crawl",
174
173
  execution_time=delta_time,
175
- config=self.crawler_service.config.crawler_type.name,
174
+ config=crawler,
176
175
  extra={
177
176
  "number_of_results": len(results),
178
177
  "contents": [result.model_dump() for result in results],
@@ -180,7 +179,7 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
180
179
  )
181
180
  )
182
181
  _LOGGER.info(
183
- f"Crawled {len(results)} pages with {self.crawler_service} completed in {delta_time} seconds"
182
+ f"Crawled {len(results)} pages with {crawler} completed in {delta_time} seconds"
184
183
  )
185
184
  return results
186
185
 
@@ -217,7 +216,7 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
217
216
  StepDebugInfo(
218
217
  step_name=f"{step_name}.crawl",
219
218
  execution_time=delta_time,
220
- config=self.crawler_service.config.crawler_type.name,
219
+ config=crawler,
221
220
  extra={
222
221
  "url": step.query_or_url,
223
222
  "content": results,
@@ -95,11 +95,9 @@ class WebSearchV3Executor(BaseWebSearchExecutor[WebSearchV3ToolParameters]):
95
95
  )
96
96
  )
97
97
 
98
- engine = self.search_service.config.search_engine_name.value
98
+ engine = self.search_service.config.engine.value
99
99
  time_start = time()
100
- _LOGGER.info(
101
- "Company %s Searching with %s", self.company_id, self.search_service
102
- )
100
+ _LOGGER.info(f"Company {self.company_id} Searching with {engine}")
103
101
 
104
102
  await self._message_log_callback.log_queries([query])
105
103
  with metric_scope(search_duration, search_errors, engine=engine):
@@ -112,7 +110,7 @@ class WebSearchV3Executor(BaseWebSearchExecutor[WebSearchV3ToolParameters]):
112
110
  StepDebugInfo(
113
111
  step_name="SEARCH.search",
114
112
  execution_time=delta_time,
115
- config=self.search_service.config.search_engine_name.name,
113
+ config=engine,
116
114
  extra={
117
115
  "query": query,
118
116
  "number_of_results": len(results),
@@ -183,7 +181,9 @@ class WebSearchV3Executor(BaseWebSearchExecutor[WebSearchV3ToolParameters]):
183
181
  urls = list(urls)
184
182
  crawler = self.crawler_service.config.crawler_type.value
185
183
  time_start = time()
186
- _LOGGER.info("Company %s Crawling %s URLs", self.company_id, len(urls))
184
+ _LOGGER.info(
185
+ f"Company {self.company_id} Crawling {len(urls)} URLs with {crawler}"
186
+ )
187
187
  await self._message_log_callback.log_queries(urls)
188
188
  with metric_scope(crawl_duration, crawl_errors, crawler=crawler):
189
189
  contents = await self.crawler_service.crawl(urls)
@@ -199,7 +199,7 @@ class WebSearchV3Executor(BaseWebSearchExecutor[WebSearchV3ToolParameters]):
199
199
  StepDebugInfo(
200
200
  step_name="FETCH_URLS.crawl",
201
201
  execution_time=delta_time,
202
- config=self.crawler_service.config.crawler_type.name,
202
+ config=crawler,
203
203
  extra={"urls": urls, "number_of_results": len(results)},
204
204
  )
205
205
  )
@@ -67,19 +67,17 @@ GOOGLE_SEARCH_ENGINE_ID=your_search_engine_id
67
67
  #### Configuration
68
68
 
69
69
  ```python
70
- from unique_web_search.services.search_engine.google import GoogleConfig, GoogleSearch
71
- from unique_web_search.services.search_engine.utils.google.schema import GoogleSearchOptionalQueryParams
70
+ from unique_search_proxy_core.search_engines.google.schema import GoogleConfig
71
+ from unique_search_proxy_core.param_policy.exposable_param import ExposableParam
72
+ from unique_web_search.services.search_engine.google import GoogleSearch
72
73
 
73
74
  config = GoogleConfig(
74
- search_engine_name=SearchEngineType.GOOGLE,
75
- fetch_size=10, # Number of results to fetch
76
- custom_search_config=GoogleSearchOptionalQueryParams(
77
- safe="active", # Safe search: off, active, high
78
- cr="countryUS", # Country restriction
79
- lr="lang_en", # Language restriction
80
- siteSearch="example.com", # Limit to specific site
81
- dateRestrict="m1" # Results from last month
82
- )
75
+ fetch_size=10,
76
+ search_engine_id="your-search-engine-id", # optional; env default when unset
77
+ safe="active",
78
+ gl=ExposableParam(expose=False, value="us"),
79
+ site_search=ExposableParam(expose=False, value="example.com"),
80
+ date_restrict=ExposableParam(expose=False, value="m1"),
83
81
  )
84
82
 
85
83
  search = GoogleSearch(config)
@@ -140,7 +138,6 @@ USE_UNIQUE_PRIVATE_ENDPOINT_TRANSPORT=false
140
138
  from unique_web_search.services.search_engine.bing import BingSearchConfig, BingSearch
141
139
 
142
140
  config = BingSearchConfig(
143
- search_engine_name=SearchEngineType.BING,
144
141
  agent_id="your-agent-id", # Azure AI Agent ID
145
142
  endpoint="https://your-project.openai.azure.com", # Azure AI Project endpoint
146
143
  requires_scraping=False # Grounding returns full content
@@ -206,12 +203,10 @@ BRAVE_SEARCH_API_KEY=your_brave_api_key
206
203
  #### Configuration
207
204
 
208
205
  ```python
209
- from unique_web_search.services.search_engine.brave import BraveSearchConfig, BraveSearch
206
+ from unique_search_proxy_core.search_engines.brave.schema import BraveConfig
207
+ from unique_web_search.services.search_engine.brave import BraveSearch
210
208
 
211
- config = BraveSearchConfig(
212
- search_engine_name=SearchEngineType.BRAVE,
213
- fetch_size=10
214
- )
209
+ config = BraveConfig(fetch_size=10)
215
210
 
216
211
  search = BraveSearch(config)
217
212
  results = await search.search("privacy-focused search")
@@ -384,9 +379,8 @@ results = await search.search("dynamic web content")
384
379
  from unique_web_search.services.search_engine.vertexai import VertexAIConfig, VertexAI
385
380
 
386
381
  config = VertexAIConfig(
387
- search_engine_name=SearchEngineType.VERTEXAI,
388
- model_name="gemini-2.5-flash", # or "gemini-2.5-pro"
389
- grounding_system_instruction="Provide detailed technical answers with citations",
382
+ vertexai_model_name="gemini-3-flash-preview",
383
+ generation_instructions="Provide detailed technical answers with citations",
390
384
  enable_entreprise_search=False, # Use enterprise search if available
391
385
  enable_redirect_resolution=True, # Resolve URL redirects
392
386
  requires_scraping=False
@@ -459,7 +453,6 @@ from unique_web_search.services.search_engine.custom_api import CustomAPIConfig,
459
453
  from unique_web_search.settings import CUSTOM_API_REQUEST_METHOD
460
454
 
461
455
  config = CustomAPIConfig(
462
- search_engine_name=SearchEngineType.CUSTOM_API,
463
456
  api_endpoint="https://your-api.example.com/search",
464
457
  api_request_method=CUSTOM_API_REQUEST_METHOD.GET,
465
458
  api_headers={"X-API-Key": "your_api_key"},
@@ -481,7 +474,6 @@ Headers: X-API-Key: your_api_key
481
474
  **POST Request Example:**
482
475
  ```python
483
476
  config = CustomAPIConfig(
484
- search_engine_name=SearchEngineType.CUSTOM_API,
485
477
  api_endpoint="https://your-api.example.com/search",
486
478
  api_request_method=CUSTOM_API_REQUEST_METHOD.POST,
487
479
  api_headers={
@@ -529,15 +521,11 @@ Body:
529
521
 
530
522
  ## Common Configuration
531
523
 
532
- All search engines support these common configuration options:
524
+ Search engine configs use a discriminated union on the `engine` field:
533
525
 
534
- ### BaseSearchEngineConfig
535
-
536
- ```python
537
- class BaseSearchEngineConfig:
538
- search_engine_name: SearchEngineType # Required
539
- fetch_size: int = 5 # Number of results to fetch (where applicable)
540
- ```
526
+ - **Google, Brave, Perplexity**: import config types directly from `unique_search_proxy_core`
527
+ - **Bing, VertexAI**: extend core agent configs (`BingAgentConfig`, `VertexAIAgentConfig`) with web-only fields
528
+ - **Custom API**: standalone `BaseModel` with `engine: Literal[SearchEngineType.CUSTOM_API]`
541
529
 
542
530
  ### Factory Pattern
543
531
 
@@ -667,19 +655,37 @@ except Exception as e:
667
655
  To add a new search engine:
668
656
 
669
657
  1. Create a new file in this directory (e.g., `newsearchengine.py`)
670
- 2. Implement the configuration class inheriting from `BaseSearchEngineConfig`
658
+ 2. If the engine exists in `unique_search_proxy_core`, extend its config with web-only fields; otherwise define a `BaseModel` with `engine: Literal[SearchEngineType.NEW]`
671
659
  3. Implement the search engine class inheriting from `SearchEngine`
672
- 4. Add the search engine type to `SearchEngineType` enum in `base.py`
673
- 5. Register it in `__init__.py`'s factory function
660
+ 4. Register the engine with `@register_search_engine(...)` on the class — no `__init__.py` edit needed (modules are auto-discovered via `pkgutil`)
661
+ 5. Add the config and implementation types to the static `SearchEngineConfigTypes` / `SearchEngineTypes` unions in `__init__.py` (required for pyright; guarded by `tests/test_search_engine_registry.py`)
674
662
  6. Add tests in `tests/test_search_engines.py`
675
663
 
676
- Example template:
664
+ The shared registration machinery lives in [`services/_registry.py`](../_registry.py); crawlers will reuse the same core in a future migration.
665
+
666
+ Example template (web-only engine):
677
667
 
678
668
  ```python
679
- class NewSearchEngineConfig(BaseSearchEngineConfig[SearchEngineType.NEW]):
680
- search_engine_name: Literal[SearchEngineType.NEW] = SearchEngineType.NEW
669
+ from unique_search_proxy_core.search_engines.base import SearchEngineType
670
+ from unique_toolkit.agentic.tools.config import get_configuration_dict
671
+
672
+ from unique_web_search.services.search_engine.base import SearchEngine, SearchEngineMode
673
+ from unique_web_search.services.search_engine.registry import register_search_engine
674
+
675
+ class NewSearchEngineConfig(BaseModel):
676
+ model_config = get_configuration_dict(title="New Search Engine")
677
+
678
+ engine: Literal[SearchEngineType.NEW] = SearchEngineType.NEW
679
+ fetch_size: int = Field(default=5, ...)
681
680
  # Add custom configuration fields
682
681
 
682
+ @register_search_engine(
683
+ name="new",
684
+ key=SearchEngineType.NEW,
685
+ config_cls=NewSearchEngineConfig,
686
+ mode=SearchEngineMode.STANDARD,
687
+ config_display_name="New Search Engine",
688
+ )
683
689
  class NewSearchEngine(SearchEngine[NewSearchEngineConfig]):
684
690
  def __init__(self, config: NewSearchEngineConfig):
685
691
  super().__init__(config)
@@ -689,9 +695,11 @@ class NewSearchEngine(SearchEngine[NewSearchEngineConfig]):
689
695
  def requires_scraping(self) -> bool:
690
696
  return False # or True, depending on your implementation
691
697
 
692
- async def search(self, query: str, **kwargs) -> list[WebSearchResult]:
693
- # Implement search logic
694
- pass
698
+ async def _proxy_search(self, query: str, **kwargs) -> list[WebSearchResult]:
699
+ ...
700
+
701
+ async def _legacy_search(self, query: str, **kwargs) -> list[WebSearchResult]:
702
+ ...
695
703
  ```
696
704
 
697
705
  ## Performance Considerations
@@ -0,0 +1,85 @@
1
+ from pydantic import BaseModel
2
+
3
+ from unique_web_search.services.search_engine.base import (
4
+ SearchEngine,
5
+ SearchEngineMode,
6
+ )
7
+ from unique_web_search.services.search_engine.registry import (
8
+ SEARCH_ENGINE_REGISTRY,
9
+ get_search_engine_mode,
10
+ get_search_engine_service,
11
+ )
12
+
13
+ SEARCH_ENGINE_REGISTRY.autodiscover(__path__, __name__, exclude=frozenset({"schema"}))
14
+
15
+ from unique_search_proxy_core.search_engines import ( # noqa: E402
16
+ BraveConfig,
17
+ GoogleConfig,
18
+ PerplexityConfig,
19
+ SearchEngineType,
20
+ )
21
+
22
+ from unique_web_search.services.search_engine.bing import ( # noqa: E402
23
+ BingSearch,
24
+ BingSearchConfig,
25
+ )
26
+ from unique_web_search.services.search_engine.brave import BraveSearch # noqa: E402
27
+ from unique_web_search.services.search_engine.custom_api import ( # noqa: E402
28
+ CustomAPI,
29
+ CustomAPIConfig,
30
+ )
31
+ from unique_web_search.services.search_engine.google import GoogleSearch # noqa: E402
32
+ from unique_web_search.services.search_engine.perplexity import ( # noqa: E402
33
+ PerplexitySearch,
34
+ )
35
+ from unique_web_search.services.search_engine.vertexai import ( # noqa: E402
36
+ VertexAI,
37
+ VertexAIConfig,
38
+ )
39
+
40
+ SearchEngineTypes = (
41
+ GoogleSearch | BingSearch | CustomAPI | BraveSearch | PerplexitySearch | VertexAI
42
+ )
43
+ SearchEngineConfigTypes = (
44
+ GoogleConfig
45
+ | BingSearchConfig
46
+ | CustomAPIConfig
47
+ | BraveConfig
48
+ | PerplexityConfig
49
+ | VertexAIConfig
50
+ )
51
+
52
+ ENGINE_NAME_TO_CONFIG = SEARCH_ENGINE_REGISTRY.name_to_config()
53
+
54
+
55
+ def get_search_engine_config_types_from_names(
56
+ engine_names: list[str],
57
+ ) -> type[BaseModel]:
58
+ return SEARCH_ENGINE_REGISTRY.config_types_from_names(engine_names)
59
+
60
+
61
+ def get_default_search_engine_config(
62
+ engine_names: list[str],
63
+ ) -> type[BaseModel]:
64
+ return SEARCH_ENGINE_REGISTRY.default_config(engine_names)
65
+
66
+
67
+ __all__ = [
68
+ "SearchEngineMode",
69
+ "SearchEngineType",
70
+ "get_search_engine_mode",
71
+ "GoogleConfig",
72
+ "GoogleSearch",
73
+ "BingSearchConfig",
74
+ "BingSearch",
75
+ "BraveSearch",
76
+ "BraveConfig",
77
+ "PerplexitySearch",
78
+ "PerplexityConfig",
79
+ "get_search_engine_service",
80
+ "SearchEngine",
81
+ "get_search_engine_config_types_from_names",
82
+ "get_default_search_engine_config",
83
+ "VertexAIConfig",
84
+ "VertexAI",
85
+ ]
@@ -0,0 +1,62 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import StrEnum
3
+ from typing import ClassVar, Generic, TypeVar
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from unique_web_search.services.proxy.bridge import search_proxy_client_enabled
8
+ from unique_web_search.services.search_engine.schema import (
9
+ WebSearchResult,
10
+ )
11
+
12
+
13
+ class SearchEngineMode(StrEnum):
14
+ STANDARD = "standard"
15
+ AGENT = "agent"
16
+
17
+
18
+ class LocalSearchEngineType(StrEnum):
19
+ CUSTOM_API = "custom_api"
20
+
21
+
22
+ SearchEngineConfig = TypeVar(
23
+ "SearchEngineConfig",
24
+ bound=BaseModel,
25
+ )
26
+
27
+
28
+ class SearchEngine(ABC, Generic[SearchEngineConfig]):
29
+ """Base class for the search engine. It provides the common methods to search the web. This allows to use different search engines easily.
30
+
31
+ Abstract Methods:
32
+ _legacy_search: Direct search implementation (no proxy).
33
+ requires_scraping: Whether the search engine requires scraping.
34
+
35
+ Subclasses with proxy support set ``supports_proxy_search = True`` and
36
+ implement ``_proxy_search``.
37
+
38
+ """
39
+
40
+ supports_proxy_search: ClassVar[bool] = False
41
+
42
+ def __init__(self, config: SearchEngineConfig):
43
+ self.config = config
44
+
45
+ @property
46
+ def requires_scraping(self) -> bool:
47
+ """Whether the search engine requires scraping."""
48
+ return False
49
+
50
+ async def search(self, query: str, **kwargs) -> list[WebSearchResult]:
51
+ """Search the web for the given query using the search engine."""
52
+ if search_proxy_client_enabled and self.supports_proxy_search:
53
+ return await self._proxy_search(query=query, **kwargs)
54
+ return await self._legacy_search(query=query, **kwargs)
55
+
56
+ @abstractmethod
57
+ async def _proxy_search(self, query: str, **kwargs) -> list[WebSearchResult]:
58
+ """Search the web via the search proxy."""
59
+
60
+ @abstractmethod
61
+ async def _legacy_search(self, query: str, **kwargs) -> list[WebSearchResult]:
62
+ """Search the web directly without the search proxy."""