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.
Files changed (131) hide show
  1. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/PKG-INFO +4 -7
  2. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/README.md +1 -2
  3. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/pyproject.toml +3 -5
  4. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/config.py +1 -1
  5. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/service.py +35 -140
  6. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/README.md +58 -0
  7. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/__init__.py +60 -0
  8. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/base.py +62 -0
  9. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/basic.py +54 -51
  10. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/firecrawl.py +49 -0
  11. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/jina.py +61 -51
  12. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/crawlers/registry.py +50 -0
  13. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/tavily.py +21 -41
  14. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/modes.py +164 -0
  15. {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
  16. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/v1/strategy.py +67 -0
  17. {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
  18. {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
  19. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/v2/strategy.py +79 -0
  20. {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
  21. {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
  22. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/executors/v3/strategy.py +100 -0
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {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
  29. unique_web_search-2026.30.0.dev3/src/unique_web_search/services/search_engine/registry.py +110 -0
  30. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/settings.py +1 -1
  31. unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/README.md +0 -360
  32. unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/__init__.py +0 -107
  33. unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/base.py +0 -77
  34. unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/crawl4ai.py +0 -326
  35. unique_web_search-2026.30.0.dev1/src/unique_web_search/services/crawlers/firecrawl.py +0 -70
  36. unique_web_search-2026.30.0.dev1/src/unique_web_search/services/search_engine/registry.py +0 -62
  37. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/__init__.py +0 -0
  38. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/client_settings.py +0 -0
  39. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/metrics.py +0 -0
  40. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/prompts.py +0 -0
  41. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/schema.py +0 -0
  42. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/_registry.py +0 -0
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {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
  77. {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
  78. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/crawlers/utils.py +0 -0
  79. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/README.md +0 -0
  80. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/__init__.py +0 -0
  81. {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
  82. {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
  83. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/executors/context.py +0 -0
  84. {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
  85. {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
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {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
  94. {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
  95. {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
  96. {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
  97. {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
  98. {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
  99. {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
  100. {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
  101. {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
  102. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/helpers.py +0 -0
  103. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/message_log.py +0 -0
  104. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/__init__.py +0 -0
  105. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/bridge.py +0 -0
  106. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/proxy/mappers.py +0 -0
  107. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/query_elicitation.py +0 -0
  108. {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
  109. {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
  110. {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
  111. {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
  112. {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
  113. {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
  114. {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
  115. {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
  116. {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
  117. {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
  118. {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
  119. {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
  120. {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
  121. {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
  122. {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
  123. {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
  124. {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
  125. {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
  126. {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
  127. {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
  128. {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
  129. {unique_web_search-2026.30.0.dev1 → unique_web_search-2026.30.0.dev3}/src/unique_web_search/services/structured_llm.py +0 -0
  130. {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
  131. {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.dev1
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.dev10,<2026.30.0rc0
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: playwright>=1.58.0
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) &mdash; full catalogue of supported engines, configuration, and usage examples.
88
- - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Crawl4AI, Tavily, Firecrawl, Jina) with setup guides.
85
+ - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Tavily, Firecrawl, Jina) with setup guides.
89
86
  - [Executors](./unique_web_search/services/executors/README.md) &mdash; 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) &mdash; full catalogue of supported engines, configuration, and usage examples.
54
- - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Crawl4AI, Tavily, Firecrawl, Jina) with setup guides.
53
+ - [Crawlers](./unique_web_search/services/crawlers/README.md) &mdash; comparison of crawling strategies (Basic, Tavily, Firecrawl, Jina) with setup guides.
55
54
  - [Executors](./unique_web_search/services/executors/README.md) &mdash; 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.dev1"
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.dev10,<2026.30.0rc0",
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
- "playwright>=1.58.0",
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="crawler_type",
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.v1.schema import WebSearchToolParameters
49
- from unique_web_search.services.executors.v2.schema import WebSearchPlan
50
- from unique_web_search.services.executors.v3.schema import WebSearchV3ToolParameters
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 _resolve_search_engine_mode(self) -> SearchEngineMode:
125
- """Derive the search-engine mode, respecting CustomAPI overrides."""
126
- cfg = self.search_engine_service.config
127
- override = cfg.search_engine_mode if isinstance(cfg, CustomAPIConfig) else None
128
- return get_search_engine_mode(cfg.engine, override=override)
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
- exposed = self.exposed_params_cls
133
- if self.config.web_search_mode_config.mode == WebSearchMode.V1:
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
- mode_config = self.config.web_search_mode_config
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
- if self.config.web_search_active_mode == WebSearchMode.V3:
201
- return self.config.web_search_mode_config_v3.tool_format_information_for_system_prompt
202
- return self.config.tool_format_information_for_system_prompt
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: WebSearchPlan | WebSearchToolParameters | WebSearchV3ToolParameters,
241
+ parameters: WebSearchToolParametersInstance,
305
242
  debug_info: WebSearchDebugInfo,
306
243
  web_search_message_logger: WebSearchMessageLogger,
307
- ) -> WebSearchV1Executor | WebSearchV2Executor | WebSearchV3Executor:
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
- search_config = self.config.web_search_mode_config
338
- if search_config.mode == WebSearchMode.V3:
339
- assert isinstance(search_config, WebSearchV3Config)
340
- assert isinstance(parameters, WebSearchV3ToolParameters)
341
-
342
- return WebSearchV3Executor(
343
- services=services,
344
- config=config,
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: WebSearchPlan | WebSearchToolParameters | WebSearchV3ToolParameters,
335
+ parameters: WebSearchToolParametersInstance,
433
336
  ) -> str:
434
- if not isinstance(parameters, WebSearchV3ToolParameters):
435
- return self.display_name()
436
-
437
- display_name = self.display_name()
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]: ...