unique_web_search 2026.30.0.dev2__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.dev2 → unique_web_search-2026.30.0.dev3}/PKG-INFO +4 -7
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/README.md +1 -2
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/pyproject.toml +3 -5
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/config.py +1 -1
- 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/tavily.py +21 -41
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/executor.py +2 -2
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/executor.py +2 -2
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/executor.py +1 -1
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/README.md +1 -1
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/settings.py +1 -1
- 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 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/client_settings.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/service.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/_registry.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/argument_screening/service.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/client/proxy_config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/content_processing/service.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/README.md +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/base_config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/base_executor.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/context.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/modes.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/config.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/bridge.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/mappers.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/query_elicitation.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/base.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/bing.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/brave.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/custom_api.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/google.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/registry.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → 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.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/snippet_judge/service.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/structured_llm.py +0 -0
- {unique_web_search-2026.30.0.dev2 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/skills/web-search-v3.md +0 -0
- {unique_web_search-2026.30.0.dev2 → 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(
|
|
@@ -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]: ...
|
|
@@ -1,84 +1,82 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
import timeout_decorator
|
|
7
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.crawlers.base import CrawlerType
|
|
12
|
+
from unique_search_proxy_core.crawlers.basic.content_types import (
|
|
13
|
+
CONTENT_TYPE_TOGGLE_TO_MIME,
|
|
14
|
+
)
|
|
15
|
+
from unique_search_proxy_core.crawlers.basic.schema import (
|
|
16
|
+
BasicConfig as CoreBasicConfig,
|
|
17
|
+
)
|
|
18
|
+
from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
|
|
11
19
|
|
|
12
20
|
from unique_web_search.services.client.proxy_config import async_client
|
|
13
|
-
from unique_web_search.services.crawlers.base import
|
|
14
|
-
|
|
15
|
-
BaseCrawlerConfig,
|
|
16
|
-
CrawlerType,
|
|
17
|
-
)
|
|
21
|
+
from unique_web_search.services.crawlers.base import BaseCrawler
|
|
22
|
+
from unique_web_search.services.crawlers.registry import register_crawler
|
|
18
23
|
from unique_web_search.services.crawlers.url_safety import (
|
|
19
24
|
CrawlTargetValidationError,
|
|
20
25
|
ResolvedCrawlTarget,
|
|
21
26
|
)
|
|
22
27
|
from unique_web_search.services.crawlers.utils import get_random_user_agent
|
|
23
|
-
from unique_web_search.services.proxy.bridge import
|
|
24
|
-
open_search_proxy_client,
|
|
25
|
-
)
|
|
28
|
+
from unique_web_search.services.proxy.bridge import open_search_proxy_client
|
|
26
29
|
from unique_web_search.services.proxy.mappers import map_crawl_response
|
|
27
30
|
|
|
28
31
|
_LOGGER = logging.getLogger(__name__)
|
|
29
32
|
|
|
30
|
-
unwanted_types = {
|
|
31
|
-
"application/octet-stream",
|
|
32
|
-
"application/pdf",
|
|
33
|
-
"application/msword",
|
|
34
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
35
|
-
"image/",
|
|
36
|
-
"video/",
|
|
37
|
-
"audio/",
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class BasicCrawlerConfig(BaseCrawlerConfig[CrawlerType.BASIC]):
|
|
42
|
-
crawler_type: Literal[CrawlerType.BASIC] = CrawlerType.BASIC
|
|
43
|
-
url_pattern_blacklist: list[str] = Field(
|
|
44
|
-
default=[r".*\.pdf$"],
|
|
45
|
-
description="List of URL patterns to blacklist",
|
|
46
|
-
)
|
|
47
|
-
unwanted_content_types: set[str] = Field(
|
|
48
|
-
default=unwanted_types,
|
|
49
|
-
description="Set of content types to not allow",
|
|
50
|
-
)
|
|
51
|
-
max_concurrent_requests: int = Field(
|
|
52
|
-
default=10,
|
|
53
|
-
description="The maximum number of concurrent requests to make to the same domain.",
|
|
54
|
-
)
|
|
55
33
|
|
|
34
|
+
class BasicConfig(CoreBasicConfig):
|
|
35
|
+
"""Tool-local Basic crawler config; extends proxy-core with legacy URL filters."""
|
|
36
|
+
|
|
37
|
+
url_blocked_patterns: Annotated[
|
|
38
|
+
list[str],
|
|
39
|
+
RJSFMetaTag({"ui:options": {"orderable": False}}),
|
|
40
|
+
] = Field(
|
|
41
|
+
default_factory=lambda: [r".*\.pdf$"],
|
|
42
|
+
title="URL blocked patterns",
|
|
43
|
+
description=(
|
|
44
|
+
"List of URL regex patterns to skip when crawling. "
|
|
45
|
+
"**Note:** This field will be deprecated in the future."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
56
48
|
|
|
57
|
-
class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
|
|
58
|
-
supports_proxy_crawl = True
|
|
59
49
|
|
|
60
|
-
|
|
50
|
+
@register_crawler(
|
|
51
|
+
name="basic",
|
|
52
|
+
key=CrawlerType.BASIC,
|
|
53
|
+
config_cls=BasicConfig,
|
|
54
|
+
config_display_name="Basic",
|
|
55
|
+
)
|
|
56
|
+
class BasicCrawler(BaseCrawler[BasicConfig]):
|
|
57
|
+
def __init__(self, config: BasicConfig):
|
|
61
58
|
super().__init__(config)
|
|
62
59
|
|
|
63
60
|
self.semaphore: asyncio.Semaphore = asyncio.Semaphore(
|
|
64
61
|
self.config.max_concurrent_requests
|
|
65
62
|
)
|
|
66
63
|
|
|
67
|
-
# TODO: Find a good way for tracking
|
|
68
|
-
# @track(
|
|
69
|
-
# tags=["basic", "scrape"],
|
|
70
|
-
# )
|
|
71
64
|
@override
|
|
72
65
|
async def _proxy_crawl(self, urls: list[str]) -> list[str]:
|
|
66
|
+
"""Proxy crawl excludes tool-only fields the search-proxy schema forbids."""
|
|
67
|
+
params = self.config.model_dump(
|
|
68
|
+
exclude={"crawler", "url_blocked_patterns"},
|
|
69
|
+
exclude_none=True,
|
|
70
|
+
)
|
|
73
71
|
async with open_search_proxy_client(
|
|
74
|
-
timeout=float(self.config.timeout)
|
|
72
|
+
timeout=float(self.config.timeout)
|
|
75
73
|
) as client:
|
|
76
|
-
response = await client.crawl.
|
|
74
|
+
response = await client.crawl.crawl(
|
|
77
75
|
urls=urls,
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
crawler=self.config.crawler,
|
|
77
|
+
**params,
|
|
80
78
|
)
|
|
81
|
-
|
|
79
|
+
return map_crawl_response(response, urls)
|
|
82
80
|
|
|
83
81
|
@override
|
|
84
82
|
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
|
|
@@ -111,8 +109,8 @@ class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
|
|
|
111
109
|
) -> str:
|
|
112
110
|
headers = {"User-Agent": get_random_user_agent()}
|
|
113
111
|
|
|
114
|
-
if self.
|
|
115
|
-
return "Unable to crawl URL due to
|
|
112
|
+
if self._is_url_blocked(target.normalized_url):
|
|
113
|
+
return "Unable to crawl URL due to blocked pattern"
|
|
116
114
|
|
|
117
115
|
request_headers = dict(headers)
|
|
118
116
|
request_extensions: dict[str, str] = {}
|
|
@@ -142,13 +140,18 @@ class BasicCrawler(BaseCrawler[BasicCrawlerConfig]):
|
|
|
142
140
|
|
|
143
141
|
return markdown
|
|
144
142
|
|
|
145
|
-
def
|
|
143
|
+
def _is_url_blocked(self, url: str) -> bool:
|
|
146
144
|
return any(
|
|
147
|
-
re.match(pattern, url) for pattern in self.config.
|
|
145
|
+
re.match(pattern, url) for pattern in self.config.url_blocked_patterns
|
|
148
146
|
)
|
|
149
147
|
|
|
150
148
|
def _content_type_not_allowed(self, content_type: str) -> bool:
|
|
151
|
-
|
|
149
|
+
allowed_mimes = {
|
|
150
|
+
mime
|
|
151
|
+
for field_name, mime in CONTENT_TYPE_TOGGLE_TO_MIME.items()
|
|
152
|
+
if getattr(self.config.content_types, field_name)
|
|
153
|
+
}
|
|
154
|
+
return content_type not in allowed_mimes
|
|
152
155
|
|
|
153
156
|
|
|
154
157
|
def _markdownify_html_with_timeout(content: str, timeout: float) -> str:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from firecrawl import AsyncFirecrawl
|
|
4
|
+
from unique_search_proxy_core.crawlers.base import CrawlerType
|
|
5
|
+
from unique_search_proxy_core.crawlers.firecrawl.schema import FirecrawlConfig
|
|
6
|
+
|
|
7
|
+
from unique_web_search.client_settings import (
|
|
8
|
+
get_firecrawl_search_settings,
|
|
9
|
+
)
|
|
10
|
+
from unique_web_search.services.crawlers.base import BaseCrawler
|
|
11
|
+
from unique_web_search.services.crawlers.registry import register_crawler
|
|
12
|
+
from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_crawler(
|
|
16
|
+
name="firecrawl",
|
|
17
|
+
key=CrawlerType.FIRECRAWL,
|
|
18
|
+
config_cls=FirecrawlConfig,
|
|
19
|
+
config_display_name="Firecrawl",
|
|
20
|
+
)
|
|
21
|
+
class FirecrawlCrawler(BaseCrawler[FirecrawlConfig]):
|
|
22
|
+
def __init__(self, config: FirecrawlConfig):
|
|
23
|
+
super().__init__(config)
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
|
|
27
|
+
urls = [target.normalized_url for target in targets]
|
|
28
|
+
|
|
29
|
+
api_key = get_firecrawl_search_settings().api_key
|
|
30
|
+
assert api_key is not None, "Firecrawl API key is not configured"
|
|
31
|
+
|
|
32
|
+
client = AsyncFirecrawl(api_key=api_key)
|
|
33
|
+
|
|
34
|
+
response = await client.batch_scrape(
|
|
35
|
+
urls=urls,
|
|
36
|
+
formats=["markdown"],
|
|
37
|
+
wait_timeout=self.config.timeout,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
results = response.data
|
|
41
|
+
|
|
42
|
+
markdowns = [
|
|
43
|
+
result.markdown
|
|
44
|
+
if result.markdown is not None
|
|
45
|
+
else "An error occurred while scraping the URL"
|
|
46
|
+
for result in results
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
return markdowns
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import Any,
|
|
2
|
+
from typing import Any, override
|
|
3
3
|
|
|
4
4
|
from httpx import AsyncClient
|
|
5
5
|
from pydantic import BaseModel, Field, HttpUrl
|
|
6
|
-
from unique_search_proxy_core.crawlers.
|
|
6
|
+
from unique_search_proxy_core.crawlers.base import CrawlerType
|
|
7
|
+
from unique_search_proxy_core.crawlers.jina.schema import JinaConfig
|
|
7
8
|
|
|
8
9
|
from unique_web_search.client_settings import get_jina_search_settings
|
|
9
|
-
from unique_web_search.services.crawlers.base import
|
|
10
|
-
|
|
11
|
-
BaseCrawlerConfigExperimental,
|
|
12
|
-
CrawlerType,
|
|
13
|
-
)
|
|
10
|
+
from unique_web_search.services.crawlers.base import BaseCrawler
|
|
11
|
+
from unique_web_search.services.crawlers.registry import register_crawler
|
|
14
12
|
from unique_web_search.services.crawlers.url_safety import ResolvedCrawlTarget
|
|
15
|
-
from unique_web_search.services.proxy.bridge import (
|
|
16
|
-
open_search_proxy_client,
|
|
17
|
-
)
|
|
18
|
-
from unique_web_search.services.proxy.mappers import map_crawl_response
|
|
19
13
|
|
|
20
14
|
|
|
21
15
|
class ReaderBody(BaseModel):
|
|
@@ -38,46 +32,16 @@ class ReaderResponse(BaseModel):
|
|
|
38
32
|
data: ReaderData | None = None
|
|
39
33
|
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
description="Headers to send with the request",
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
|
|
54
|
-
supports_proxy_crawl = True
|
|
55
|
-
|
|
56
|
-
def __init__(self, config: JinaCrawlerConfig):
|
|
35
|
+
@register_crawler(
|
|
36
|
+
name="jina",
|
|
37
|
+
key=CrawlerType.JINA,
|
|
38
|
+
config_cls=JinaConfig,
|
|
39
|
+
config_display_name="Jina",
|
|
40
|
+
)
|
|
41
|
+
class JinaCrawler(BaseCrawler[JinaConfig]):
|
|
42
|
+
def __init__(self, config: JinaConfig):
|
|
57
43
|
super().__init__(config)
|
|
58
44
|
|
|
59
|
-
# TODO: Find a solution for tracking
|
|
60
|
-
# @track(
|
|
61
|
-
# tags=["jina", "scrape"],
|
|
62
|
-
# )
|
|
63
|
-
@override
|
|
64
|
-
async def _proxy_crawl(self, urls: list[str]) -> list[str]:
|
|
65
|
-
headers = self.config.headers
|
|
66
|
-
async with open_search_proxy_client(
|
|
67
|
-
timeout=float(self.config.timeout),
|
|
68
|
-
) as client:
|
|
69
|
-
response = await client.crawl.jina(
|
|
70
|
-
urls=urls,
|
|
71
|
-
timeout=int(self.config.timeout),
|
|
72
|
-
return_format=cast(
|
|
73
|
-
JinaReturnFormat,
|
|
74
|
-
headers.get("X-Return-Format", "markdown"),
|
|
75
|
-
),
|
|
76
|
-
engine=cast(JinaEngine, headers.get("X-Engine", "browser")),
|
|
77
|
-
do_not_track=headers.get("DNT") == "1",
|
|
78
|
-
)
|
|
79
|
-
return map_crawl_response(response, urls)
|
|
80
|
-
|
|
81
45
|
@override
|
|
82
46
|
async def _legacy_crawl(self, targets: list[ResolvedCrawlTarget]) -> list[str]:
|
|
83
47
|
urls = [target.normalized_url for target in targets]
|
|
@@ -91,7 +55,7 @@ class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
|
|
|
91
55
|
"Authorization": f"Bearer {api_key}",
|
|
92
56
|
"Content-Type": "application/json",
|
|
93
57
|
"Accept": "application/json",
|
|
94
|
-
}
|
|
58
|
+
}
|
|
95
59
|
|
|
96
60
|
async with AsyncClient(timeout=self.config.timeout) as client:
|
|
97
61
|
tasks = [
|
|
@@ -116,6 +80,52 @@ class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
|
|
|
116
80
|
|
|
117
81
|
return markdown_results
|
|
118
82
|
|
|
83
|
+
def _build_reader_body(self, url: str) -> dict[str, Any]:
|
|
84
|
+
"""Build the Jina Reader POST body applying all deployment config fields.
|
|
85
|
+
|
|
86
|
+
Mirrors ``build_jina_reader_body`` on the search-proxy path so the direct
|
|
87
|
+
(proxy-disabled) crawl honours the same ``JinaConfig`` fields.
|
|
88
|
+
"""
|
|
89
|
+
config = self.config
|
|
90
|
+
page_timeout = config.page_timeout
|
|
91
|
+
if page_timeout is None:
|
|
92
|
+
page_timeout = min(max(config.timeout, 1), 180)
|
|
93
|
+
|
|
94
|
+
body: dict[str, Any] = {
|
|
95
|
+
"url": url,
|
|
96
|
+
"respondWith": config.return_format,
|
|
97
|
+
"engine": config.engine,
|
|
98
|
+
"timeout": page_timeout,
|
|
99
|
+
"doNotTrack": config.do_not_track,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if config.no_cache:
|
|
103
|
+
body["noCache"] = True
|
|
104
|
+
if config.target_selector is not None:
|
|
105
|
+
body["targetSelector"] = config.target_selector
|
|
106
|
+
if config.wait_for_selector is not None:
|
|
107
|
+
body["waitForSelector"] = config.wait_for_selector
|
|
108
|
+
if config.remove_selector is not None:
|
|
109
|
+
body["removeSelector"] = config.remove_selector
|
|
110
|
+
if config.with_generated_alt:
|
|
111
|
+
body["withGeneratedAlt"] = True
|
|
112
|
+
if config.with_links_summary:
|
|
113
|
+
body["withLinksSummary"] = True
|
|
114
|
+
if config.with_images_summary:
|
|
115
|
+
body["withImagesSummary"] = True
|
|
116
|
+
if config.with_iframe:
|
|
117
|
+
body["withIframe"] = True
|
|
118
|
+
if config.retain_images is not None:
|
|
119
|
+
body["retainImages"] = config.retain_images
|
|
120
|
+
if config.locale is not None:
|
|
121
|
+
body["locale"] = config.locale
|
|
122
|
+
if config.referer is not None:
|
|
123
|
+
body["referer"] = config.referer
|
|
124
|
+
if config.proxy_url is not None:
|
|
125
|
+
body["proxyUrl"] = config.proxy_url
|
|
126
|
+
|
|
127
|
+
return body
|
|
128
|
+
|
|
119
129
|
async def _crawl_url(
|
|
120
130
|
self,
|
|
121
131
|
url: str,
|
|
@@ -126,7 +136,7 @@ class JinaCrawler(BaseCrawler[JinaCrawlerConfig]):
|
|
|
126
136
|
params = {
|
|
127
137
|
"url": reader_api_endpoint,
|
|
128
138
|
"headers": headers,
|
|
129
|
-
"json":
|
|
139
|
+
"json": self._build_reader_body(url),
|
|
130
140
|
}
|
|
131
141
|
response = await client.post(**params)
|
|
132
142
|
return ReaderResponse.model_validate(response.json())
|