unique_web_search 2026.26.0.dev8__tar.gz → 2026.26.0.dev10__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 (124) hide show
  1. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/PKG-INFO +3 -2
  2. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/pyproject.toml +4 -4
  3. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/client_settings.py +0 -27
  4. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/config.py +11 -0
  5. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/prompts/__init__.py +3 -0
  6. unique_web_search-2026.26.0.dev10/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +40 -0
  7. unique_web_search-2026.26.0.dev10/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +7 -0
  8. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/service.py +13 -1
  9. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/base.py +12 -3
  10. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/basic.py +19 -1
  11. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/crawl4ai.py +6 -2
  12. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/firecrawl.py +22 -2
  13. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/jina.py +29 -2
  14. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/tavily.py +22 -1
  15. unique_web_search-2026.26.0.dev10/src/unique_web_search/services/proxy/__init__.py +21 -0
  16. unique_web_search-2026.26.0.dev10/src/unique_web_search/services/proxy/bridge.py +30 -0
  17. unique_web_search-2026.26.0.dev10/src/unique_web_search/services/proxy/mappers.py +73 -0
  18. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/base.py +18 -7
  19. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/bing.py +30 -2
  20. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/brave.py +22 -3
  21. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/custom_api.py +7 -2
  22. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/firecrawl.py +7 -2
  23. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/google.py +70 -46
  24. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/jina.py +19 -18
  25. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/perplexity.py +32 -3
  26. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/tavily.py +7 -2
  27. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/vertexai.py +32 -3
  28. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/settings.py +1 -4
  29. unique_web_search-2026.26.0.dev8/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -39
  30. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/README.md +0 -0
  31. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/__init__.py +0 -0
  32. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/config.py +0 -0
  33. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/metrics.py +0 -0
  34. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/prompts.py +0 -0
  35. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/schema.py +0 -0
  36. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/service.py +0 -0
  37. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  38. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  39. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  40. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  41. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  42. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/client/proxy_config.py +0 -0
  43. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  44. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  45. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  46. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  47. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  48. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/config.py +0 -0
  49. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  50. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  51. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  52. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  53. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  54. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  55. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  56. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  57. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  58. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  59. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  60. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  61. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  62. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  63. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  64. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  65. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/content_processing/service.py +0 -0
  66. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/README.md +0 -0
  67. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  68. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  69. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/utils.py +0 -0
  70. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/README.md +0 -0
  71. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/__init__.py +0 -0
  72. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/base_config.py +0 -0
  73. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/base_executor.py +0 -0
  74. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/context.py +0 -0
  75. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  76. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/config.py +0 -0
  77. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/executor.py +0 -0
  78. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  79. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  80. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/prompts/restrict_date_description.j2 +0 -0
  81. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  82. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  83. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  84. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  85. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/config.py +0 -0
  86. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/executor.py +0 -0
  87. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  88. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  89. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  90. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  91. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  92. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  93. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v3/config.py +0 -0
  94. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  95. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  96. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  97. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  98. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  99. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/helpers.py +0 -0
  100. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/message_log.py +0 -0
  101. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/query_elicitation.py +0 -0
  102. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/README.md +0 -0
  103. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  104. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/schema.py +0 -0
  105. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  106. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  107. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  108. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  109. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  110. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
  111. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  112. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  113. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  114. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  115. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  116. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  117. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  118. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  119. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  120. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  121. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  122. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/structured_llm.py +0 -0
  123. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/skills/web-search-v3.md +0 -0
  124. {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-web-search
3
- Version: 2026.26.0.dev8
3
+ Version: 2026.26.0.dev10
4
4
  Summary:
5
5
  Author: Andreas Hauri, Gustav Hartz, Rami Azouz
6
6
  Author-email: Andreas Hauri <andreas@unique.ch>, Gustav Hartz <gustav.hartz.ext@unique.ch>, Rami Azouz <rami.ext@unique.ch>
@@ -28,7 +28,8 @@ Requires-Dist: certifi>=2025.11.12
28
28
  Requires-Dist: jinja2>=3.1.6
29
29
  Requires-Dist: playwright>=1.58.0
30
30
  Requires-Dist: perplexityai>=0.34.0
31
- Requires-Dist: unique-search-proxy-core>=2026.26.0.dev6,<2026.26.0rc0
31
+ Requires-Dist: unique-search-proxy-core>=2026.26.0.dev7,<2026.26.0rc0
32
+ Requires-Dist: unique-search-proxy-sdk>=2026.26.0.dev5,<2026.26.0rc0
32
33
  Requires-Python: >=3.12, <4.0
33
34
  Description-Content-Type: text/markdown
34
35
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_web_search"
3
- version = "2026.26.0.dev8"
3
+ version = "2026.26.0.dev10"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -34,8 +34,8 @@ dependencies = [
34
34
  "jinja2>=3.1.6",
35
35
  "playwright>=1.58.0",
36
36
  "perplexityai>=0.34.0",
37
- "unique-search-proxy-core>=2026.26.0.dev6,<2026.26.0rc0",
38
- # "unique-search-proxy-sdk>=2026.24.0.dev0",
37
+ "unique-search-proxy-core>=2026.26.0.dev7,<2026.26.0rc0",
38
+ "unique-search-proxy-sdk>=2026.26.0.dev5,<2026.26.0rc0",
39
39
  ]
40
40
 
41
41
  [build-system]
@@ -93,5 +93,5 @@ extend-select = ["I"]
93
93
 
94
94
  [tool.uv.sources]
95
95
  unique-toolkit = { workspace = true }
96
- # unique-search-proxy-sdk = { workspace = true }
96
+ unique-search-proxy-sdk = { workspace = true }
97
97
  unique-search-proxy-core = { workspace = true }
@@ -235,30 +235,3 @@ def get_perplexity_search_settings() -> PerplexitySearchSettings:
235
235
  if _perplexity_search_settings is None:
236
236
  _perplexity_search_settings = PerplexitySearchSettings.from_env_settings()
237
237
  return _perplexity_search_settings
238
-
239
-
240
- class SearchProxySettings(BaseModel):
241
- base_url: str | None = None
242
-
243
- @property
244
- def is_configured(self) -> bool:
245
- return self.base_url is not None
246
-
247
- @classmethod
248
- def from_env_settings(cls):
249
- if env_settings.search_proxy_base_url is None:
250
- _LOGGER.warning("Unique Search Proxy missing required setting: Base URL")
251
- else:
252
- _LOGGER.info("Unique Search Proxy is properly configured")
253
-
254
- return cls(base_url=env_settings.search_proxy_base_url)
255
-
256
-
257
- _search_proxy_settings: SearchProxySettings | None = None
258
-
259
-
260
- def get_search_proxy_settings() -> SearchProxySettings:
261
- global _search_proxy_settings
262
- if _search_proxy_settings is None:
263
- _search_proxy_settings = SearchProxySettings.from_env_settings()
264
- return _search_proxy_settings
@@ -42,6 +42,17 @@ class ArgumentScreeningConfig(BaseModel):
42
42
  title="Screening Guidelines",
43
43
  description="Rules the screening agent follows to decide whether arguments are safe. This is the primary field for administrators to customize.",
44
44
  )
45
+ organization_specific_blocked_keywords: Annotated[
46
+ list[str],
47
+ RJSFMetaTag({"ui:options": {"orderable": False}}),
48
+ ] = Field(
49
+ default_factory=list,
50
+ title="Organization-Specific Blocked Keywords",
51
+ description=(
52
+ "Terms, domains, product names, portals, or internal project names "
53
+ "that must always block WebSearch when detected."
54
+ ),
55
+ )
45
56
  system_prompt: Annotated[
46
57
  str,
47
58
  RJSFMetaTag.StringWidget.textarea(rows=4),
@@ -7,6 +7,9 @@ _PROMPTS_DIR = Path(__file__).parent
7
7
  DEFAULT_SYSTEM_PROMPT: str = load_template(_PROMPTS_DIR, "system_prompt.j2")
8
8
  DEFAULT_USER_PROMPT_TEMPLATE: str = load_template(_PROMPTS_DIR, "user_prompt.j2")
9
9
  DEFAULT_GUIDELINES: str = load_template(_PROMPTS_DIR, "guidelines.j2")
10
+ DEFAULT_GUIDELINES_WITH_KEYWORDS_TEMPLATE: str = load_template(
11
+ _PROMPTS_DIR, "guidelines_with_keywords.j2"
12
+ )
10
13
  DEFAULT_REJECTION_RESPONSE_TEMPLATE: str = load_template(
11
14
  _PROMPTS_DIR, "rejection_response.j2"
12
15
  )
@@ -0,0 +1,40 @@
1
+ Flag the request as "no-go" if the arguments would send client-identifying data (CID), bank-client relationship information, confidential financial information, credentials, or internal/private infrastructure details to WebSearch.
2
+
3
+ The goal is not general GDPR classification. The goal is to prevent clear-text data that could directly, indirectly, or potentially indirectly identify a private person or entity as a client, prospect, employee, counterparty, or related party of the organization from leaving the Unique environment through WebSearch.
4
+
5
+ When any violation is detected, stop immediately. Do not echo, summarize, transform, or reproduce the sensitive values. The reason must reference only the violated category, for example "client-identifying data", "bank-client relationship", "financial identifier", "blocked organization keyword", "credentials", or "internal infrastructure".
6
+
7
+ Apply all rules even when names, keywords, domains, or identifiers are misspelled, abbreviated, spaced out, hyphenated, split across words, or otherwise obfuscated.
8
+
9
+ ## Blocked By Default
10
+ Mark as "no-go" if the query contains any of the following:
11
+
12
+ - Direct identifiers of a private individual or private entity: full name, partial name with context, address, email address, phone number, date of birth, passport number, national ID, tax ID, social security number, driver's license number, employee ID, client ID, customer number, CRM ID, ticket ID, or any similar unique identifier.
13
+ - Financial identifiers: bank account number, IBAN, routing number, SWIFT/BIC tied to a person or account, credit/debit card number, portfolio number, custody account, mandate ID, transaction reference, payment reference, invoice number tied to a client, loan number, mortgage number, or insurance policy number.
14
+ - Bank-client relationship signals: any statement or question implying that a private person or private entity is a client, customer, prospect, account holder, beneficial owner, portfolio owner, mandate holder, borrower, investor, relationship-managed client, or onboarding/KYC subject.
15
+ - Financial context linked to an identifiable person or private entity: holdings, portfolio value, assets under management, transactions, trades, account balance, source of wealth, income, salary, compensation, creditworthiness, risk rating, KYC/AML status, sanctions/PEP screening, complaints, legal disputes, debt, collateral, or tax situation.
16
+ - Indirect or potentially indirect identification: combinations of attributes that could identify a client even without a name, such as date of birth plus nationality plus profession, location plus relationship manager plus portfolio size, branch plus rare occupation, company plus owner details, or any narrow combination of facts about a non-public person or private entity.
17
+ - Confidential business or non-public information: MNPI, trade secrets, internal reports, unpublished client lists, internal project names, confidential contracts, deal names, acquisition targets, or proprietary research.
18
+ - Authentication and secrets: passwords, PINs, API keys, tokens, private keys, recovery phrases, session cookies, authorization headers, or secret URLs.
19
+ - Internal or private infrastructure: private URLs, intranet hosts, internal IP addresses, VPN hosts, non-public service names, internal dashboards, private storage links, internal ticket links, or environment-specific system details.
20
+
21
+ ## Public Information That Can Be Allowed
22
+ Mark as "go" only when the query is clearly outside CID and does not include organization-specific blocked keywords:
23
+
24
+ - General knowledge, technical, legal, or regulatory questions not tied to an identifiable private person, private entity, or the organization.
25
+ - Public organizations, public products, market news, public events, or public company information where no private client relationship is implied.
26
+ - Public figures in their public capacity, such as politicians, executives, athletes, or public officials, when the query concerns publicly available public-role information and not private banking, personal financial, health, family, address, or contact details.
27
+ - Aggregated or anonymized statistics where no individual, client, small group, relationship manager, branch, or private entity can be singled out.
28
+
29
+ ## Ambiguity Rule
30
+ If it is unclear whether the query contains CID or implies a bank-client relationship, choose "no-go". WebSearch is an external disclosure path, so the safe default is to block ambiguous private-person or client-context queries.
31
+
32
+ ## Reason Style
33
+ Return a short category-only reason. Do not include raw names, account numbers, emails, phone numbers, domains, URLs, or any sensitive values in the reason.
34
+
35
+ ## Organization-Specific Blocked Keywords
36
+ Mark as "no-go" for any query containing organization-specific blocked terms or close variants, including misspellings, spacing, punctuation, hyphenation, domain variants, product names, internal project names, portal names, or other configured sensitive terms.
37
+
38
+ This keyword rule applies even if the rest of the query looks public or harmless. If no organization-specific terms are configured, ignore this section.
39
+
40
+ Configured blocked terms:
@@ -0,0 +1,7 @@
1
+ {{ guidelines.rstrip() }}
2
+
3
+ {% for keyword in organization_specific_blocked_keywords -%}
4
+ - {{ keyword }}
5
+ {% else -%}
6
+ - [none configured]
7
+ {% endfor %}
@@ -22,6 +22,9 @@ from unique_web_search.services.argument_screening.config import (
22
22
  from unique_web_search.services.argument_screening.exceptions import (
23
23
  ArgumentScreeningUnparseableResponseException,
24
24
  )
25
+ from unique_web_search.services.argument_screening.prompts import (
26
+ DEFAULT_GUIDELINES_WITH_KEYWORDS_TEMPLATE,
27
+ )
25
28
  from unique_web_search.services.message_log import WebSearchMessageLogger
26
29
  from unique_web_search.services.structured_llm import (
27
30
  StructuredLlmUnparseableResponseError,
@@ -92,9 +95,18 @@ class ArgumentScreeningService:
92
95
  _LOGGER.info("Running argument screening agent...")
93
96
 
94
97
  serialized_args = json.dumps(arguments, indent=2, default=str)
98
+ rendered_guidelines = Template(
99
+ DEFAULT_GUIDELINES_WITH_KEYWORDS_TEMPLATE
100
+ ).render(
101
+ guidelines=self._config.guidelines,
102
+ organization_specific_blocked_keywords=(
103
+ self._config.organization_specific_blocked_keywords
104
+ ),
105
+ )
106
+
95
107
  user_prompt = Template(self._config.user_prompt_template).render(
96
108
  arguments=serialized_args,
97
- guidelines=self._config.guidelines,
109
+ guidelines=rendered_guidelines,
98
110
  )
99
111
 
100
112
  try:
@@ -1,6 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from enum import StrEnum
3
- from typing import Generic, TypeVar
3
+ from typing import ClassVar, Generic, TypeVar
4
4
 
5
5
  from pydantic import BaseModel
6
6
  from unique_search_proxy_core.url_safety import (
@@ -17,6 +17,7 @@ from unique_web_search.services.helpers import (
17
17
  clean_model_title_generator,
18
18
  experimental_model_title_generator,
19
19
  )
20
+ from unique_web_search.services.proxy.bridge import search_proxy_client_enabled
20
21
 
21
22
 
22
23
  class CrawlerType(StrEnum):
@@ -52,17 +53,25 @@ CrawlerConfig = TypeVar(
52
53
 
53
54
 
54
55
  class BaseCrawler(ABC, Generic[CrawlerConfig]):
56
+ supports_proxy_crawl: ClassVar[bool] = False
57
+
55
58
  def __init__(self, config: CrawlerConfig):
56
59
  self.config = config
57
60
 
58
61
  async def crawl(self, urls: list[str]) -> list[str]:
62
+ if search_proxy_client_enabled and self.supports_proxy_crawl:
63
+ return await self._proxy_crawl(urls)
64
+
59
65
  try:
60
66
  targets = await UrlSafetyService.validate_batch_urls(urls)
61
67
  except CrawlTargetValidationError as exc:
62
68
  for target in exc.blocked_targets:
63
69
  crawl_blocked.labels(reason_category=target.category).inc()
64
70
  raise
65
- return await self._crawl(targets)
71
+ return await self._legacy_crawl(targets)
72
+
73
+ @abstractmethod
74
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]: ...
66
75
 
67
76
  @abstractmethod
68
- async def _crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]: ...
77
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]: ...
@@ -20,6 +20,10 @@ from unique_web_search.services.crawlers.url_safety import (
20
20
  ResolvedCrawlTarget,
21
21
  )
22
22
  from unique_web_search.services.crawlers.utils import get_random_user_agent
23
+ from unique_web_search.services.proxy.bridge import (
24
+ open_search_proxy_client,
25
+ )
26
+ from unique_web_search.services.proxy.mappers import map_crawl_response
23
27
 
24
28
  _LOGGER = logging.getLogger(__name__)
25
29
 
@@ -51,6 +55,8 @@ class BasicCrawlerConfig(BaseCrawlerConfig[CrawlerType.BASIC]):
51
55
 
52
56
 
53
57
  class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
58
+ supports_proxy_crawl = True
59
+
54
60
  def __init__(self, config: BasicCrawlerConfig):
55
61
  super().__init__(config)
56
62
 
@@ -63,7 +69,19 @@ class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
63
69
  # tags=["basic", "scrape"],
64
70
  # )
65
71
  @override
66
- async def _crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
72
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]:
73
+ async with open_search_proxy_client(
74
+ timeout=float(self.config.timeout),
75
+ ) as client:
76
+ response = await client.crawl.basic(
77
+ urls=urls,
78
+ timeout=int(self.config.timeout),
79
+ max_concurrent_requests=self.config.max_concurrent_requests,
80
+ )
81
+ return map_crawl_response(response, urls)
82
+
83
+ @override
84
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
67
85
  async with async_client(timeout=Timeout(self.config.timeout)) as client:
68
86
  tasks = [self._crawl_url_with_client(client, target) for target in targets]
69
87
  results = await asyncio.gather(*tasks, return_exceptions=True)
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from enum import StrEnum
3
- from typing import Any, Literal
3
+ from typing import Any, Literal, override
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
  from unique_toolkit.agentic.tools.config import get_configuration_dict
@@ -169,6 +169,9 @@ class Crawl4AiCrawler(BaseCrawler[Crawl4AiCrawlerConfig]):
169
169
  # @track(
170
170
  # tags=["crawl4ai", "scrape"],
171
171
  # )
172
+ @override
173
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]:
174
+ raise NotImplementedError("Crawl4AiCrawler does not support proxy crawl")
172
175
 
173
176
  @staticmethod
174
177
  def _get_ssrf_guard_hook():
@@ -209,7 +212,8 @@ class Crawl4AiCrawler(BaseCrawler[Crawl4AiCrawlerConfig]):
209
212
 
210
213
  return _ssrf_guard_hook
211
214
 
212
- async def _crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
215
+ @override
216
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
213
217
  urls = [target.normalized_url for target in targets]
214
218
  # Lazy import of crawl4ai - only import when actually needed
215
219
  from crawl4ai import (
@@ -1,4 +1,4 @@
1
- from typing import Literal
1
+ from typing import Literal, override
2
2
 
3
3
  from firecrawl import AsyncFirecrawl
4
4
 
@@ -11,6 +11,10 @@ from unique_web_search.services.crawlers.base import (
11
11
  CrawlerType,
12
12
  )
13
13
  from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
14
+ from unique_web_search.services.proxy.bridge import (
15
+ open_search_proxy_client,
16
+ )
17
+ from unique_web_search.services.proxy.mappers import map_crawl_response
14
18
 
15
19
 
16
20
  class FirecrawlCrawlerConfig(BaseCrawlerConfigExperimental[CrawlerType.FIRECRAWL]):
@@ -18,6 +22,8 @@ class FirecrawlCrawlerConfig(BaseCrawlerConfigExperimental[CrawlerType.FIRECRAWL
18
22
 
19
23
 
20
24
  class FirecrawlCrawler(BaseCrawler[FirecrawlCrawlerConfig]):
25
+ supports_proxy_crawl = True
26
+
21
27
  def __init__(self, config: FirecrawlCrawlerConfig):
22
28
  super().__init__(config)
23
29
 
@@ -25,8 +31,22 @@ class FirecrawlCrawler(BaseCrawler[FirecrawlCrawlerConfig]):
25
31
  # @track(
26
32
  # tags=["firecrawl", "scrape"],
27
33
  # )
28
- async def _crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
34
+ @override
35
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]:
36
+ async with open_search_proxy_client(
37
+ timeout=float(self.config.timeout),
38
+ ) as client:
39
+ response = await client.crawl.firecrawl(
40
+ urls=urls,
41
+ timeout=int(self.config.timeout),
42
+ only_main_content=True,
43
+ )
44
+ return map_crawl_response(response, urls)
45
+
46
+ @override
47
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
29
48
  urls = [target.normalized_url for target in targets]
49
+
30
50
  api_key = get_firecrawl_search_settings().api_key
31
51
  assert api_key is not None, "Firecrawl API key is not configured"
32
52
 
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
- from typing import Any, Literal
2
+ from typing import Any, Literal, cast, override
3
3
 
4
4
  from httpx import AsyncClient
5
5
  from pydantic import BaseModel, Field, HttpUrl
6
+ from unique_search_proxy_core.crawlers.jina.schema import JinaEngine, JinaReturnFormat
6
7
 
7
8
  from unique_web_search.client_settings import get_jina_search_settings
8
9
  from unique_web_search.services.crawlers.base import (
@@ -11,6 +12,10 @@ from unique_web_search.services.crawlers.base import (
11
12
  CrawlerType,
12
13
  )
13
14
  from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
15
+ from unique_web_search.services.proxy.bridge import (
16
+ open_search_proxy_client,
17
+ )
18
+ from unique_web_search.services.proxy.mappers import map_crawl_response
14
19
 
15
20
 
16
21
  class ReaderBody(BaseModel):
@@ -46,6 +51,8 @@ class JinaCrawlerConfig(BaseCrawlerConfigExperimental[CrawlerType.JINA]):
46
51
 
47
52
 
48
53
  class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
54
+ supports_proxy_crawl = True
55
+
49
56
  def __init__(self, config: JinaCrawlerConfig):
50
57
  super().__init__(config)
51
58
 
@@ -53,8 +60,28 @@ class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
53
60
  # @track(
54
61
  # tags=["jina", "scrape"],
55
62
  # )
56
- async def _crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
63
+ @override
64
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]:
65
+ headers = self.config.headers
66
+ async with open_search_proxy_client(
67
+ timeout=float(self.config.timeout),
68
+ ) as client:
69
+ response = await client.crawl.jina(
70
+ urls=urls,
71
+ timeout=int(self.config.timeout),
72
+ return_format=cast(
73
+ JinaReturnFormat,
74
+ headers.get("X-Return-Format", "markdown"),
75
+ ),
76
+ engine=cast(JinaEngine, headers.get("X-Engine", "browser")),
77
+ do_not_track=headers.get("DNT") == "1",
78
+ )
79
+ return map_crawl_response(response, urls)
80
+
81
+ @override
82
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
57
83
  urls = [target.normalized_url for target in targets]
84
+
58
85
  jina_settings = get_jina_search_settings()
59
86
  api_key = jina_settings.api_key
60
87
  assert api_key is not None, "Jina API key is not configured"
@@ -10,6 +10,10 @@ from unique_web_search.services.crawlers.base import (
10
10
  CrawlerType,
11
11
  )
12
12
  from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
13
+ from unique_web_search.services.proxy.bridge import (
14
+ open_search_proxy_client,
15
+ )
16
+ from unique_web_search.services.proxy.mappers import map_crawl_response
13
17
 
14
18
 
15
19
  class TavilyCrawlerConfig(BaseCrawlerConfigExperimental[CrawlerType.TAVILY]):
@@ -18,6 +22,8 @@ class TavilyCrawlerConfig(BaseCrawlerConfigExperimental[CrawlerType.TAVILY]):
18
22
 
19
23
 
20
24
  class TavilyCrawler(BaseCrawler[TavilyCrawlerConfig]):
25
+ supports_proxy_crawl = True
26
+
21
27
  def __init__(self, config: TavilyCrawlerConfig):
22
28
  super().__init__(config)
23
29
 
@@ -25,8 +31,23 @@ class TavilyCrawler(BaseCrawler[TavilyCrawlerConfig]):
25
31
  # @track(
26
32
  # tags=["tavily", "scrape"],
27
33
  # )
28
- async def _crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
34
+ async def _proxy_crawl(self, urls: list[str]) -> list[str]:
35
+ async with open_search_proxy_client(
36
+ timeout=float(self.config.timeout),
37
+ ) as client:
38
+ response = await client.crawl.tavily(
39
+ urls=urls,
40
+ timeout=int(self.config.timeout),
41
+ extract_depth=self.config.depth,
42
+ format="markdown",
43
+ include_images=False,
44
+ include_favicon=False,
45
+ )
46
+ return map_crawl_response(response, urls)
47
+
48
+ async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
29
49
  urls = [target.normalized_url for target in targets]
50
+
30
51
  api_key = get_tavily_search_settings().api_key
31
52
  assert api_key is not None, "Tavily API key is not configured"
32
53
 
@@ -0,0 +1,21 @@
1
+ from unique_web_search.services.proxy.bridge import (
2
+ open_search_proxy_client,
3
+ search_proxy_client_enabled,
4
+ )
5
+ from unique_web_search.services.proxy.mappers import (
6
+ agent_answer_text,
7
+ map_agent_answer,
8
+ map_crawl_response,
9
+ map_search_response,
10
+ result_to_markdown,
11
+ )
12
+
13
+ __all__ = [
14
+ "agent_answer_text",
15
+ "map_agent_answer",
16
+ "map_crawl_response",
17
+ "map_search_response",
18
+ "open_search_proxy_client",
19
+ "result_to_markdown",
20
+ "search_proxy_client_enabled",
21
+ ]
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+
6
+ from unique_search_proxy_sdk import UniqueSearchProxyClient
7
+
8
+ from unique_web_search.settings import env_settings
9
+
10
+ search_proxy_client_enabled = env_settings.search_proxy_base_url is not None
11
+
12
+
13
+ @asynccontextmanager
14
+ async def open_search_proxy_client(
15
+ timeout: float,
16
+ ) -> AsyncIterator[UniqueSearchProxyClient]:
17
+ base_url = env_settings.search_proxy_base_url
18
+ assert base_url is not None, "Unique Search Proxy base URL is not configured"
19
+
20
+ async with UniqueSearchProxyClient(
21
+ base_url=base_url,
22
+ timeout=timeout,
23
+ ) as client:
24
+ yield client
25
+
26
+
27
+ __all__ = [
28
+ "open_search_proxy_client",
29
+ "search_proxy_client_enabled",
30
+ ]
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from typing import TYPE_CHECKING
5
+
6
+ from unique_search_proxy_sdk._generated.models.agent_search_response import (
7
+ AgentSearchResponse,
8
+ )
9
+ from unique_search_proxy_sdk._generated.models.crawl_response import CrawlResponse
10
+ from unique_search_proxy_sdk._generated.models.crawl_url_result import CrawlUrlResult
11
+ from unique_search_proxy_sdk._generated.models.per_url_error import PerUrlError
12
+ from unique_search_proxy_sdk._generated.models.search_response import SearchResponse
13
+
14
+ if TYPE_CHECKING:
15
+ from unique_web_search.services.search_engine.schema import WebSearchResult
16
+ from unique_web_search.services.search_engine.utils.grounding.response_parsing import (
17
+ ResponseParser,
18
+ )
19
+
20
+
21
+ def map_search_response(response: SearchResponse) -> list[WebSearchResult]:
22
+ from unique_web_search.services.search_engine.schema import WebSearchResult
23
+
24
+ return [
25
+ WebSearchResult(
26
+ url=result.url,
27
+ title=result.title,
28
+ snippet=result.snippet,
29
+ content=result.content if isinstance(result.content, str) else "",
30
+ )
31
+ for result in response.curated
32
+ ]
33
+
34
+
35
+ _CRAWL_RESULT_TEMPLATE = "URL: {url}\n\n{body}"
36
+
37
+
38
+ def _crawl_result_body(result: CrawlUrlResult) -> str:
39
+ if isinstance(result.content, str):
40
+ return result.content
41
+
42
+ if isinstance(result.error, PerUrlError):
43
+ return f"Error: {result.error.message}"
44
+
45
+ return "Error: No content returned by search proxy"
46
+
47
+
48
+ def result_to_markdown(result: CrawlUrlResult) -> str:
49
+ return _CRAWL_RESULT_TEMPLATE.format(
50
+ url=result.url,
51
+ body=_crawl_result_body(result),
52
+ )
53
+
54
+
55
+ def map_crawl_response(response: CrawlResponse, urls: list[str]) -> list[str]:
56
+ return [result_to_markdown(result) for result in response.results]
57
+
58
+
59
+ async def map_agent_answer(
60
+ answer: str,
61
+ parsers: Sequence[ResponseParser],
62
+ ) -> list[WebSearchResult]:
63
+ from unique_web_search.services.search_engine.utils.grounding.response_parsing import (
64
+ convert_response_to_search_results,
65
+ )
66
+
67
+ return await convert_response_to_search_results(answer, list(parsers))
68
+
69
+
70
+ def agent_answer_text(response: AgentSearchResponse) -> str:
71
+ if isinstance(response.answer, str):
72
+ return response.answer
73
+ return ""
@@ -1,10 +1,11 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from enum import StrEnum
3
- from typing import Generic, TypeVar
3
+ from typing import ClassVar, Generic, TypeVar
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
6
  from unique_toolkit.agentic.tools.config import get_configuration_dict
7
7
 
8
+ from unique_web_search.services.proxy.bridge import search_proxy_client_enabled
8
9
  from unique_web_search.services.search_engine.schema import (
9
10
  WebSearchResult,
10
11
  )
@@ -104,16 +105,16 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
104
105
  """Base class for the search engine. It provides the common methods to search the web. This allows to use different search engines easily.
105
106
 
106
107
  Abstract Methods:
107
- _extract_urls: Clean the response from the search engine.
108
- _get_request_params: Get the request parameters.
108
+ _legacy_search: Direct search implementation (no proxy).
109
109
  requires_scraping: Whether the search engine requires scraping.
110
110
 
111
- Args:
112
- assistant: The assistant configuration.
113
- chat_manager: The chat manager.
111
+ Subclasses with proxy support set ``supports_proxy_search = True`` and
112
+ implement ``_proxy_search``.
114
113
 
115
114
  """
116
115
 
116
+ supports_proxy_search: ClassVar[bool] = False
117
+
117
118
  def __init__(self, config: SearchEngineConfig):
118
119
  self.config = config
119
120
 
@@ -122,6 +123,16 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
122
123
  def requires_scraping(self) -> bool:
123
124
  """Whether the search engine requires scraping."""
124
125
 
125
- @abstractmethod
126
126
  async def search(self, query: str, **kwargs) -> list[WebSearchResult]:
127
127
  """Search the web for the given query using the search engine."""
128
+ if search_proxy_client_enabled and self.supports_proxy_search:
129
+ return await self._proxy_search(query=query, **kwargs)
130
+ return await self._legacy_search(query=query, **kwargs)
131
+
132
+ @abstractmethod
133
+ async def _proxy_search(self, query: str, **kwargs) -> list[WebSearchResult]:
134
+ """Search the web via the search proxy."""
135
+
136
+ @abstractmethod
137
+ async def _legacy_search(self, query: str, **kwargs) -> list[WebSearchResult]:
138
+ """Search the web directly without the search proxy."""