unique_web_search 2026.30.0.dev1__tar.gz → 2026.30.0.dev3__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.dev1 → unique_web_search-2026.30.0.dev3}/PKG-INFO +4 -7
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/README.md +1 -2
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/pyproject.toml +3 -5
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/config.py +1 -1
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/service.py +35 -140
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/README.md +58 -0
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/__init__.py +60 -0
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/base.py +62 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/basic.py +54 -51
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/firecrawl.py +49 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/jina.py +61 -51
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/registry.py +50 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/tavily.py +21 -41
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/modes.py +164 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/executor.py +2 -2
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/v1/strategy.py +67 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/config.py +28 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/executor.py +2 -2
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/v2/strategy.py +79 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/executor.py +1 -1
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/schema.py +0 -8
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/v3/strategy.py +100 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/README.md +1 -1
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/__init__.py +2 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/brave.py +1 -1
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/custom_api.py +2 -2
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/google.py +1 -1
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/perplexity.py +1 -1
- unique_web_search-2026.30.0.dev3/src/unique_web_search/services/search_engine/registry.py +110 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/settings.py +1 -1
- unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/README.md +0 -360
- unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/__init__.py +0 -107
- unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/base.py +0 -77
- unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/crawl4ai.py +0 -326
- unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/firecrawl.py +0 -70
- unique_web_search-2026.30.0.dev1/src/unique_web_search/services/search_engine/registry.py +0 -62
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/client_settings.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/_registry.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/service.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/client/proxy_config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/service.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/README.md +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/base_config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/base_executor.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/context.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/schema.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/schema.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/config.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/bridge.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/mappers.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/query_elicitation.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/base.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/bing.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/schema.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/service.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/structured_llm.py +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/skills/web-search-v3.md +0 -0
- {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-web-search
|
|
3
|
-
Version: 2026.30.0.
|
|
3
|
+
Version: 2026.30.0.dev3
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Andreas Hauri, Gustav Hartz, Rami Azouz
|
|
6
6
|
Author-email: Andreas Hauri <andreas@unique.ch>, Gustav Hartz <gustav.hartz.ext@unique.ch>, Rami Azouz <rami.ext@unique.ch>
|
|
@@ -11,13 +11,12 @@ Requires-Dist: pydantic-settings>=2.10.1,<3
|
|
|
11
11
|
Requires-Dist: timeout-decorator>=0.5.0,<1
|
|
12
12
|
Requires-Dist: markdownify>=0.14.1,<1
|
|
13
13
|
Requires-Dist: fake-useragent>=2.2.0,<3
|
|
14
|
-
Requires-Dist: crawl4ai>=0.6.3,<1
|
|
15
14
|
Requires-Dist: firecrawl>=3.3.2,<4
|
|
16
15
|
Requires-Dist: tavily-python>=0.7.11,<1
|
|
17
16
|
Requires-Dist: unidecode>=1.4.0,<2
|
|
18
17
|
Requires-Dist: azure-ai-projects>=1.0.0,<2
|
|
19
18
|
Requires-Dist: azure-identity>=1.25.0,<2
|
|
20
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.
|
|
19
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev14,<2026.30.0rc0
|
|
21
20
|
Requires-Dist: azure-core>=1.36.0,<2
|
|
22
21
|
Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
|
|
23
22
|
Requires-Dist: google-auth>=2.43.0,<3
|
|
@@ -26,8 +25,7 @@ Requires-Dist: rapidfuzz>=3.0.0,<4
|
|
|
26
25
|
Requires-Dist: httpx>=0.28.1
|
|
27
26
|
Requires-Dist: certifi>=2025.11.12
|
|
28
27
|
Requires-Dist: jinja2>=3.1.6
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist: unique-search-proxy-core>=2026.30.0.dev2,<2026.30.0rc0
|
|
28
|
+
Requires-Dist: unique-search-proxy-core>=2026.30.0.dev3,<2026.30.0rc0
|
|
31
29
|
Requires-Dist: unique-search-proxy-sdk>=2026.30.0.dev2,<2026.30.0rc0
|
|
32
30
|
Requires-Python: >=3.12, <4.0
|
|
33
31
|
Description-Content-Type: text/markdown
|
|
@@ -60,7 +58,6 @@ The following diagram illustrates the complete architecture and workflow of the
|
|
|
60
58
|
|
|
61
59
|
- **Multiple Web Crawlers**:
|
|
62
60
|
- Basic HTTP Crawler
|
|
63
|
-
- Crawl4AI
|
|
64
61
|
- Jina Reader
|
|
65
62
|
- Tavily Crawler
|
|
66
63
|
- Firecrawl Crawler
|
|
@@ -85,7 +82,7 @@ The following diagram illustrates the complete architecture and workflow of the
|
|
|
85
82
|
For deeper dives into each subsystem, see the dedicated READMEs:
|
|
86
83
|
|
|
87
84
|
- [Search Engines](./unique_web_search/services/search_engine/README.md) — full catalogue of supported engines, configuration, and usage examples.
|
|
88
|
-
- [Crawlers](./unique_web_search/services/crawlers/README.md) — comparison of crawling strategies (Basic,
|
|
85
|
+
- [Crawlers](./unique_web_search/services/crawlers/README.md) — comparison of crawling strategies (Basic, Tavily, Firecrawl, Jina) with setup guides.
|
|
89
86
|
- [Executors](./unique_web_search/services/executors/README.md) — orchestration layer (V1 & V2) covering query refinement, planning, logging, and best practices.
|
|
90
87
|
|
|
91
88
|
## Configuration
|
|
@@ -26,7 +26,6 @@ The following diagram illustrates the complete architecture and workflow of the
|
|
|
26
26
|
|
|
27
27
|
- **Multiple Web Crawlers**:
|
|
28
28
|
- Basic HTTP Crawler
|
|
29
|
-
- Crawl4AI
|
|
30
29
|
- Jina Reader
|
|
31
30
|
- Tavily Crawler
|
|
32
31
|
- Firecrawl Crawler
|
|
@@ -51,7 +50,7 @@ The following diagram illustrates the complete architecture and workflow of the
|
|
|
51
50
|
For deeper dives into each subsystem, see the dedicated READMEs:
|
|
52
51
|
|
|
53
52
|
- [Search Engines](./unique_web_search/services/search_engine/README.md) — full catalogue of supported engines, configuration, and usage examples.
|
|
54
|
-
- [Crawlers](./unique_web_search/services/crawlers/README.md) — comparison of crawling strategies (Basic,
|
|
53
|
+
- [Crawlers](./unique_web_search/services/crawlers/README.md) — comparison of crawling strategies (Basic, Tavily, Firecrawl, Jina) with setup guides.
|
|
55
54
|
- [Executors](./unique_web_search/services/executors/README.md) — orchestration layer (V1 & V2) covering query refinement, planning, logging, and best practices.
|
|
56
55
|
|
|
57
56
|
## Configuration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_web_search"
|
|
3
|
-
version = "2026.30.0.
|
|
3
|
+
version = "2026.30.0.dev3"
|
|
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.dev14,<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,8 +31,7 @@ dependencies = [
|
|
|
32
31
|
"httpx>=0.28.1",
|
|
33
32
|
"certifi>=2025.11.12",
|
|
34
33
|
"jinja2>=3.1.6",
|
|
35
|
-
"
|
|
36
|
-
"unique-search-proxy-core>=2026.30.0.dev2,<2026.30.0rc0",
|
|
34
|
+
"unique-search-proxy-core>=2026.30.0.dev3,<2026.30.0rc0",
|
|
37
35
|
"unique-search-proxy-sdk>=2026.30.0.dev2,<2026.30.0rc0",
|
|
38
36
|
]
|
|
39
37
|
|
|
@@ -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(
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import re
|
|
3
2
|
from datetime import datetime
|
|
4
3
|
from time import time
|
|
5
4
|
|
|
6
|
-
from jinja2 import Template
|
|
7
5
|
from typing_extensions import override
|
|
8
6
|
from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
|
|
9
7
|
from unique_search_proxy_core.search_engines.base import BaseSearchEngineConfig
|
|
@@ -33,29 +31,22 @@ from unique_web_search.services.argument_screening import (
|
|
|
33
31
|
)
|
|
34
32
|
from unique_web_search.services.content_processing import ContentProcessor
|
|
35
33
|
from unique_web_search.services.crawlers import get_crawler_service
|
|
36
|
-
from unique_web_search.services.executors import (
|
|
37
|
-
WebSearchMode,
|
|
38
|
-
WebSearchV1Executor,
|
|
39
|
-
WebSearchV2Executor,
|
|
40
|
-
WebSearchV3Config,
|
|
41
|
-
WebSearchV3Executor,
|
|
42
|
-
)
|
|
43
34
|
from unique_web_search.services.executors.context import (
|
|
44
35
|
ExecutorCallbacks,
|
|
45
36
|
ExecutorConfiguration,
|
|
46
37
|
ExecutorServiceContext,
|
|
47
38
|
)
|
|
48
|
-
from unique_web_search.services.executors.
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
from unique_web_search.services.executors.modes import (
|
|
40
|
+
WebSearchExecutor,
|
|
41
|
+
WebSearchToolContext,
|
|
42
|
+
WebSearchToolParametersInstance,
|
|
43
|
+
get_mode_strategy,
|
|
44
|
+
)
|
|
51
45
|
from unique_web_search.services.message_log import WebSearchMessageLogger
|
|
52
46
|
from unique_web_search.services.query_elicitation import QueryElicitationService
|
|
53
47
|
from unique_web_search.services.search_engine import (
|
|
54
|
-
SearchEngineMode,
|
|
55
|
-
get_search_engine_mode,
|
|
56
48
|
get_search_engine_service,
|
|
57
49
|
)
|
|
58
|
-
from unique_web_search.services.search_engine.custom_api import CustomAPIConfig
|
|
59
50
|
from unique_web_search.utils import (
|
|
60
51
|
WebPageChunk,
|
|
61
52
|
reduce_sources_to_token_limit,
|
|
@@ -100,6 +91,7 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
100
91
|
self.debug = self.config.debug
|
|
101
92
|
self._display_name = kwargs.get("display_name", "Web Search")
|
|
102
93
|
self.exposed_params_cls = self._resolve_exposed_params_cls()
|
|
94
|
+
self._mode_strategy = get_mode_strategy(self.config.web_search_mode_config)
|
|
103
95
|
|
|
104
96
|
def content_reducer(web_page_chunks: list[WebPageChunk]) -> list[WebPageChunk]:
|
|
105
97
|
|
|
@@ -121,85 +113,30 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
121
113
|
return cfg.exposed_params_model()
|
|
122
114
|
return None
|
|
123
115
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
def _tool_context(self) -> WebSearchToolContext:
|
|
117
|
+
return WebSearchToolContext(
|
|
118
|
+
search_engine_config=self.search_engine_service.config,
|
|
119
|
+
date_string=datetime.now().strftime("%A %B %d, %Y"),
|
|
120
|
+
exposed_params_cls=self.exposed_params_cls,
|
|
121
|
+
)
|
|
129
122
|
|
|
130
123
|
@override
|
|
131
124
|
def tool_description(self) -> LanguageModelToolDescription:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
self.tool_parameter_calls = WebSearchToolParameters.with_exposed_params(
|
|
135
|
-
exposed
|
|
136
|
-
)
|
|
137
|
-
else:
|
|
138
|
-
if self.config.web_search_mode_config.mode == WebSearchMode.V3:
|
|
139
|
-
self.tool_parameter_calls = (
|
|
140
|
-
WebSearchV3ToolParameters.with_exposed_params(exposed)
|
|
141
|
-
)
|
|
142
|
-
else:
|
|
143
|
-
engine_mode = self._resolve_search_engine_mode()
|
|
144
|
-
self.tool_parameter_calls = WebSearchPlan.with_search_engine_mode(
|
|
145
|
-
engine_mode
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
tool_description = self.config.web_search_mode_config.tool_description
|
|
149
|
-
|
|
150
|
-
if self.config.web_search_mode_config.mode == WebSearchMode.V3:
|
|
151
|
-
tool_description = Template(tool_description).render(
|
|
152
|
-
tool_parameters_schema=WebSearchV3ToolParameters.schema_hint(exposed),
|
|
153
|
-
)
|
|
154
|
-
|
|
125
|
+
ctx = self._tool_context()
|
|
126
|
+
self.tool_parameter_calls = self._mode_strategy.build_tool_parameters(ctx)
|
|
155
127
|
return LanguageModelToolDescription(
|
|
156
128
|
name=self.name,
|
|
157
|
-
description=tool_description,
|
|
129
|
+
description=self._mode_strategy.tool_description(ctx),
|
|
158
130
|
parameters=self.tool_parameter_calls,
|
|
159
131
|
)
|
|
160
132
|
|
|
161
133
|
def tool_description_for_system_prompt(self) -> str:
|
|
162
|
-
|
|
163
|
-
if mode_config.mode == WebSearchMode.V1:
|
|
164
|
-
return mode_config.tool_description_for_system_prompt
|
|
165
|
-
|
|
166
|
-
template_str = mode_config.tool_description_for_system_prompt
|
|
167
|
-
|
|
168
|
-
if mode_config.mode == WebSearchMode.V3:
|
|
169
|
-
return Template(template_str).render(
|
|
170
|
-
date_string=datetime.now().strftime("%A %B %d, %Y"),
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
# Backwards compatibility: V2 prompts persisted before the Jinja
|
|
174
|
-
# migration use the legacy ``$max_steps`` placeholder. Jinja would
|
|
175
|
-
# otherwise pass it through verbatim, so rewrite it to the Jinja
|
|
176
|
-
# equivalent before rendering.
|
|
177
|
-
if "$max_steps" in template_str:
|
|
178
|
-
_LOGGER.warning(
|
|
179
|
-
"V2 web-search prompt contains legacy '$max_steps' placeholder; "
|
|
180
|
-
"rewriting to '{{ max_steps }}'. Please update the stored "
|
|
181
|
-
"configuration to use Jinja syntax."
|
|
182
|
-
)
|
|
183
|
-
template_str = template_str.replace("$max_steps", "{{ max_steps }}")
|
|
184
|
-
|
|
185
|
-
engine_mode = self._resolve_search_engine_mode()
|
|
186
|
-
return Template(template_str).render(
|
|
187
|
-
max_steps=mode_config.max_steps,
|
|
188
|
-
date_string=datetime.now().strftime("%A %B %d, %Y"),
|
|
189
|
-
search_engine_mode=engine_mode.value,
|
|
190
|
-
tool_parameters_schema=WebSearchPlan.schema_hint(engine_mode),
|
|
191
|
-
example_simple=WebSearchPlan.build_example_simple(
|
|
192
|
-
engine_mode
|
|
193
|
-
).model_dump_json(indent=2),
|
|
194
|
-
example_complex=WebSearchPlan.build_example_complex(
|
|
195
|
-
engine_mode
|
|
196
|
-
).model_dump_json(indent=2),
|
|
197
|
-
)
|
|
134
|
+
return self._mode_strategy.system_prompt(self._tool_context())
|
|
198
135
|
|
|
199
136
|
def tool_format_information_for_system_prompt(self) -> str:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
137
|
+
return self._mode_strategy.format_information_for_system_prompt(
|
|
138
|
+
default=self.config.tool_format_information_for_system_prompt,
|
|
139
|
+
)
|
|
203
140
|
|
|
204
141
|
def evaluation_check_list(self) -> list[EvaluationMetricName]:
|
|
205
142
|
return self.config.evaluation_check_list
|
|
@@ -301,18 +238,16 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
301
238
|
def _get_executor(
|
|
302
239
|
self,
|
|
303
240
|
tool_call: LanguageModelFunction,
|
|
304
|
-
parameters:
|
|
241
|
+
parameters: WebSearchToolParametersInstance,
|
|
305
242
|
debug_info: WebSearchDebugInfo,
|
|
306
243
|
web_search_message_logger: WebSearchMessageLogger,
|
|
307
|
-
) ->
|
|
308
|
-
# Initialize query elicitation service and get callbacks
|
|
244
|
+
) -> WebSearchExecutor:
|
|
309
245
|
elicitation_service = QueryElicitationService(
|
|
310
246
|
chat_service=self._chat_service,
|
|
311
247
|
display_name=self.display_name(),
|
|
312
248
|
config=self.config.query_elicitation_config,
|
|
313
249
|
)
|
|
314
250
|
|
|
315
|
-
# Build context objects
|
|
316
251
|
services = ExecutorServiceContext(
|
|
317
252
|
search_engine_service=self.search_engine_service,
|
|
318
253
|
crawler_service=self.crawler_service,
|
|
@@ -334,46 +269,14 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
334
269
|
tool_progress_reporter=self._ff_tool_progress_reporter(),
|
|
335
270
|
)
|
|
336
271
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
callbacks=callbacks,
|
|
346
|
-
tool_call=tool_call,
|
|
347
|
-
tool_parameters=parameters,
|
|
348
|
-
exposed_params_cls=self.exposed_params_cls,
|
|
349
|
-
)
|
|
350
|
-
if isinstance(parameters, WebSearchPlan):
|
|
351
|
-
assert search_config.mode == WebSearchMode.V2
|
|
352
|
-
return WebSearchV2Executor(
|
|
353
|
-
services=services,
|
|
354
|
-
config=config,
|
|
355
|
-
callbacks=callbacks,
|
|
356
|
-
tool_call=tool_call,
|
|
357
|
-
tool_parameters=parameters,
|
|
358
|
-
max_steps=search_config.max_steps,
|
|
359
|
-
exposed_params_cls=self.exposed_params_cls,
|
|
360
|
-
)
|
|
361
|
-
elif isinstance(parameters, WebSearchToolParameters):
|
|
362
|
-
assert search_config.mode == WebSearchMode.V1
|
|
363
|
-
return WebSearchV1Executor(
|
|
364
|
-
services=services,
|
|
365
|
-
config=config,
|
|
366
|
-
callbacks=callbacks,
|
|
367
|
-
tool_call=tool_call,
|
|
368
|
-
tool_parameters=parameters,
|
|
369
|
-
refine_query_system_prompt=search_config.refine_query_mode.system_prompt,
|
|
370
|
-
refine_query_language_model=search_config.refine_query_mode.language_model,
|
|
371
|
-
mode=search_config.refine_query_mode.mode,
|
|
372
|
-
max_queries=search_config.max_queries,
|
|
373
|
-
exposed_params_cls=self.exposed_params_cls,
|
|
374
|
-
)
|
|
375
|
-
else:
|
|
376
|
-
raise ValueError(f"Invalid parameters: {parameters}")
|
|
272
|
+
return self._mode_strategy.build_executor(
|
|
273
|
+
services=services,
|
|
274
|
+
config=config,
|
|
275
|
+
callbacks=callbacks,
|
|
276
|
+
tool_call=tool_call,
|
|
277
|
+
parameters=parameters,
|
|
278
|
+
exposed_params_cls=self.exposed_params_cls,
|
|
279
|
+
)
|
|
377
280
|
|
|
378
281
|
def get_evaluation_checks_based_on_tool_response(
|
|
379
282
|
self,
|
|
@@ -429,20 +332,12 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
429
332
|
|
|
430
333
|
def _build_display_name(
|
|
431
334
|
self,
|
|
432
|
-
parameters:
|
|
335
|
+
parameters: WebSearchToolParametersInstance,
|
|
433
336
|
) -> str:
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
# Remove "search" from the display name (case-insensitive, all occurrences).
|
|
439
|
-
display_name = re.sub(
|
|
440
|
-
r"\bsearch\b", "", display_name, flags=re.IGNORECASE
|
|
441
|
-
).strip()
|
|
442
|
-
|
|
443
|
-
suffix = parameters.get_display_name_suffix()
|
|
444
|
-
|
|
445
|
-
return display_name + suffix
|
|
337
|
+
return self._mode_strategy.build_display_name(
|
|
338
|
+
base_display_name=self.display_name(),
|
|
339
|
+
parameters=parameters,
|
|
340
|
+
)
|
|
446
341
|
|
|
447
342
|
|
|
448
343
|
ToolFactory.register_tool(WebSearchTool, WebSearchConfig)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Crawlers
|
|
2
|
+
|
|
3
|
+
Web-page readers used by Unique Web Search to fetch and convert URLs into readable text.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
Crawler configuration lives in **proxy-core** (`unique_search_proxy_core.crawlers`). The tool consumes those configs, registers implementations via `CRAWLER_REGISTRY`, and centralizes proxy dispatch in `BaseCrawler` — the same pattern as search engines.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart LR
|
|
11
|
+
Config["proxy-core *Config\n(crawler discriminator)"]
|
|
12
|
+
Registry["CRAWLER_REGISTRY"]
|
|
13
|
+
Base["BaseCrawler._proxy_crawl"]
|
|
14
|
+
SDK["client.crawl.crawl(crawler, urls, **config)"]
|
|
15
|
+
Legacy["_legacy_crawl\n(direct provider call)"]
|
|
16
|
+
|
|
17
|
+
Config --> Registry
|
|
18
|
+
Registry --> Base
|
|
19
|
+
Base -->|"proxy enabled"| SDK
|
|
20
|
+
Base -->|"proxy disabled"| Legacy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Key invariants
|
|
24
|
+
|
|
25
|
+
- **No exposable params.** Crawlers receive URLs only; deployment defaults come from the config. There is no LLM override surface.
|
|
26
|
+
- **Proxy request = config fields + urls.** `BaseCrawler._proxy_crawl` dumps `config.model_dump(exclude={"crawler"})` into the generic SDK call. No merge helper.
|
|
27
|
+
- **Legacy fallback retained.** When the search proxy is disabled, each crawler implements `_legacy_crawl` against the provider SDK / HTTP client.
|
|
28
|
+
|
|
29
|
+
## Available crawlers
|
|
30
|
+
|
|
31
|
+
| Name | Config | Auth | Notes |
|
|
32
|
+
|------|--------|------|-------|
|
|
33
|
+
| Basic | tool-local `BasicConfig` (extends proxy-core) | None | HTTP fetch + markdownify; content-type toggles; `url_blocked_patterns` (to be decommissioned) |
|
|
34
|
+
| Tavily | `TavilyConfig` | `TAVILY_API_KEY` | Extract API; `extract_depth`, `format`, … |
|
|
35
|
+
| Jina | `JinaConfig` | `JINA_API_KEY` | Reader API; typed `return_format` / `engine` / `do_not_track` |
|
|
36
|
+
| Firecrawl | `FirecrawlConfig` | `FIRECRAWL_API_KEY` | Batch scrape; `only_main_content`, … |
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
`WebSearchConfig.crawler_config` is a discriminated union on `crawler` (`"Basic"` / `"Tavily"` / `"Jina"` / `"Firecrawl"`). Active crawlers are selected via `active_inhouse_crawlers` plus API-key auto-append in settings.
|
|
41
|
+
|
|
42
|
+
### Example
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from unique_search_proxy_core.crawlers import CrawlerType
|
|
46
|
+
from unique_web_search.services.crawlers import BasicConfig, get_crawler_service
|
|
47
|
+
|
|
48
|
+
config = BasicConfig(crawler=CrawlerType.BASIC, timeout=30)
|
|
49
|
+
crawler = get_crawler_service(config)
|
|
50
|
+
markdowns = await crawler.crawl(["https://example.com"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Adding a crawler
|
|
54
|
+
|
|
55
|
+
1. Define the deployment config + request model in proxy-core (`unique_search_proxy_core.crawlers.<name>`).
|
|
56
|
+
2. Add a tool module under `services/crawlers/` that `@register_crawler(...)`s an implementation with `_legacy_crawl` only.
|
|
57
|
+
3. Modules are auto-discovered — no `__init__.py` factory edit needed.
|
|
58
|
+
4. Add the config/impl types to the static unions in `__init__.py` (for pyright) and cover them in `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,62 @@
|
|
|
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.crawlers.base import BaseCrawlerConfig
|
|
7
|
+
from unique_search_proxy_core.url_safety import (
|
|
8
|
+
CrawlTargetValidationError,
|
|
9
|
+
ResolvedCrawlTarget,
|
|
10
|
+
UrlSafetyService,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from unique_web_search.metrics import crawl_blocked
|
|
14
|
+
from unique_web_search.services.proxy.bridge import (
|
|
15
|
+
open_search_proxy_client,
|
|
16
|
+
search_proxy_client_enabled,
|
|
17
|
+
)
|
|
18
|
+
from unique_web_search.services.proxy.mappers import map_crawl_response
|
|
19
|
+
|
|
20
|
+
CrawlerConfig = TypeVar(
|
|
21
|
+
"CrawlerConfig",
|
|
22
|
+
bound=BaseCrawlerConfig,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseCrawler(ABC, Generic[CrawlerConfig]):
|
|
27
|
+
"""Base class for web-page crawlers.
|
|
28
|
+
|
|
29
|
+
The search-proxy path is implemented entirely here. Subclasses only provide
|
|
30
|
+
the direct ``_legacy_crawl`` implementation used when the proxy is disabled.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: CrawlerConfig):
|
|
34
|
+
self.config = config
|
|
35
|
+
|
|
36
|
+
async def crawl(self, urls: list[str]) -> list[str]:
|
|
37
|
+
if search_proxy_client_enabled:
|
|
38
|
+
return await self._proxy_crawl(urls)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
targets = await UrlSafetyService.validate_batch_urls(urls)
|
|
42
|
+
except CrawlTargetValidationError as exc:
|
|
43
|
+
for target in exc.blocked_targets:
|
|
44
|
+
crawl_blocked.labels(reason_category=target.category).inc()
|
|
45
|
+
raise
|
|
46
|
+
return await self._legacy_crawl(targets)
|
|
47
|
+
|
|
48
|
+
async def _proxy_crawl(self, urls: list[str]) -> list[str]:
|
|
49
|
+
"""Dump deployment config fields into the generic proxy crawl call."""
|
|
50
|
+
params = self.config.model_dump(exclude={"crawler"}, exclude_none=True)
|
|
51
|
+
async with open_search_proxy_client(
|
|
52
|
+
timeout=float(self.config.timeout)
|
|
53
|
+
) as client:
|
|
54
|
+
response = await client.crawl.crawl(
|
|
55
|
+
urls=urls,
|
|
56
|
+
crawler=self.config.crawler,
|
|
57
|
+
**params,
|
|
58
|
+
)
|
|
59
|
+
return map_crawl_response(response, urls)
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]: ...
|