unique_web_search 2026.24.0.dev1__tar.gz → 2026.24.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.24.0.dev1 → unique_web_search-2026.24.0.dev3}/PKG-INFO +3 -2
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/pyproject.toml +3 -2
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/client_settings.py +35 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/prompts.py +21 -2
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v3/executor.py +4 -2
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +6 -2
- unique_web_search-2026.24.0.dev3/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +78 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v3/schema.py +41 -4
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/__init__.py +11 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/base.py +3 -0
- unique_web_search-2026.24.0.dev3/src/unique_web_search/services/search_engine/perplexity.py +121 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/settings.py +3 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/skills/web-search-v3.md +128 -29
- unique_web_search-2026.24.0.dev1/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -40
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/README.md +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/service.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/argument_screening/service.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/client/proxy_config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/content_processing/service.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/README.md +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/base.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/basic.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/crawl4ai.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/jina.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/tavily.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/dns.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/models.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/netloc.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/policy.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/redirect.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/resolver.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/service.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/url_safety/settings.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/README.md +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/base_config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/base_executor.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/context.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/executor.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/prompts/restrict_date_description.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v1/schema.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/executor.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v2/schema.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v3/config.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/query_elicitation.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/README.md +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/bing.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/brave.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/custom_api.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/firecrawl.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/google.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/jina.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/schema.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/tavily.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/snippet_judge/service.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/structured_llm.py +0 -0
- {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.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.24.0.
|
|
3
|
+
Version: 2026.24.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>
|
|
@@ -17,7 +17,7 @@ Requires-Dist: tavily-python>=0.7.11,<1
|
|
|
17
17
|
Requires-Dist: unidecode>=1.4.0,<2
|
|
18
18
|
Requires-Dist: azure-ai-projects>=1.0.0,<2
|
|
19
19
|
Requires-Dist: azure-identity>=1.25.0,<2
|
|
20
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.24.0.
|
|
20
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.24.0.dev10,<2026.24.0rc0
|
|
21
21
|
Requires-Dist: azure-core>=1.36.0,<2
|
|
22
22
|
Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
|
|
23
23
|
Requires-Dist: google-auth>=2.43.0,<3
|
|
@@ -27,6 +27,7 @@ Requires-Dist: httpx>=0.28.1
|
|
|
27
27
|
Requires-Dist: certifi>=2025.11.12
|
|
28
28
|
Requires-Dist: jinja2>=3.1.6
|
|
29
29
|
Requires-Dist: playwright>=1.58.0
|
|
30
|
+
Requires-Dist: perplexityai>=0.34.0
|
|
30
31
|
Requires-Python: >=3.12, <4.0
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_web_search"
|
|
3
|
-
version = "2026.24.0.
|
|
3
|
+
version = "2026.24.0.dev3"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12,<4.0"
|
|
@@ -23,7 +23,7 @@ dependencies = [
|
|
|
23
23
|
"unidecode>=1.4.0,<2",
|
|
24
24
|
"azure-ai-projects>=1.0.0,<2",
|
|
25
25
|
"azure-identity>=1.25.0,<2",
|
|
26
|
-
"unique-toolkit[monitoring]>=2026.24.0.
|
|
26
|
+
"unique-toolkit[monitoring]>=2026.24.0.dev10,<2026.24.0rc0",
|
|
27
27
|
"azure-core>=1.36.0,<2",
|
|
28
28
|
"google-cloud-aiplatform>=1.128.0,<2",
|
|
29
29
|
"google-auth>=2.43.0,<3",
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
|
33
33
|
"certifi>=2025.11.12",
|
|
34
34
|
"jinja2>=3.1.6",
|
|
35
35
|
"playwright>=1.58.0",
|
|
36
|
+
"perplexityai>=0.34.0",
|
|
36
37
|
]
|
|
37
38
|
|
|
38
39
|
[build-system]
|
|
@@ -200,3 +200,38 @@ def get_brave_search_settings() -> BraveSearchSettings:
|
|
|
200
200
|
if _brave_search_settings is None:
|
|
201
201
|
_brave_search_settings = BraveSearchSettings.from_env_settings()
|
|
202
202
|
return _brave_search_settings
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class PerplexitySearchSettings(BaseModel):
|
|
206
|
+
api_key: str | None = None
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def is_configured(self) -> bool:
|
|
210
|
+
return self.api_key is not None
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def from_env_settings(cls):
|
|
214
|
+
missing_settings = []
|
|
215
|
+
|
|
216
|
+
if env_settings.perplexity_api_key is None:
|
|
217
|
+
missing_settings.append("API Key")
|
|
218
|
+
|
|
219
|
+
if missing_settings:
|
|
220
|
+
_LOGGER.warning(
|
|
221
|
+
"Perplexity Search API missing required settings: "
|
|
222
|
+
f"{', '.join(missing_settings)}"
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
_LOGGER.info("Perplexity Search API is properly configured")
|
|
226
|
+
|
|
227
|
+
return cls(api_key=env_settings.perplexity_api_key)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
_perplexity_search_settings: PerplexitySearchSettings | None = None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_perplexity_search_settings() -> PerplexitySearchSettings:
|
|
234
|
+
global _perplexity_search_settings
|
|
235
|
+
if _perplexity_search_settings is None:
|
|
236
|
+
_perplexity_search_settings = PerplexitySearchSettings.from_env_settings()
|
|
237
|
+
return _perplexity_search_settings
|
|
@@ -39,11 +39,28 @@ When multiple sources are available, prioritize:
|
|
|
39
39
|
4. **Primary sources**: Original reports over summaries
|
|
40
40
|
5. **Domain expertise**: Medical info from health institutions, financial from finance sources
|
|
41
41
|
|
|
42
|
-
**When sources are insufficient**: Clearly state "Based on available search results..." and indicate limitations.
|
|
43
|
-
|
|
44
42
|
**REMEMBER**: Source numbers reset with each new WebSearch. Only use numbers from the most recent tool response.
|
|
45
43
|
"""
|
|
46
44
|
|
|
45
|
+
_TOOL_FORMAT_INFORMATION_SYNTHESIS_SECTION = """
|
|
46
|
+
## Synthesizing WebSearch results
|
|
47
|
+
|
|
48
|
+
After one or more WebSearch tool calls, **synthesize** findings in your answer. Do not only cite facts when a single source made the answer obvious.
|
|
49
|
+
|
|
50
|
+
**Evidence ledger (per facet or sub-question):** Track each facet with a confidence label:
|
|
51
|
+
|
|
52
|
+
- **Confirmed** — the exact fact is supported by cited tool content [sourceX].
|
|
53
|
+
- **Partial** — only part of the facet is supported; say what is missing [sourceX].
|
|
54
|
+
- **Not found** — no on-topic public evidence after reasonable search/refinement; state what you tried (no citation required for absence).
|
|
55
|
+
- **Related** — tool content is adjacent but does not answer the exact ask; cite it [sourceX] and label it **related, not confirmed**.
|
|
56
|
+
|
|
57
|
+
**Mandatory behaviors:**
|
|
58
|
+
|
|
59
|
+
- **Always summarize partial findings** before saying something is unavailable. Never reply with only "I couldn't find X" if the tool returned related material—put related material in a **Related leads** (or equivalent) subsection with citations.
|
|
60
|
+
- For multi-facet tasks (diligence, Source of Wealth, company profiles), use a structured answer: direct answer → evidence by facet → gaps and limitations → related leads → optional next steps.
|
|
61
|
+
- Distinguish **Confirmed** from **Related** in prose—do not imply ownership, revenue, or UBO facts unless citations support them.
|
|
62
|
+
""".strip()
|
|
63
|
+
|
|
47
64
|
|
|
48
65
|
_TOOL_FORMAT_INFORMATION_V3_DOMAIN_DIVERSITY_SECTION = """
|
|
49
66
|
## Domain Diversity Requirement
|
|
@@ -61,4 +78,6 @@ DEFAULT_TOOL_FORMAT_INFORMATION_FOR_SYSTEM_PROMPT_V3 = (
|
|
|
61
78
|
DEFAULT_TOOL_FORMAT_INFORMATION_FOR_SYSTEM_PROMPT.rstrip()
|
|
62
79
|
+ "\n\n"
|
|
63
80
|
+ _TOOL_FORMAT_INFORMATION_V3_DOMAIN_DIVERSITY_SECTION
|
|
81
|
+
+ "\n\n"
|
|
82
|
+
+ _TOOL_FORMAT_INFORMATION_SYNTHESIS_SECTION
|
|
64
83
|
)
|
|
@@ -58,11 +58,13 @@ class WebSearchV3Executor(BaseWebSearchExecutor[WebSearchV3ToolParameters]):
|
|
|
58
58
|
p = self.tool_parameters
|
|
59
59
|
if isinstance(p.payload, SearchPayload):
|
|
60
60
|
await self._message_log_callback.log_progress("_Executing Search_")
|
|
61
|
-
return await self._run_search(
|
|
61
|
+
return await self._run_search(
|
|
62
|
+
query=p.payload.query, objective=p.relevance_focus()
|
|
63
|
+
)
|
|
62
64
|
if isinstance(p.payload, FetchUrlsPayload):
|
|
63
65
|
await self._message_log_callback.log_progress("_Reading Web Pages_")
|
|
64
66
|
return await self._run_fetch_urls(
|
|
65
|
-
urls=p.payload.urls, objective=p.
|
|
67
|
+
urls=p.payload.urls, objective=p.relevance_focus()
|
|
66
68
|
)
|
|
67
69
|
|
|
68
70
|
msg = "WebSearchV3ToolParameters requires exactly one of 'query' or 'urls'."
|
|
@@ -3,7 +3,11 @@ The WebSearch tool returns up-to-date information from the web. It exposes two c
|
|
|
3
3
|
- **Search** (`command: "search"`) — runs the configured search engine and returns SERP rows (URL, domain, title, snippet). Gives a high-level read of what relevant pages contain without crawling them. Useful for **exploration** and quick lookups when a snippet is enough to answer.
|
|
4
4
|
- **Fetch** (`command: "read_urls"`) — crawls one or more URLs and returns full-page text. Useful for **deep dives** when the task is complex and needs exact figures, quotes, regulatory text, or specs, and when the user provides a URL directly.
|
|
5
5
|
|
|
6
|
-
Each call performs **one** operation. Drive multi-part tasks by chaining calls.
|
|
6
|
+
Each call performs **one** operation. Drive multi-part tasks by chaining calls; use **parallel** `search` calls for independent facets (short `query` each, not one long query).
|
|
7
|
+
|
|
8
|
+
**`phase`:** `exploratory` (map what exists) | `target` (precise facet in `gap`) | `redirect` (related context when the facet is not on the web). **`gap`:** one atomic facet per call.
|
|
9
|
+
|
|
10
|
+
**Complex research:** build an evidence ledger as you go and **synthesize** in the final answer—do not stop at "not found" if results contain related context.
|
|
7
11
|
|
|
8
12
|
### Fields
|
|
9
13
|
|
|
@@ -11,4 +15,4 @@ Each call performs **one** operation. Drive multi-part tasks by chaining calls.
|
|
|
11
15
|
|
|
12
16
|
Never invent URLs.
|
|
13
17
|
|
|
14
|
-
For
|
|
18
|
+
For the full playbook (search vs fetch, diligence decomposition, partial evidence, synthesis, URL-selection, anti-patterns, SoW examples), activate the `web-search-v3` skill before/while using this tool.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Use `WebSearch` for up-to-date information from the web. It exposes two capabilities, picked per call via `command`. Each invocation performs **one** operation; complex user tasks are handled by **chaining** invocations — use **parallel** `search` calls when several facets are independent (one short `query` per call, not one long query).
|
|
2
|
+
|
|
3
|
+
### `phase` — research phase (required on every call)
|
|
4
|
+
|
|
5
|
+
Set **`phase`** on each WebSearch invocation (replaces the old `objective` field):
|
|
6
|
+
|
|
7
|
+
- **`exploratory`** — sense what exists on the web and what is closest to the user's ask. Use broad, short mapping queries. Typical at the start of a complex task or when scoping an entity.
|
|
8
|
+
- **`target`** — pursue one specific facet named in `payload.gap` with a precise, short `payload.query`. Use for verifiable facts (revenue, ownership, officers, filings).
|
|
9
|
+
- **`redirect`** — last resort when the exact facet is not on the public web. Search for **related, attributable** context (sector, peers, parent company, adjacent news). Label results as related, not confirmed; never invent figures or ownership.
|
|
10
|
+
|
|
11
|
+
**`payload.gap`** — one atomic facet per call (not the whole user question). **`payload.query`** — ~3–8 keywords only; decompose multi-facet asks into parallel `search` calls instead of one bloated query.
|
|
12
|
+
|
|
13
|
+
#### `command: "search"` — search the web
|
|
14
|
+
|
|
15
|
+
Runs the configured search engine and returns SERP rows (URL, domain, title, snippet). It is **fast** and gives a **high-level read** of what relevant pages contain without paying the cost of crawling them.
|
|
16
|
+
|
|
17
|
+
Use it when:
|
|
18
|
+
|
|
19
|
+
- You need to **discover sources**: you don't yet have URLs and need the engine to surface candidates for a topic.
|
|
20
|
+
- The user's question can be answered from **snippets alone** — a definition, a status, a current value, a short factual lookup.
|
|
21
|
+
- You're at the **start of a complex task** and need to map out what's out there before deciding which pages are worth a deeper read.
|
|
22
|
+
- You want to **scope or sanity-check** a topic cheaply (does this entity / event / product even exist? how recent is the latest coverage?).
|
|
23
|
+
- You need to **cover a new gap / sub-question** in a multi-step task — issue another search rather than blindly fetching.
|
|
24
|
+
|
|
25
|
+
#### `command: "read_urls"` — fetch full page content
|
|
26
|
+
|
|
27
|
+
Crawls one or more URLs and returns the full-page text. It is **slower** than search but gives the **complete content** of the chosen pages, not just a snippet.
|
|
28
|
+
|
|
29
|
+
Use it when:
|
|
30
|
+
|
|
31
|
+
- The user **pasted a URL** or asked you to read a specific page — go straight here, do not search first.
|
|
32
|
+
- The task is **complex / in-depth** and needs more than a snippet: exact figures, full quotes, regulatory text, contract language, statute numbers, API specs, version numbers.
|
|
33
|
+
- The previous search returned **paraphrased / hedged snippets** ("around", "approximately", "is expected to", "reportedly") for a question that needs precision.
|
|
34
|
+
- The topic is **canonical-source-driven** — financial filings (10-K, 10-Q, earnings releases), regulator publications, official product docs, peer-reviewed papers, primary press releases — where the page itself is the source of truth.
|
|
35
|
+
- You found a **highly relevant** SERP hit but the title/snippet alone doesn't support the claim you want to make.
|
|
36
|
+
- The snippets from the previous "search" commands are relevant but do not fully answer the question or fill the gap and fetching the full page is necessary.
|
|
37
|
+
|
|
38
|
+
**Current date:** {{ date_string }}
|
|
39
|
+
|
|
40
|
+
### Balance exploration and exploitation
|
|
41
|
+
|
|
42
|
+
- **Explore first.** Start with `command: "search"` to get a cheap, high-level read of candidate pages. Cover one `gap` per call.
|
|
43
|
+
- **Refine, don't escalate prematurely.** If the first search did not fill the `gap`, issue another `command: "search"` with a refined query (different keywords, narrower entity, different timeframe) before reaching for `read_urls`.
|
|
44
|
+
- **Exploit when snippets aren't enough.** Switch to `command: "read_urls"` when snippets paraphrase / hedge, or the task needs exact figures, quotes, regulatory text, specs, or canonical-source content. Pick **1-3 high-signal URLs** from the most recent SERP — complementary domains preferred.
|
|
45
|
+
- **User-provided URLs go straight to fetch.** If the user pasted URL(s), the first call should be `command: "read_urls"`. Do not "search" for a URL the user already supplied.
|
|
46
|
+
- **Latency is a fair trade.** `read_urls` takes longer than `search`. That latency is acceptable on complex queries where the user expects depth — give them the answer that's actually grounded. Don't fetch when a snippet already answers the question; don't refuse to fetch when the snippet doesn't.
|
|
47
|
+
|
|
48
|
+
### WebSearch — Complex and diligence-style research
|
|
49
|
+
|
|
50
|
+
Use this pattern when the user needs **multiple facts about an entity** (ownership, revenue, officers, filings, sanctions, geography, industry context)—including Source of Wealth / KYC-style profiles where not everything is on the public web.
|
|
51
|
+
|
|
52
|
+
- **Before the first WebSearch call** (in your reasoning, not in tool args), decompose the task into **5–10 atomic gaps**—each gap is one verifiable facet (e.g. legal name and jurisdiction, revenue scale, ownership / UBO signals, key executives, recent filings, adverse media, industry peer context).
|
|
53
|
+
- **Execute gradually:** one `command: "search"` (and `read_urls` when snippets are insufficient) **per gap**, in a deliberate order (identity → scale → ownership → corroboration). Use `phase: "exploratory"` to map, then `phase: "target"` per facet; use `phase: "redirect"` only when a target facet has no public evidence after refinement.
|
|
54
|
+
- **After each tool round**, update a mental **evidence ledger** per facet: **Confirmed**, **Partial**, **Not found**, or **Related only**. Do not discard partial or adjacent SERP hits—they may matter in the final synthesis.
|
|
55
|
+
|
|
56
|
+
### WebSearch — When public evidence is missing or indirect
|
|
57
|
+
|
|
58
|
+
- **Do not stop** after one empty or weak SERP for a gap. Refine the query (keywords, entity disambiguation, registry/filing terms, language) before marking a gap **Not found**.
|
|
59
|
+
- If the **exact** fact is not published on the open web, still surface **related, attributable context** from search results: sector, approximate scale, parent/subsidiary links, adjacent filings, news mentioning the person or company, comparable peers. Label these clearly as **related, not confirmed** for the original ask.
|
|
60
|
+
- **Never invent** figures, ownership percentages, or ultimate-beneficial-owner claims. **Never** present related context as if it answered the exact question.
|
|
61
|
+
|
|
62
|
+
### WebSearch — Synthesizing before you answer
|
|
63
|
+
|
|
64
|
+
After WebSearch research (or when the user needs an interim summary), **synthesize**—do not dump raw tool output or answer only the facets that were easy to find.
|
|
65
|
+
|
|
66
|
+
For **complex** tasks, structure the answer as:
|
|
67
|
+
|
|
68
|
+
1. **Direct answer** — what you can support with citations.
|
|
69
|
+
2. **Evidence by facet** — facet → finding → confidence (**Confirmed** / **Partial** / **Related**) → [sourceX].
|
|
70
|
+
3. **Gaps and limitations** — what was not found on the public web and which searches you tried.
|
|
71
|
+
4. **Related leads** — indirect but useful pointers for the user's underlying goal (all cited).
|
|
72
|
+
5. **Suggested next steps** (optional)—e.g. corporate registry, paid databases, user-provided documents—only when appropriate.
|
|
73
|
+
|
|
74
|
+
For **simple** lookups, a short cited answer is enough; do not force the full structure.
|
|
75
|
+
|
|
76
|
+
For the deeper explore-then-exploit playbook (snippet hedging, decomposition, partial evidence, synthesis, URL-selection, anti-patterns, worked examples including Source of Wealth), activate the `web-search-v3` skill before/while using this tool.
|
|
77
|
+
|
|
78
|
+
Tool arguments are defined by the tool schema; the live JSON schema comes from the tool definition.
|
|
@@ -11,12 +11,29 @@ class Command(StrEnum):
|
|
|
11
11
|
FETCH_URLS = "read_urls"
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class SearchPhase(StrEnum):
|
|
15
|
+
"""How this WebSearch call fits in the research arc."""
|
|
16
|
+
|
|
17
|
+
EXPLORATORY = "exploratory"
|
|
18
|
+
TARGET = "target"
|
|
19
|
+
REDIRECT = "redirect"
|
|
20
|
+
|
|
21
|
+
|
|
14
22
|
class SearchPayload(BaseModel):
|
|
15
23
|
gap: str = Field(
|
|
16
|
-
description=
|
|
24
|
+
description=(
|
|
25
|
+
"One atomic facet this call addresses (e.g. '2023 revenue band for Company X in CH')—"
|
|
26
|
+
"not the whole user question. Pair with `phase`: exploratory scopes the topic; "
|
|
27
|
+
"target pursues this facet precisely; redirect seeks related context when the facet "
|
|
28
|
+
"is unlikely to be on the public web."
|
|
29
|
+
)
|
|
17
30
|
)
|
|
18
31
|
query: str = Field(
|
|
19
|
-
description=
|
|
32
|
+
description=(
|
|
33
|
+
"Short search-engine keyword line (~3–8 words, not a sentence). "
|
|
34
|
+
"Do not pack multiple facets into one query—issue parallel `search` calls with "
|
|
35
|
+
"one `gap` each instead. Do not paste the user question or `gap` text here."
|
|
36
|
+
)
|
|
20
37
|
)
|
|
21
38
|
|
|
22
39
|
|
|
@@ -35,14 +52,31 @@ class WebSearchV3ToolParameters(BaseModel):
|
|
|
35
52
|
description="The command to execute. Must be either 'search' or 'read_urls'."
|
|
36
53
|
)
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
description=
|
|
55
|
+
phase: SearchPhase = Field(
|
|
56
|
+
description=(
|
|
57
|
+
"Research phase for this call. "
|
|
58
|
+
"`exploratory` — sense what exists on the web and what is closest to the user's ask "
|
|
59
|
+
"(broad, mapping queries). "
|
|
60
|
+
"`target` — pursue one specific facet in `payload.gap` with a precise, short `query`. "
|
|
61
|
+
"`redirect` — last resort when the exact facet is not on the public web; find related, "
|
|
62
|
+
"attributable context (sector, peers, parent entity, adjacent news)—not invented facts."
|
|
63
|
+
)
|
|
40
64
|
)
|
|
41
65
|
|
|
42
66
|
payload: SearchPayload | FetchUrlsPayload = Field(
|
|
43
67
|
description="The payload of the command. Must be either a SearchPayload or a FetchUrlsPayload."
|
|
44
68
|
)
|
|
45
69
|
|
|
70
|
+
def relevance_focus(self) -> str:
|
|
71
|
+
"""Focus string for notify UI, snippet judging, and content processing."""
|
|
72
|
+
phase_label = self.phase.value
|
|
73
|
+
if isinstance(self.payload, SearchPayload):
|
|
74
|
+
return f"[{phase_label}] {self.payload.gap}"
|
|
75
|
+
urls_preview = ", ".join(self.payload.urls[:3])
|
|
76
|
+
if len(self.payload.urls) > 3:
|
|
77
|
+
urls_preview += ", …"
|
|
78
|
+
return f"[{phase_label}] {urls_preview}"
|
|
79
|
+
|
|
46
80
|
@classmethod
|
|
47
81
|
def schema_hint(cls) -> str:
|
|
48
82
|
"""Return a markdown-bulleted hint mirroring the field descriptions of this model.
|
|
@@ -68,6 +102,9 @@ class WebSearchV3ToolParameters(BaseModel):
|
|
|
68
102
|
if name == "command":
|
|
69
103
|
choices = " or ".join(f'`"{c.value}"`' for c in Command)
|
|
70
104
|
lines.append(f"- **`{name}`** — {choices}.")
|
|
105
|
+
elif name == "phase":
|
|
106
|
+
choices = " | ".join(f'`"{c.value}"`' for c in SearchPhase)
|
|
107
|
+
lines.append(f"- **`{name}`** — {choices}.")
|
|
71
108
|
elif name == "payload":
|
|
72
109
|
continue
|
|
73
110
|
else:
|
|
@@ -35,6 +35,10 @@ from unique_web_search.services.search_engine.jina import (
|
|
|
35
35
|
JinaConfig,
|
|
36
36
|
JinaSearch,
|
|
37
37
|
)
|
|
38
|
+
from unique_web_search.services.search_engine.perplexity import (
|
|
39
|
+
PerplexitySearch,
|
|
40
|
+
PerplexitySearchConfig,
|
|
41
|
+
)
|
|
38
42
|
from unique_web_search.services.search_engine.tavily import (
|
|
39
43
|
TavilyConfig,
|
|
40
44
|
TavilySearch,
|
|
@@ -51,6 +55,7 @@ SearchEngineTypes = (
|
|
|
51
55
|
| JinaSearch
|
|
52
56
|
| TavilySearch
|
|
53
57
|
| BraveSearch
|
|
58
|
+
| PerplexitySearch
|
|
54
59
|
| FireCrawlSearch
|
|
55
60
|
| VertexAI
|
|
56
61
|
)
|
|
@@ -61,6 +66,7 @@ SearchEngineConfigTypes = (
|
|
|
61
66
|
| JinaConfig
|
|
62
67
|
| TavilyConfig
|
|
63
68
|
| BraveSearchConfig
|
|
69
|
+
| PerplexitySearchConfig
|
|
64
70
|
| FireCrawlConfig
|
|
65
71
|
| VertexAIConfig
|
|
66
72
|
)
|
|
@@ -71,6 +77,7 @@ ENGINE_NAME_TO_CONFIG = {
|
|
|
71
77
|
"tavily": TavilyConfig,
|
|
72
78
|
"bing": BingSearchConfig,
|
|
73
79
|
"brave": BraveSearchConfig,
|
|
80
|
+
"perplexity": PerplexitySearchConfig,
|
|
74
81
|
"firecrawl": FireCrawlConfig,
|
|
75
82
|
"vertexai": VertexAIConfig,
|
|
76
83
|
"custom_api": CustomAPIConfig,
|
|
@@ -94,6 +101,8 @@ def get_search_engine_service(
|
|
|
94
101
|
return BingSearch(search_engine_config, language_model_service)
|
|
95
102
|
case SearchEngineType.BRAVE:
|
|
96
103
|
return BraveSearch(search_engine_config)
|
|
104
|
+
case SearchEngineType.PERPLEXITY:
|
|
105
|
+
return PerplexitySearch(search_engine_config)
|
|
97
106
|
case SearchEngineType.VERTEXAI:
|
|
98
107
|
return VertexAI(search_engine_config, language_model_service)
|
|
99
108
|
case SearchEngineType.CUSTOM_API:
|
|
@@ -140,6 +149,8 @@ __all__ = [
|
|
|
140
149
|
"BingSearch",
|
|
141
150
|
"BraveSearch",
|
|
142
151
|
"BraveSearchConfig",
|
|
152
|
+
"PerplexitySearch",
|
|
153
|
+
"PerplexitySearchConfig",
|
|
143
154
|
"get_search_engine_service",
|
|
144
155
|
"BaseSearchEngineConfig",
|
|
145
156
|
"SearchEngine",
|
|
@@ -21,6 +21,7 @@ class SearchEngineType(StrEnum):
|
|
|
21
21
|
FIRECRAWL = "Firecrawl"
|
|
22
22
|
TAVILY = "Tavily"
|
|
23
23
|
BRAVE = "Brave"
|
|
24
|
+
PERPLEXITY = "Perplexity"
|
|
24
25
|
BING = "Bing"
|
|
25
26
|
DUCKDUCKGO = "DuckDuckGo"
|
|
26
27
|
VERTEXAI = "VertexAI"
|
|
@@ -33,6 +34,7 @@ _SEARCH_ENGINE_MODE_MAP: dict[SearchEngineType, SearchEngineMode] = {
|
|
|
33
34
|
SearchEngineType.FIRECRAWL: SearchEngineMode.STANDARD,
|
|
34
35
|
SearchEngineType.TAVILY: SearchEngineMode.STANDARD,
|
|
35
36
|
SearchEngineType.BRAVE: SearchEngineMode.STANDARD,
|
|
37
|
+
SearchEngineType.PERPLEXITY: SearchEngineMode.STANDARD,
|
|
36
38
|
SearchEngineType.DUCKDUCKGO: SearchEngineMode.STANDARD,
|
|
37
39
|
SearchEngineType.BING: SearchEngineMode.AGENT,
|
|
38
40
|
SearchEngineType.VERTEXAI: SearchEngineMode.AGENT,
|
|
@@ -62,6 +64,7 @@ _SearchEngineExposedName = {
|
|
|
62
64
|
SearchEngineType.FIRECRAWL: "Firecrawl Search",
|
|
63
65
|
SearchEngineType.TAVILY: "Tavily Search",
|
|
64
66
|
SearchEngineType.BRAVE: "Brave Search",
|
|
67
|
+
SearchEngineType.PERPLEXITY: "Perplexity Search",
|
|
65
68
|
SearchEngineType.BING: "Grounding with Bing",
|
|
66
69
|
SearchEngineType.DUCKDUCKGO: "DuckDuckGo Search",
|
|
67
70
|
SearchEngineType.VERTEXAI: "Grounding with VertexAI",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Annotated, Literal, TypeVar
|
|
3
|
+
|
|
4
|
+
from perplexity import AsyncPerplexity, Omit
|
|
5
|
+
from perplexity.types.search_create_response import Result
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from unique_toolkit._common.pydantic_helpers import DeactivatedNone
|
|
8
|
+
|
|
9
|
+
from unique_web_search.client_settings import get_perplexity_search_settings
|
|
10
|
+
from unique_web_search.services.client.proxy_config import async_client
|
|
11
|
+
from unique_web_search.services.search_engine import (
|
|
12
|
+
BaseSearchEngineConfig,
|
|
13
|
+
SearchEngine,
|
|
14
|
+
SearchEngineType,
|
|
15
|
+
)
|
|
16
|
+
from unique_web_search.services.search_engine.base import get_search_engine_model_config
|
|
17
|
+
from unique_web_search.services.search_engine.schema import WebSearchResult
|
|
18
|
+
|
|
19
|
+
_LOGGER = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Perplexity Search API allows 1–20 results per request.
|
|
22
|
+
MAX_RESULTS_PER_REQUEST = 20
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PerplexitySearchConfig(BaseSearchEngineConfig[SearchEngineType.PERPLEXITY]):
|
|
26
|
+
model_config = get_search_engine_model_config(SearchEngineType.PERPLEXITY)
|
|
27
|
+
search_engine_name: Literal[SearchEngineType.PERPLEXITY] = (
|
|
28
|
+
SearchEngineType.PERPLEXITY
|
|
29
|
+
)
|
|
30
|
+
country: Annotated[str, Field(title="Active")] | DeactivatedNone = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
description="The country to use for the search. It's a two-letter country code.",
|
|
33
|
+
)
|
|
34
|
+
max_tokens: Annotated[int, Field(title="Active")] | DeactivatedNone = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="The maximum number of tokens to use for the context. It's a positive integer.",
|
|
37
|
+
)
|
|
38
|
+
search_mode: (
|
|
39
|
+
Annotated[Literal["web", "academic", "sec"], Field(title="Active")]
|
|
40
|
+
| DeactivatedNone
|
|
41
|
+
) = Field(
|
|
42
|
+
default=None,
|
|
43
|
+
description="The search mode to use for the search. It's a string.",
|
|
44
|
+
)
|
|
45
|
+
search_recency_filter: (
|
|
46
|
+
Annotated[
|
|
47
|
+
Literal["hour", "day", "week", "month", "year"], Field(title="Active")
|
|
48
|
+
]
|
|
49
|
+
| DeactivatedNone
|
|
50
|
+
) = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description="The recency filter to use for the search. It's a string.",
|
|
53
|
+
)
|
|
54
|
+
search_type: (
|
|
55
|
+
Annotated[Literal["web", "people"], Field(title="Active")] | DeactivatedNone
|
|
56
|
+
) = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description="The search type to use for the search. It's a string.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
requires_scraping: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PerplexitySearch(SearchEngine[PerplexitySearchConfig]):
|
|
65
|
+
def __init__(self, *args, **kwargs):
|
|
66
|
+
super().__init__(*args, **kwargs)
|
|
67
|
+
self.is_configured = get_perplexity_search_settings().is_configured
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def requires_scraping(self) -> bool:
|
|
71
|
+
return self.config.requires_scraping
|
|
72
|
+
|
|
73
|
+
async def search(self, query: str, **kwargs) -> list[WebSearchResult]:
|
|
74
|
+
settings = get_perplexity_search_settings()
|
|
75
|
+
assert settings.is_configured
|
|
76
|
+
|
|
77
|
+
max_results = min(self.config.fetch_size, MAX_RESULTS_PER_REQUEST)
|
|
78
|
+
|
|
79
|
+
async with async_client() as http_client:
|
|
80
|
+
perplexity_client = AsyncPerplexity(
|
|
81
|
+
api_key=settings.api_key, http_client=http_client
|
|
82
|
+
)
|
|
83
|
+
response = await perplexity_client.search.create(
|
|
84
|
+
query=query,
|
|
85
|
+
max_results=max_results,
|
|
86
|
+
country=_return_omit_if_none(self.config.country),
|
|
87
|
+
max_tokens=_return_omit_if_none(self.config.max_tokens),
|
|
88
|
+
search_mode=_return_omit_if_none(self.config.search_mode),
|
|
89
|
+
search_recency_filter=_return_omit_if_none(
|
|
90
|
+
self.config.search_recency_filter
|
|
91
|
+
),
|
|
92
|
+
search_type=_return_omit_if_none(self.config.search_type),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return self._to_web_search_results(response.results)
|
|
96
|
+
|
|
97
|
+
def _to_web_search_results(
|
|
98
|
+
self, results: list[Result] | None
|
|
99
|
+
) -> list[WebSearchResult]:
|
|
100
|
+
if not results:
|
|
101
|
+
_LOGGER.warning("No search results found in Perplexity search response")
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
WebSearchResult(
|
|
106
|
+
url=result.url,
|
|
107
|
+
title=result.title,
|
|
108
|
+
snippet=result.snippet or "No Snippet Found",
|
|
109
|
+
)
|
|
110
|
+
for result in results
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
T = TypeVar("T")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _return_omit_if_none(value: T | None) -> T | Omit:
|
|
118
|
+
if value is None:
|
|
119
|
+
return Omit()
|
|
120
|
+
else:
|
|
121
|
+
return value
|
|
@@ -60,6 +60,9 @@ class Base(BaseSettings):
|
|
|
60
60
|
brave_search_api_key: str | None = None
|
|
61
61
|
brave_search_api_endpoint: str | None = None
|
|
62
62
|
|
|
63
|
+
# Perplexity API settings
|
|
64
|
+
perplexity_api_key: str | None = None
|
|
65
|
+
|
|
63
66
|
# VertexAI API settings
|
|
64
67
|
vertexai_service_account_credentials: str | None = None
|
|
65
68
|
vertexai_service_account_scopes: list[str] | None = None
|