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.
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/PKG-INFO +3 -2
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/pyproject.toml +4 -4
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/client_settings.py +0 -27
- {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
- {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
- unique_web_search-2026.26.0.dev10/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +40 -0
- unique_web_search-2026.26.0.dev10/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +7 -0
- {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
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/base.py +12 -3
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/basic.py +19 -1
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/crawl4ai.py +6 -2
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/firecrawl.py +22 -2
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/jina.py +29 -2
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/tavily.py +22 -1
- unique_web_search-2026.26.0.dev10/src/unique_web_search/services/proxy/__init__.py +21 -0
- unique_web_search-2026.26.0.dev10/src/unique_web_search/services/proxy/bridge.py +30 -0
- unique_web_search-2026.26.0.dev10/src/unique_web_search/services/proxy/mappers.py +73 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/settings.py +1 -4
- unique_web_search-2026.26.0.dev8/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -39
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/README.md +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/config.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/prompts.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/service.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/README.md +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/__init__.py +0 -0
- {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
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/README.md +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/__init__.py +0 -0
- {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
- {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
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/executors/context.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/query_elicitation.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {unique_web_search-2026.26.0.dev8 → unique_web_search-2026.26.0.dev10}/src/unique_web_search/services/structured_llm.py +0 -0
- {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
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
38
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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=
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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."""
|