unique_web_search 2026.30.0.dev2__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.dev2 → unique_web_search-2026.30.0.dev4}/pyproject.toml +4 -6
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/config.py +1 -1
- {unique_web_search-2026.30.0.dev2 → 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.dev4/src/unique_web_search/services/crawlers/__init__.py +60 -0
- unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/base.py +70 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/basic.py +61 -51
- unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/firecrawl.py +46 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/jina.py +60 -53
- unique_web_search-2026.30.0.dev4/src/unique_web_search/services/crawlers/registry.py +58 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/tavily.py +20 -43
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/README.md +5 -2
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/executor.py +2 -2
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/executor.py +2 -2
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/executor.py +1 -1
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/base.py +14 -3
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/bing.py +4 -1
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/registry.py +12 -2
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/vertexai.py +4 -1
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/settings.py +1 -1
- unique_web_search-2026.30.0.dev2/PKG-INFO +0 -149
- unique_web_search-2026.30.0.dev2/README.md +0 -115
- unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/README.md +0 -360
- unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/__init__.py +0 -107
- unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/base.py +0 -77
- unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/crawl4ai.py +0 -326
- unique_web_search-2026.30.0.dev2/src/unique_web_search/services/crawlers/firecrawl.py +0 -70
- unique_web_search-2026.30.0.dev2/src/unique_web_search/services/search_engine/README.md +0 -715
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/client_settings.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/_registry.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/argument_screening/service.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/client/proxy_config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/content_processing/service.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/base_executor.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/context.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/modes.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/proxy/mappers.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/query_elicitation.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/brave.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/google.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/search_engine/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/snippet_judge/service.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/services/structured_llm.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev4}/src/unique_web_search/skills/web-search-v3.md +0 -0
- {unique_web_search-2026.30.0.dev2 → 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"
|
|
@@ -17,13 +17,12 @@ dependencies = [
|
|
|
17
17
|
"timeout-decorator>=0.5.0,<1",
|
|
18
18
|
"markdownify>=0.14.1,<1",
|
|
19
19
|
"fake-useragent>=2.2.0,<3",
|
|
20
|
-
"crawl4ai>=0.6.3,<1",
|
|
21
20
|
"firecrawl>=3.3.2,<4",
|
|
22
21
|
"tavily-python>=0.7.11,<1",
|
|
23
22
|
"unidecode>=1.4.0,<2",
|
|
24
23
|
"azure-ai-projects>=1.0.0,<2",
|
|
25
24
|
"azure-identity>=1.25.0,<2",
|
|
26
|
-
"unique-toolkit[monitoring]>=2026.30.0.
|
|
25
|
+
"unique-toolkit[monitoring]>=2026.30.0.dev20,<2026.30.0rc0",
|
|
27
26
|
"azure-core>=1.36.0,<2",
|
|
28
27
|
"google-cloud-aiplatform>=1.128.0,<2",
|
|
29
28
|
"google-auth>=2.43.0,<3",
|
|
@@ -32,9 +31,8 @@ dependencies = [
|
|
|
32
31
|
"httpx>=0.28.1",
|
|
33
32
|
"certifi>=2025.11.12",
|
|
34
33
|
"jinja2>=3.1.6",
|
|
35
|
-
"
|
|
36
|
-
"unique-search-proxy-
|
|
37
|
-
"unique-search-proxy-sdk>=2026.30.0.dev2,<2026.30.0rc0",
|
|
34
|
+
"unique-search-proxy-core>=2026.30.0.dev5,<2026.30.0rc0",
|
|
35
|
+
"unique-search-proxy-sdk>=2026.30.0.dev3,<2026.30.0rc0",
|
|
38
36
|
]
|
|
39
37
|
|
|
40
38
|
[build-system]
|
|
@@ -180,7 +180,7 @@ class WebSearchConfig(BaseToolConfig):
|
|
|
180
180
|
default_factory=DefaultCrawler, # type: ignore (This type is computed at runtime so pyright is not able to infer it)
|
|
181
181
|
title="Web Page Reader",
|
|
182
182
|
description="Choose and configure how web pages are fetched and converted to readable text.",
|
|
183
|
-
discriminator="
|
|
183
|
+
discriminator="crawler",
|
|
184
184
|
)
|
|
185
185
|
|
|
186
186
|
content_processor_config: ContentProcessorConfig = Field(
|
|
@@ -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`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from unique_search_proxy_core.crawlers import CrawlerType
|
|
3
|
+
from unique_search_proxy_core.crawlers.firecrawl.schema import FirecrawlConfig
|
|
4
|
+
from unique_search_proxy_core.crawlers.jina.schema import JinaConfig
|
|
5
|
+
from unique_search_proxy_core.crawlers.tavily.schema import TavilyConfig
|
|
6
|
+
|
|
7
|
+
from unique_web_search.services.crawlers.base import BaseCrawler
|
|
8
|
+
from unique_web_search.services.crawlers.registry import (
|
|
9
|
+
CRAWLER_REGISTRY,
|
|
10
|
+
get_crawler_service,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
CRAWLER_REGISTRY.autodiscover(
|
|
14
|
+
__path__,
|
|
15
|
+
__name__,
|
|
16
|
+
exclude=frozenset({"url_safety", "utils", "registry"}),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from unique_web_search.services.crawlers.basic import ( # noqa: E402
|
|
20
|
+
BasicConfig,
|
|
21
|
+
BasicCrawler,
|
|
22
|
+
)
|
|
23
|
+
from unique_web_search.services.crawlers.firecrawl import ( # noqa: E402
|
|
24
|
+
FirecrawlCrawler,
|
|
25
|
+
)
|
|
26
|
+
from unique_web_search.services.crawlers.jina import JinaCrawler # noqa: E402
|
|
27
|
+
from unique_web_search.services.crawlers.tavily import TavilyCrawler # noqa: E402
|
|
28
|
+
|
|
29
|
+
CrawlerTypes = BasicCrawler | FirecrawlCrawler | JinaCrawler | TavilyCrawler
|
|
30
|
+
|
|
31
|
+
CrawlerConfigTypes = BasicConfig | FirecrawlConfig | JinaConfig | TavilyConfig
|
|
32
|
+
|
|
33
|
+
CRAWLER_NAME_TO_CONFIG = CRAWLER_REGISTRY.name_to_config()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_crawler_config_types_from_names(crawler_names: list[str]) -> type[BaseModel]:
|
|
37
|
+
return CRAWLER_REGISTRY.config_types_from_names(crawler_names)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_default_crawler_config(crawler_names: list[str]) -> type[BaseModel]:
|
|
41
|
+
return CRAWLER_REGISTRY.default_config(crawler_names)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"BasicCrawler",
|
|
46
|
+
"BasicConfig",
|
|
47
|
+
"FirecrawlCrawler",
|
|
48
|
+
"FirecrawlConfig",
|
|
49
|
+
"JinaCrawler",
|
|
50
|
+
"JinaConfig",
|
|
51
|
+
"TavilyCrawler",
|
|
52
|
+
"TavilyConfig",
|
|
53
|
+
"CrawlerType",
|
|
54
|
+
"BaseCrawler",
|
|
55
|
+
"get_crawler_service",
|
|
56
|
+
"CrawlerConfigTypes",
|
|
57
|
+
"CrawlerTypes",
|
|
58
|
+
"get_crawler_config_types_from_names",
|
|
59
|
+
"get_default_crawler_config",
|
|
60
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
|
|
7
|
+
from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig
|
|
8
|
+
from unique_search_proxy_core.url_safety import (
|
|
9
|
+
CrawlTargetValidationError,
|
|
10
|
+
ResolvedCrawlTarget,
|
|
11
|
+
UrlSafetyService,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from unique_web_search.metrics import crawl_blocked
|
|
15
|
+
from unique_web_search.services.proxy.bridge import (
|
|
16
|
+
open_search_proxy_client,
|
|
17
|
+
search_proxy_client_enabled,
|
|
18
|
+
)
|
|
19
|
+
from unique_web_search.services.proxy.mappers import map_crawl_response
|
|
20
|
+
|
|
21
|
+
CrawlerConfig = TypeVar(
|
|
22
|
+
"CrawlerConfig",
|
|
23
|
+
bound=BaseCrawlerConfig,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseCrawler(ABC, Generic[CrawlerConfig]):
|
|
28
|
+
"""Base class for web-page crawlers.
|
|
29
|
+
|
|
30
|
+
The search-proxy path is implemented entirely here. Subclasses only provide
|
|
31
|
+
the direct ``_legacy_crawl`` implementation used when the proxy is disabled.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
config: CrawlerConfig,
|
|
37
|
+
*,
|
|
38
|
+
request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
|
|
39
|
+
):
|
|
40
|
+
self.config = config
|
|
41
|
+
self._request_context = request_context
|
|
42
|
+
|
|
43
|
+
async def crawl(self, urls: list[str]) -> list[str]:
|
|
44
|
+
if search_proxy_client_enabled:
|
|
45
|
+
return await self._proxy_crawl(urls)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
targets = await UrlSafetyService.validate_batch_urls(urls)
|
|
49
|
+
except CrawlTargetValidationError as exc:
|
|
50
|
+
for target in exc.blocked_targets:
|
|
51
|
+
crawl_blocked.labels(reason_category=target.category).inc()
|
|
52
|
+
raise
|
|
53
|
+
return await self._legacy_crawl(targets)
|
|
54
|
+
|
|
55
|
+
async def _proxy_crawl(self, urls: list[str]) -> list[str]:
|
|
56
|
+
"""Dump deployment config fields into the generic proxy crawl call."""
|
|
57
|
+
params = self.config.model_dump(exclude={"crawler"}, exclude_none=True)
|
|
58
|
+
async with open_search_proxy_client(
|
|
59
|
+
timeout=float(self.config.timeout),
|
|
60
|
+
context=self._request_context,
|
|
61
|
+
) as client:
|
|
62
|
+
response = await client.crawl.crawl(
|
|
63
|
+
urls=urls,
|
|
64
|
+
crawler=self.config.crawler,
|
|
65
|
+
**params,
|
|
66
|
+
)
|
|
67
|
+
return map_crawl_response(response, urls)
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]: ...
|