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.
Files changed (128) hide show
  1. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/PKG-INFO +3 -2
  2. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/pyproject.toml +3 -2
  3. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/client_settings.py +35 -0
  4. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/prompts.py +21 -2
  5. {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
  6. {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
  7. unique_web_search-2026.24.0.dev3/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +78 -0
  8. {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
  9. {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
  10. {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
  11. unique_web_search-2026.24.0.dev3/src/unique_web_search/services/search_engine/perplexity.py +121 -0
  12. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/settings.py +3 -0
  13. {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
  14. unique_web_search-2026.24.0.dev1/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -40
  15. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/README.md +0 -0
  16. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/__init__.py +0 -0
  17. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/config.py +0 -0
  18. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/metrics.py +0 -0
  19. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/schema.py +0 -0
  20. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/service.py +0 -0
  21. {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
  22. {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
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {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
  29. {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
  30. {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
  31. {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
  32. {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
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/README.md +0 -0
  55. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  56. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/base.py +0 -0
  57. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/basic.py +0 -0
  58. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/crawl4ai.py +0 -0
  59. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  60. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/jina.py +0 -0
  61. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/crawlers/utils.py +0 -0
  72. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/README.md +0 -0
  73. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/__init__.py +0 -0
  74. {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
  75. {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
  76. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/executors/context.py +0 -0
  77. {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
  78. {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
  79. {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
  80. {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
  81. {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
  82. {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
  83. {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
  84. {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
  85. {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
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {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
  94. {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
  95. {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
  96. {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
  97. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/helpers.py +0 -0
  98. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/message_log.py +0 -0
  99. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/query_elicitation.py +0 -0
  100. {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
  101. {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
  102. {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
  103. {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
  104. {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
  105. {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
  106. {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
  107. {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
  108. {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
  109. {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
  110. {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
  111. {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
  112. {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
  113. {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
  114. {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
  115. {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
  116. {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
  117. {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
  118. {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
  119. {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
  120. {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
  121. {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
  122. {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
  123. {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
  124. {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
  125. {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
  126. {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
  127. {unique_web_search-2026.24.0.dev1 → unique_web_search-2026.24.0.dev3}/src/unique_web_search/services/structured_llm.py +0 -0
  128. {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.dev1
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.dev7,<2026.24.0rc0
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.dev1"
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.dev7,<2026.24.0rc0",
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(query=p.payload.query, objective=p.objective)
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.objective
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 a deeper playbook on when to search vs fetch (snippet hedging, canonical-source topics, sub-question decomposition, URL-selection rules, anti-patterns), activate the `web-search-v3` skill before/while using this tool.
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="A brief description of the gap that the query is meant to fill"
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="Search query for the configured search engine. Query must be fixed for the entire rounds."
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
- objective: str = Field(
39
- description="One concise sentence: what this call is meant to accomplish."
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