unique_web_search 2026.30.0.dev3__tar.gz → 2026.30.0.dev4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unique_web_search-2026.30.0.dev4/PKG-INFO +146 -0
- unique_web_search-2026.30.0.dev4/README.md +114 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/pyproject.toml +4 -4
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/service.py +13 -3
- unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/README.md +110 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/base.py +10 -2
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/basic.py +10 -3
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/firecrawl.py +0 -3
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/jina.py +0 -3
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/registry.py +10 -2
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/tavily.py +0 -3
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/README.md +5 -2
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/bridge.py +3 -0
- unique_web_search-2026.30.0.dev4/src/unique_web_search/services/search_engine/README.md +192 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/base.py +14 -3
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/bing.py +4 -1
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/custom_api.py +8 -2
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/registry.py +12 -2
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/vertexai.py +4 -1
- unique_web_search-2026.30.0.dev3/PKG-INFO +0 -146
- unique_web_search-2026.30.0.dev3/README.md +0 -114
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/README.md +0 -58
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/search_engine/README.md +0 -715
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/client_settings.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/_registry.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/service.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/client/proxy_config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/service.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_executor.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/context.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/modes.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/executor.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/executor.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/config.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/executor.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/mappers.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/query_elicitation.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/brave.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/google.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/service.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/structured_llm.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/settings.py +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/skills/web-search-v3.md +0 -0
- {unique_web_search-2026.30.0.dev3 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/utils.py +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: unique-web-search
|
|
3
|
+
Version: 2026.30.0.dev4
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Andreas Hauri, Gustav Hartz, Rami Azouz
|
|
6
|
+
Author-email: Andreas Hauri <andreas@unique.ch>, Gustav Hartz <gustav.hartz.ext@unique.ch>, Rami Azouz <rami.ext@unique.ch>
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Requires-Dist: typing-extensions>=4.14.1,<5
|
|
9
|
+
Requires-Dist: pydantic>=2.12.3,<3
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.10.1,<3
|
|
11
|
+
Requires-Dist: timeout-decorator>=0.5.0,<1
|
|
12
|
+
Requires-Dist: markdownify>=0.14.1,<1
|
|
13
|
+
Requires-Dist: fake-useragent>=2.2.0,<3
|
|
14
|
+
Requires-Dist: firecrawl>=3.3.2,<4
|
|
15
|
+
Requires-Dist: tavily-python>=0.7.11,<1
|
|
16
|
+
Requires-Dist: unidecode>=1.4.0,<2
|
|
17
|
+
Requires-Dist: azure-ai-projects>=1.0.0,<2
|
|
18
|
+
Requires-Dist: azure-identity>=1.25.0,<2
|
|
19
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev20,<2026.30.0rc0
|
|
20
|
+
Requires-Dist: azure-core>=1.36.0,<2
|
|
21
|
+
Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
|
|
22
|
+
Requires-Dist: google-auth>=2.43.0,<3
|
|
23
|
+
Requires-Dist: google-generativeai>=0.8.5,<0.9
|
|
24
|
+
Requires-Dist: rapidfuzz>=3.0.0,<4
|
|
25
|
+
Requires-Dist: httpx>=0.28.1
|
|
26
|
+
Requires-Dist: certifi>=2025.11.12
|
|
27
|
+
Requires-Dist: jinja2>=3.1.6
|
|
28
|
+
Requires-Dist: unique-search-proxy-core>=2026.30.0.dev5,<2026.30.0rc0
|
|
29
|
+
Requires-Dist: unique-search-proxy-sdk>=2026.30.0.dev3,<2026.30.0rc0
|
|
30
|
+
Requires-Python: >=3.12, <4.0
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Unique Web Search
|
|
34
|
+
|
|
35
|
+
Configurable web search tool for Unique assistants: search the web, read pages, and return citation-ready content chunks.
|
|
36
|
+
|
|
37
|
+
## Search Proxy (recommended)
|
|
38
|
+
|
|
39
|
+
Outbound web access should go through **Unique Search Proxy** (`connectors/unique_search_proxy/`). When `SEARCH_PROXY_BASE_URL` is set, search and crawl calls use `unique-search-proxy-sdk`; credentials and provider egress stay on the proxy pod.
|
|
40
|
+
|
|
41
|
+
| Mode | When | Behaviour |
|
|
42
|
+
|------|------|-----------|
|
|
43
|
+
| **Proxy (recommended)** | `SEARCH_PROXY_BASE_URL` set | `_proxy_search` / `_proxy_crawl` via the SDK |
|
|
44
|
+
| **Legacy** | URL unset | Direct provider calls from this process (`_legacy_search` / `_legacy_crawl`) |
|
|
45
|
+
|
|
46
|
+
- **New standard engines (Brave, Perplexity) are proxy-only** — legacy raises.
|
|
47
|
+
- **Custom API** always runs locally (never routed through the proxy).
|
|
48
|
+
- Future provider work targets Search Proxy only; legacy is preserved for migrated engines.
|
|
49
|
+
|
|
50
|
+
Packages involved: [`unique-search-proxy-core`](../../connectors/unique_search_proxy/unique_search_proxy_core/README.md) (schemas), [`unique-search-proxy-sdk`](../../connectors/unique_search_proxy/unique_search_proxy_sdk/README.md) (HTTP), [`unique-search-proxy`](../../connectors/unique_search_proxy/unique_search_proxy_client/README.md) (server). System overview: [`connectors/unique_search_proxy/README.md`](../../connectors/unique_search_proxy/README.md).
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
## Search engine kinds
|
|
57
|
+
|
|
58
|
+
### Standard search
|
|
59
|
+
|
|
60
|
+
Query → normalised results (URL / title / snippet / optional content). Optional page crawl via the configured crawler.
|
|
61
|
+
|
|
62
|
+
| Engine | Proxy | Notes |
|
|
63
|
+
|--------|-------|-------|
|
|
64
|
+
| Google | legacy + proxy | Requires scraping by default |
|
|
65
|
+
| Brave | **proxy-only** | Rich snippets; `ExposableParam` knobs |
|
|
66
|
+
| Perplexity | **proxy-only** | Content extraction knobs; `ExposableParam` |
|
|
67
|
+
| Custom API | always local | Your REST endpoint |
|
|
68
|
+
|
|
69
|
+
Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `value`) for optional provider knobs. See [Search Engines README](./src/unique_web_search/services/search_engine/README.md).
|
|
70
|
+
|
|
71
|
+
### Agent search (grounding)
|
|
72
|
+
|
|
73
|
+
An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s.
|
|
74
|
+
|
|
75
|
+
| Engine | Proxy | Notes |
|
|
76
|
+
|--------|-------|-------|
|
|
77
|
+
| Bing (Azure AI Foundry grounding) | legacy + proxy | Configurable `requires_scraping` |
|
|
78
|
+
| VertexAI (Gemini grounding) | legacy + proxy | Configurable `requires_scraping` |
|
|
79
|
+
|
|
80
|
+
Agent engines do **not** use `ExposableParam`.
|
|
81
|
+
|
|
82
|
+
## Web crawlers (page readers)
|
|
83
|
+
|
|
84
|
+
| Crawler | Auth | Notes |
|
|
85
|
+
|---------|------|-------|
|
|
86
|
+
| Basic | none | HTTP + content-type toggles; tool-local `url_blocked_patterns` (legacy) |
|
|
87
|
+
| Tavily | `TAVILY_API_KEY` | Extract API |
|
|
88
|
+
| Jina | `JINA_API_KEY` | Reader API |
|
|
89
|
+
| Firecrawl | `FIRECRAWL_API_KEY` | Batch scrape |
|
|
90
|
+
|
|
91
|
+
No Crawl4AI. Crawler configs have **no exposable LLM knobs**.
|
|
92
|
+
|
|
93
|
+
## Execution modes
|
|
94
|
+
|
|
95
|
+
| Mode | Status | Behaviour |
|
|
96
|
+
|------|--------|-----------|
|
|
97
|
+
| **V1** | Stable | Refine query → search → crawl → process |
|
|
98
|
+
| **V2** | Stable | Parallel plan steps (search / read URL) |
|
|
99
|
+
| **V3** | Experimental | Agent loop: snippet `search` vs on-demand `read_urls` |
|
|
100
|
+
|
|
101
|
+
Proxy vs legacy is transparent to executors.
|
|
102
|
+
|
|
103
|
+
## Detailed subsystem docs
|
|
104
|
+
|
|
105
|
+
- [Search Engines](./src/unique_web_search/services/search_engine/README.md)
|
|
106
|
+
- [Crawlers](./src/unique_web_search/services/crawlers/README.md)
|
|
107
|
+
- [Executors](./src/unique_web_search/services/executors/README.md)
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
- `SEARCH_PROXY_BASE_URL` — enables the Search Proxy client
|
|
112
|
+
- Provider API keys (on the proxy pod when proxy is enabled; otherwise on assistants-core for legacy)
|
|
113
|
+
- Search / crawler / content-processing settings via `WebSearchConfig`
|
|
114
|
+
|
|
115
|
+
## Dependency management (uv.lock + min/latest testing)
|
|
116
|
+
|
|
117
|
+
This package is a **library** and uses `uv` for dependency management.
|
|
118
|
+
|
|
119
|
+
We run tests additionally with minimal dependencies to ensure that the listed ranges are valid. NOTE: We use lowest-direct, not lowest.
|
|
120
|
+
Lowest attempts to use the lowest possible dependency versions _transitively_ causing issues if a dependency has incorrect metadata. Example:
|
|
121
|
+
- google-cloud-aiplatform says it works with shapely<3.0.0.
|
|
122
|
+
- The lowest resolver assumes 1.0 which needs python 2 -> breaks
|
|
123
|
+
Therefore we use lowest-direct which only sets our direct dependencies to lowest. However, this only correctly verifies our min dependencies
|
|
124
|
+
if our code correctly lists all the required dependencies and never imports a transitive dependency. We therefore use deptry to ensure we
|
|
125
|
+
don't use transitive dependencies and that we have no unused dependencies.
|
|
126
|
+
|
|
127
|
+
### Test locally
|
|
128
|
+
|
|
129
|
+
- **Latest deps and deptry**:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cd tool_packages/unique_web_search
|
|
133
|
+
uv sync
|
|
134
|
+
uv run pytest
|
|
135
|
+
uv run deptry
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- **Min deps**:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd tool_packages/unique_web_search
|
|
142
|
+
uv venv
|
|
143
|
+
uv pip install -e . --resolution=lowest-direct
|
|
144
|
+
uv export --only-group dev --no-hashes | uv pip install -r -
|
|
145
|
+
uv run --no-sync pytest
|
|
146
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Unique Web Search
|
|
2
|
+
|
|
3
|
+
Configurable web search tool for Unique assistants: search the web, read pages, and return citation-ready content chunks.
|
|
4
|
+
|
|
5
|
+
## Search Proxy (recommended)
|
|
6
|
+
|
|
7
|
+
Outbound web access should go through **Unique Search Proxy** (`connectors/unique_search_proxy/`). When `SEARCH_PROXY_BASE_URL` is set, search and crawl calls use `unique-search-proxy-sdk`; credentials and provider egress stay on the proxy pod.
|
|
8
|
+
|
|
9
|
+
| Mode | When | Behaviour |
|
|
10
|
+
|------|------|-----------|
|
|
11
|
+
| **Proxy (recommended)** | `SEARCH_PROXY_BASE_URL` set | `_proxy_search` / `_proxy_crawl` via the SDK |
|
|
12
|
+
| **Legacy** | URL unset | Direct provider calls from this process (`_legacy_search` / `_legacy_crawl`) |
|
|
13
|
+
|
|
14
|
+
- **New standard engines (Brave, Perplexity) are proxy-only** — legacy raises.
|
|
15
|
+
- **Custom API** always runs locally (never routed through the proxy).
|
|
16
|
+
- Future provider work targets Search Proxy only; legacy is preserved for migrated engines.
|
|
17
|
+
|
|
18
|
+
Packages involved: [`unique-search-proxy-core`](../../connectors/unique_search_proxy/unique_search_proxy_core/README.md) (schemas), [`unique-search-proxy-sdk`](../../connectors/unique_search_proxy/unique_search_proxy_sdk/README.md) (HTTP), [`unique-search-proxy`](../../connectors/unique_search_proxy/unique_search_proxy_client/README.md) (server). System overview: [`connectors/unique_search_proxy/README.md`](../../connectors/unique_search_proxy/README.md).
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
## Search engine kinds
|
|
25
|
+
|
|
26
|
+
### Standard search
|
|
27
|
+
|
|
28
|
+
Query → normalised results (URL / title / snippet / optional content). Optional page crawl via the configured crawler.
|
|
29
|
+
|
|
30
|
+
| Engine | Proxy | Notes |
|
|
31
|
+
|--------|-------|-------|
|
|
32
|
+
| Google | legacy + proxy | Requires scraping by default |
|
|
33
|
+
| Brave | **proxy-only** | Rich snippets; `ExposableParam` knobs |
|
|
34
|
+
| Perplexity | **proxy-only** | Content extraction knobs; `ExposableParam` |
|
|
35
|
+
| Custom API | always local | Your REST endpoint |
|
|
36
|
+
|
|
37
|
+
Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `value`) for optional provider knobs. See [Search Engines README](./src/unique_web_search/services/search_engine/README.md).
|
|
38
|
+
|
|
39
|
+
### Agent search (grounding)
|
|
40
|
+
|
|
41
|
+
An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s.
|
|
42
|
+
|
|
43
|
+
| Engine | Proxy | Notes |
|
|
44
|
+
|--------|-------|-------|
|
|
45
|
+
| Bing (Azure AI Foundry grounding) | legacy + proxy | Configurable `requires_scraping` |
|
|
46
|
+
| VertexAI (Gemini grounding) | legacy + proxy | Configurable `requires_scraping` |
|
|
47
|
+
|
|
48
|
+
Agent engines do **not** use `ExposableParam`.
|
|
49
|
+
|
|
50
|
+
## Web crawlers (page readers)
|
|
51
|
+
|
|
52
|
+
| Crawler | Auth | Notes |
|
|
53
|
+
|---------|------|-------|
|
|
54
|
+
| Basic | none | HTTP + content-type toggles; tool-local `url_blocked_patterns` (legacy) |
|
|
55
|
+
| Tavily | `TAVILY_API_KEY` | Extract API |
|
|
56
|
+
| Jina | `JINA_API_KEY` | Reader API |
|
|
57
|
+
| Firecrawl | `FIRECRAWL_API_KEY` | Batch scrape |
|
|
58
|
+
|
|
59
|
+
No Crawl4AI. Crawler configs have **no exposable LLM knobs**.
|
|
60
|
+
|
|
61
|
+
## Execution modes
|
|
62
|
+
|
|
63
|
+
| Mode | Status | Behaviour |
|
|
64
|
+
|------|--------|-----------|
|
|
65
|
+
| **V1** | Stable | Refine query → search → crawl → process |
|
|
66
|
+
| **V2** | Stable | Parallel plan steps (search / read URL) |
|
|
67
|
+
| **V3** | Experimental | Agent loop: snippet `search` vs on-demand `read_urls` |
|
|
68
|
+
|
|
69
|
+
Proxy vs legacy is transparent to executors.
|
|
70
|
+
|
|
71
|
+
## Detailed subsystem docs
|
|
72
|
+
|
|
73
|
+
- [Search Engines](./src/unique_web_search/services/search_engine/README.md)
|
|
74
|
+
- [Crawlers](./src/unique_web_search/services/crawlers/README.md)
|
|
75
|
+
- [Executors](./src/unique_web_search/services/executors/README.md)
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
- `SEARCH_PROXY_BASE_URL` — enables the Search Proxy client
|
|
80
|
+
- Provider API keys (on the proxy pod when proxy is enabled; otherwise on assistants-core for legacy)
|
|
81
|
+
- Search / crawler / content-processing settings via `WebSearchConfig`
|
|
82
|
+
|
|
83
|
+
## Dependency management (uv.lock + min/latest testing)
|
|
84
|
+
|
|
85
|
+
This package is a **library** and uses `uv` for dependency management.
|
|
86
|
+
|
|
87
|
+
We run tests additionally with minimal dependencies to ensure that the listed ranges are valid. NOTE: We use lowest-direct, not lowest.
|
|
88
|
+
Lowest attempts to use the lowest possible dependency versions _transitively_ causing issues if a dependency has incorrect metadata. Example:
|
|
89
|
+
- google-cloud-aiplatform says it works with shapely<3.0.0.
|
|
90
|
+
- The lowest resolver assumes 1.0 which needs python 2 -> breaks
|
|
91
|
+
Therefore we use lowest-direct which only sets our direct dependencies to lowest. However, this only correctly verifies our min dependencies
|
|
92
|
+
if our code correctly lists all the required dependencies and never imports a transitive dependency. We therefore use deptry to ensure we
|
|
93
|
+
don't use transitive dependencies and that we have no unused dependencies.
|
|
94
|
+
|
|
95
|
+
### Test locally
|
|
96
|
+
|
|
97
|
+
- **Latest deps and deptry**:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cd tool_packages/unique_web_search
|
|
101
|
+
uv sync
|
|
102
|
+
uv run pytest
|
|
103
|
+
uv run deptry
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- **Min deps**:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cd tool_packages/unique_web_search
|
|
110
|
+
uv venv
|
|
111
|
+
uv pip install -e . --resolution=lowest-direct
|
|
112
|
+
uv export --only-group dev --no-hashes | uv pip install -r -
|
|
113
|
+
uv run --no-sync pytest
|
|
114
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_web_search"
|
|
3
|
-
version = "2026.30.0.
|
|
3
|
+
version = "2026.30.0.dev4"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12,<4.0"
|
|
@@ -22,7 +22,7 @@ dependencies = [
|
|
|
22
22
|
"unidecode>=1.4.0,<2",
|
|
23
23
|
"azure-ai-projects>=1.0.0,<2",
|
|
24
24
|
"azure-identity>=1.25.0,<2",
|
|
25
|
-
"unique-toolkit[monitoring]>=2026.30.0.
|
|
25
|
+
"unique-toolkit[monitoring]>=2026.30.0.dev20,<2026.30.0rc0",
|
|
26
26
|
"azure-core>=1.36.0,<2",
|
|
27
27
|
"google-cloud-aiplatform>=1.128.0,<2",
|
|
28
28
|
"google-auth>=2.43.0,<3",
|
|
@@ -31,8 +31,8 @@ dependencies = [
|
|
|
31
31
|
"httpx>=0.28.1",
|
|
32
32
|
"certifi>=2025.11.12",
|
|
33
33
|
"jinja2>=3.1.6",
|
|
34
|
-
"unique-search-proxy-core>=2026.30.0.
|
|
35
|
-
"unique-search-proxy-sdk>=2026.30.0.
|
|
34
|
+
"unique-search-proxy-core>=2026.30.0.dev5,<2026.30.0rc0",
|
|
35
|
+
"unique-search-proxy-sdk>=2026.30.0.dev3,<2026.30.0rc0",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[build-system]
|
|
@@ -3,6 +3,7 @@ from datetime import datetime
|
|
|
3
3
|
from time import time
|
|
4
4
|
|
|
5
5
|
from typing_extensions import override
|
|
6
|
+
from unique_search_proxy_core.context import RequestContext
|
|
6
7
|
from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
|
|
7
8
|
from unique_search_proxy_core.search_engines.base import BaseSearchEngineConfig
|
|
8
9
|
from unique_toolkit._common.chunk_relevancy_sorter.service import ChunkRelevancySorter
|
|
@@ -72,13 +73,22 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
72
73
|
language_model_orchestrator or configuration.token_counting_language_model
|
|
73
74
|
)
|
|
74
75
|
|
|
76
|
+
self.chunk_relevancy_sorter = ChunkRelevancySorter(self.event)
|
|
77
|
+
self.company_id = self.event.company_id
|
|
78
|
+
self.request_context = RequestContext(
|
|
79
|
+
company_id=self.event.company_id,
|
|
80
|
+
user_id=self.event.user_id,
|
|
81
|
+
chat_id=self.event.payload.chat_id,
|
|
82
|
+
)
|
|
75
83
|
self.search_engine_service = get_search_engine_service(
|
|
76
84
|
self.config.search_engine_config,
|
|
77
85
|
self.language_model_service,
|
|
86
|
+
request_context=self.request_context,
|
|
87
|
+
)
|
|
88
|
+
self.crawler_service = get_crawler_service(
|
|
89
|
+
self.config.crawler_config,
|
|
90
|
+
request_context=self.request_context,
|
|
78
91
|
)
|
|
79
|
-
self.crawler_service = get_crawler_service(self.config.crawler_config)
|
|
80
|
-
self.chunk_relevancy_sorter = ChunkRelevancySorter(self.event)
|
|
81
|
-
self.company_id = self.event.company_id
|
|
82
92
|
self.chat_history_token_length = 0
|
|
83
93
|
self.chat_history_chat_messages = self._chat_service.get_full_history()
|
|
84
94
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Crawlers
|
|
2
|
+
|
|
3
|
+
Web-page readers used by Unique Web Search to fetch URLs and convert them to text.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
Crawler configuration lives in **`unique_search_proxy_core.crawlers`**. The tool registers implementations via `CRAWLER_REGISTRY` and dispatches through `BaseCrawler`.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart LR
|
|
11
|
+
Config["proxy-core *Config"]
|
|
12
|
+
Registry["CRAWLER_REGISTRY"]
|
|
13
|
+
Base["BaseCrawler"]
|
|
14
|
+
SDK["SDK crawl"]
|
|
15
|
+
Legacy["_legacy_crawl"]
|
|
16
|
+
|
|
17
|
+
Config --> Registry
|
|
18
|
+
Registry --> Base
|
|
19
|
+
Base -->|"SEARCH_PROXY_BASE_URL set"| SDK
|
|
20
|
+
Base -->|"unset"| Legacy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Key invariants
|
|
24
|
+
|
|
25
|
+
- **No exposable params.** Crawlers have no LLM override surface — only deployment defaults + `urls`.
|
|
26
|
+
- **Proxy body = config fields + urls.** `BaseCrawler._proxy_crawl` dumps `config.model_dump(exclude={"crawler", ...})` into the SDK call.
|
|
27
|
+
- **Legacy retained** when the search proxy is disabled.
|
|
28
|
+
|
|
29
|
+
## Available crawlers
|
|
30
|
+
|
|
31
|
+
| Name | Config source | Auth | Notes |
|
|
32
|
+
|------|---------------|------|-------|
|
|
33
|
+
| Basic | tool-local `BasicConfig` extends proxy-core | none | HTTP fetch + processors; `content_types` toggles; `url_blocked_patterns` (tool-local, stripped before proxy) |
|
|
34
|
+
| Tavily | `TavilyConfig` | `TAVILY_API_KEY` | Extract API |
|
|
35
|
+
| Jina | `JinaConfig` | `JINA_API_KEY` | Reader API |
|
|
36
|
+
| Firecrawl | `FirecrawlConfig` | `FIRECRAWL_API_KEY` | Batch scrape |
|
|
37
|
+
|
|
38
|
+
There is **no Crawl4AI** crawler.
|
|
39
|
+
|
|
40
|
+
`WebSearchConfig.crawler_config` is a discriminated union on `crawler` (`Basic` / `Tavily` / `Jina` / `Firecrawl`).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Basic
|
|
45
|
+
|
|
46
|
+
| Field | Default | Notes |
|
|
47
|
+
|-------|---------|-------|
|
|
48
|
+
| `timeout` | from base | Per-request timeout (seconds) |
|
|
49
|
+
| `content_types` | html/xhtml/plain/markdown on; pdf off | Which MIME types to process |
|
|
50
|
+
| `max_concurrent_requests` | `10` | Parallelism |
|
|
51
|
+
| `url_blocked_patterns` | tool-local | Regex URL blocklist; **not** sent to the proxy |
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from unique_search_proxy_core.crawlers import CrawlerType
|
|
55
|
+
from unique_web_search.services.crawlers import BasicConfig, get_crawler_service
|
|
56
|
+
|
|
57
|
+
config = BasicConfig(crawler=CrawlerType.BASIC, timeout=30)
|
|
58
|
+
markdowns = await get_crawler_service(config).crawl(["https://example.com"])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Tavily
|
|
64
|
+
|
|
65
|
+
| Field | Default | Notes |
|
|
66
|
+
|-------|---------|-------|
|
|
67
|
+
| `extract_depth` | `advanced` | `basic` / `advanced` |
|
|
68
|
+
| `format` | `markdown` | `markdown` / `text` |
|
|
69
|
+
| `query` | unset | Optional rerank intent |
|
|
70
|
+
| `chunks_per_source` | unset | Requires `query` |
|
|
71
|
+
| `include_images` / `include_favicon` / `include_usage` | `false` | Response extras |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Jina
|
|
76
|
+
|
|
77
|
+
| Field | Default | Notes |
|
|
78
|
+
|-------|---------|-------|
|
|
79
|
+
| `return_format` | `markdown` | `markdown` / `html` / `text` / … |
|
|
80
|
+
| `engine` | `browser` | `auto` / `browser` / `direct` / `cf-browser-rendering` |
|
|
81
|
+
| `page_timeout` | unset | Seconds; falls back to crawl `timeout` |
|
|
82
|
+
| `max_concurrent_requests` | `10` | |
|
|
83
|
+
| `no_cache` | `false` | |
|
|
84
|
+
| `do_not_track` | `true` | |
|
|
85
|
+
| selectors / summaries | unset / false | `target_selector`, `wait_for_selector`, `remove_selector`, image/link options, `locale`, `referer`, `proxy_url` |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Firecrawl
|
|
90
|
+
|
|
91
|
+
| Field | Default | Notes |
|
|
92
|
+
|-------|---------|-------|
|
|
93
|
+
| `only_main_content` | `true` | Strip chrome before markdown |
|
|
94
|
+
| `only_clean_content` | `false` | Extra LLM clean pass |
|
|
95
|
+
| `max_concurrency` | unset | Batch parallelism |
|
|
96
|
+
| `ignore_invalid_urls` | `true` | |
|
|
97
|
+
| `wait_for` | `0` | ms delay before fetch |
|
|
98
|
+
| `mobile` | `false` | |
|
|
99
|
+
| `block_ads` | `true` | |
|
|
100
|
+
| `remove_base64_images` | `true` | |
|
|
101
|
+
| `proxy` | `auto` | Firecrawl proxy tier |
|
|
102
|
+
| `include_tags` / `exclude_tags` / `scrape_headers` / `max_age` | unset | Optional |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Adding a crawler
|
|
107
|
+
|
|
108
|
+
1. Define config (+ request model) in `unique_search_proxy_core.crawlers.<name>`.
|
|
109
|
+
2. Add a tool module with `@register_crawler(...)` implementing `_legacy_crawl`.
|
|
110
|
+
3. Modules are auto-discovered; keep static unions in `__init__.py` and cover with `tests/test_crawler_registry.py`.
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from typing import Generic, TypeVar
|
|
5
5
|
|
|
6
|
+
from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
|
|
6
7
|
from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig
|
|
7
8
|
from unique_search_proxy_core.url_safety import (
|
|
8
9
|
CrawlTargetValidationError,
|
|
@@ -30,8 +31,14 @@ class BaseCrawler(ABC, Generic[CrawlerConfig]):
|
|
|
30
31
|
the direct ``_legacy_crawl`` implementation used when the proxy is disabled.
|
|
31
32
|
"""
|
|
32
33
|
|
|
33
|
-
def __init__(
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
config: CrawlerConfig,
|
|
37
|
+
*,
|
|
38
|
+
request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
|
|
39
|
+
):
|
|
34
40
|
self.config = config
|
|
41
|
+
self._request_context = request_context
|
|
35
42
|
|
|
36
43
|
async def crawl(self, urls: list[str]) -> list[str]:
|
|
37
44
|
if search_proxy_client_enabled:
|
|
@@ -49,7 +56,8 @@ class BaseCrawler(ABC, Generic[CrawlerConfig]):
|
|
|
49
56
|
"""Dump deployment config fields into the generic proxy crawl call."""
|
|
50
57
|
params = self.config.model_dump(exclude={"crawler"}, exclude_none=True)
|
|
51
58
|
async with open_search_proxy_client(
|
|
52
|
-
timeout=float(self.config.timeout)
|
|
59
|
+
timeout=float(self.config.timeout),
|
|
60
|
+
context=self._request_context,
|
|
53
61
|
) as client:
|
|
54
62
|
response = await client.crawl.crawl(
|
|
55
63
|
urls=urls,
|
|
@@ -8,6 +8,7 @@ from httpx import AsyncClient, Timeout
|
|
|
8
8
|
from markdownify import markdownify
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
from typing_extensions import override
|
|
11
|
+
from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
|
|
11
12
|
from unique_search_proxy_core.crawlers.base import CrawlerType
|
|
12
13
|
from unique_search_proxy_core.crawlers.basic.content_types import (
|
|
13
14
|
CONTENT_TYPE_TOGGLE_TO_MIME,
|
|
@@ -54,8 +55,13 @@ class BasicConfig(CoreBasicConfig):
|
|
|
54
55
|
config_display_name="Basic",
|
|
55
56
|
)
|
|
56
57
|
class BasicCrawler(BaseCrawler[BasicConfig]):
|
|
57
|
-
def __init__(
|
|
58
|
-
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
config: BasicConfig,
|
|
61
|
+
*,
|
|
62
|
+
request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
|
|
63
|
+
):
|
|
64
|
+
super().__init__(config, request_context=request_context)
|
|
59
65
|
|
|
60
66
|
self.semaphore: asyncio.Semaphore = asyncio.Semaphore(
|
|
61
67
|
self.config.max_concurrent_requests
|
|
@@ -69,7 +75,8 @@ class BasicCrawler(BaseCrawler[BasicConfig]):
|
|
|
69
75
|
exclude_none=True,
|
|
70
76
|
)
|
|
71
77
|
async with open_search_proxy_client(
|
|
72
|
-
timeout=float(self.config.timeout)
|
|
78
|
+
timeout=float(self.config.timeout),
|
|
79
|
+
context=self._request_context,
|
|
73
80
|
) as client:
|
|
74
81
|
response = await client.crawl.crawl(
|
|
75
82
|
urls=urls,
|
|
@@ -19,9 +19,6 @@ from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
|
|
|
19
19
|
config_display_name="Firecrawl",
|
|
20
20
|
)
|
|
21
21
|
class FirecrawlCrawler(BaseCrawler[FirecrawlConfig]):
|
|
22
|
-
def __init__(self, config: FirecrawlConfig):
|
|
23
|
-
super().__init__(config)
|
|
24
|
-
|
|
25
22
|
@override
|
|
26
23
|
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
|
|
27
24
|
urls = [target.normalized_url for target in targets]
|
|
@@ -39,9 +39,6 @@ class ReaderResponse(BaseModel):
|
|
|
39
39
|
config_display_name="Jina",
|
|
40
40
|
)
|
|
41
41
|
class JinaCrawler(BaseCrawler[JinaConfig]):
|
|
42
|
-
def __init__(self, config: JinaConfig):
|
|
43
|
-
super().__init__(config)
|
|
44
|
-
|
|
45
42
|
@override
|
|
46
43
|
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
|
|
47
44
|
urls = [target.normalized_url for target in targets]
|
|
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
|
+
from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
|
|
9
10
|
from unique_search_proxy_core.crawlers.base import CrawlerType
|
|
10
11
|
|
|
11
12
|
from unique_web_search.services._registry import BaseSpec, Registry
|
|
@@ -37,9 +38,16 @@ def register_crawler(
|
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
def get_crawler_service(
|
|
41
|
+
def get_crawler_service(
|
|
42
|
+
crawler_config: object,
|
|
43
|
+
*,
|
|
44
|
+
request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
|
|
45
|
+
):
|
|
41
46
|
crawler = getattr(crawler_config, "crawler")
|
|
42
|
-
return CRAWLER_REGISTRY[crawler].impl_cls(
|
|
47
|
+
return CRAWLER_REGISTRY[crawler].impl_cls(
|
|
48
|
+
crawler_config,
|
|
49
|
+
request_context=request_context,
|
|
50
|
+
)
|
|
43
51
|
|
|
44
52
|
|
|
45
53
|
__all__ = [
|
|
@@ -18,9 +18,6 @@ from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
|
|
|
18
18
|
config_display_name="Tavily",
|
|
19
19
|
)
|
|
20
20
|
class TavilyCrawler(BaseCrawler[TavilyConfig]):
|
|
21
|
-
def __init__(self, config: TavilyConfig):
|
|
22
|
-
super().__init__(config)
|
|
23
|
-
|
|
24
21
|
@override
|
|
25
22
|
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
|
|
26
23
|
urls = [target.normalized_url for target in targets]
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Executors orchestrate the **end-to-end research workflow** once a query or plan has been validated. They sit between:
|
|
4
4
|
|
|
5
|
-
- **Search engines** (Google, Bing, VertexAI
|
|
6
|
-
- **Crawlers** (Basic, Tavily,
|
|
5
|
+
- **Search engines** (standard: Google, Brave, Perplexity, Custom API; agent/grounding: Bing, VertexAI)
|
|
6
|
+
- **Crawlers** (Basic, Tavily, Jina, Firecrawl)
|
|
7
7
|
- **Content processing** (summaries, truncation, relevancy sorters)
|
|
8
8
|
- **LLM interfaces** (query refinement, structured outputs)
|
|
9
9
|
- **Progress reporters / logging**
|
|
10
10
|
|
|
11
|
+
**Search Proxy is transparent to executors:** when `SEARCH_PROXY_BASE_URL` is set, `SearchEngine.search` / `BaseCrawler.crawl` route via the proxy SDK; otherwise they use legacy direct calls. V1 / V2 / V3 do not branch on proxy mode.
|
|
12
|
+
|
|
11
13
|
All executors extend the shared base class defined in `base_executor.py`.
|
|
12
14
|
|
|
13
15
|
```python
|
|
@@ -37,6 +39,7 @@ All steps push diagnostics into `WebSearchDebugInfo` for telemetry and debugging
|
|
|
37
39
|
|----------|-----------|----------|----------------|-------------|-------|
|
|
38
40
|
| **WebSearchV1Executor** | `WebSearchToolParameters` (single query + date restrictions) | Traditional search flows | LLM-based query refinement (Basic/Advanced) | Sequential (queries iterated) | Ideal for iterative refinement and single-shot queries |
|
|
39
41
|
| **WebSearchV2Executor** | `WebSearchPlan` (multi-step plan) | Agentic / multi-step research | Executes explicit steps (search/read) | Parallel (steps scheduled concurrently) | Best for step-based plans generated by planning agents |
|
|
42
|
+
| **WebSearchV3Executor** | Per-call `search` or `read_urls` | Iterative agent research | Model chooses next cheap search vs full read | One command per tool call | Experimental; loop bounded by orchestrator iteration cap |
|
|
40
43
|
|
|
41
44
|
---
|
|
42
45
|
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import AsyncIterator
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
5
|
|
|
6
|
+
from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
|
|
6
7
|
from unique_search_proxy_sdk import UniqueSearchProxyClient
|
|
7
8
|
|
|
8
9
|
from unique_web_search.settings import env_settings
|
|
@@ -13,6 +14,7 @@ search_proxy_client_enabled = env_settings.search_proxy_base_url is not None
|
|
|
13
14
|
@asynccontextmanager
|
|
14
15
|
async def open_search_proxy_client(
|
|
15
16
|
timeout: float,
|
|
17
|
+
context: RequestContext = LOCAL_REQUEST_CONTEXT,
|
|
16
18
|
) -> AsyncIterator[UniqueSearchProxyClient]:
|
|
17
19
|
base_url = env_settings.search_proxy_base_url
|
|
18
20
|
assert base_url is not None, "Unique Search Proxy base URL is not configured"
|
|
@@ -20,6 +22,7 @@ async def open_search_proxy_client(
|
|
|
20
22
|
async with UniqueSearchProxyClient(
|
|
21
23
|
base_url=base_url,
|
|
22
24
|
timeout=timeout,
|
|
25
|
+
context=context,
|
|
23
26
|
) as client:
|
|
24
27
|
yield client
|
|
25
28
|
|