unique_web_search 2026.32.0.dev0__tar.gz → 2026.32.0.dev2__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 (127) hide show
  1. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/PKG-INFO +5 -4
  2. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/pyproject.toml +6 -5
  3. unique_web_search-2026.32.0.dev2/src/unique_web_search/invocation_stats.py +113 -0
  4. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/service.py +26 -1
  5. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/service.py +1 -0
  6. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +13 -0
  7. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +6 -0
  8. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +6 -0
  9. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/base_executor.py +11 -0
  10. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/executor.py +6 -0
  11. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/bing.py +9 -8
  12. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +41 -10
  13. unique_web_search-2026.32.0.dev2/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +361 -0
  14. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +6 -0
  15. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/vertexai.py +6 -0
  16. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/service.py +6 -0
  17. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/structured_llm.py +8 -0
  18. unique_web_search-2026.32.0.dev0/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -201
  19. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/README.md +0 -0
  20. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/__init__.py +0 -0
  21. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/client_settings.py +0 -0
  22. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/config.py +0 -0
  23. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/metrics.py +0 -0
  24. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/prompts.py +0 -0
  25. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/schema.py +0 -0
  26. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/_registry.py +0 -0
  27. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  28. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/config.py +0 -0
  29. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  30. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  31. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  32. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  33. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  34. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  35. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  36. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/client/proxy_config.py +0 -0
  37. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  38. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  39. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  40. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  41. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  42. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/config.py +0 -0
  43. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  44. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  45. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  46. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  47. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  48. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  49. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  50. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  51. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  52. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  53. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  54. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  55. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  56. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/service.py +0 -0
  57. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/README.md +0 -0
  58. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  59. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/base.py +0 -0
  60. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/basic.py +0 -0
  61. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  62. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/jina.py +0 -0
  63. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/registry.py +0 -0
  64. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  65. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  66. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/utils.py +0 -0
  67. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/README.md +0 -0
  68. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/__init__.py +0 -0
  69. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/base_config.py +0 -0
  70. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/context.py +0 -0
  71. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/modes.py +0 -0
  72. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  73. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/config.py +0 -0
  74. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  75. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  76. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  77. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  78. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  79. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  80. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  81. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/config.py +0 -0
  82. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/executor.py +0 -0
  83. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  84. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  85. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  86. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  87. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  88. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  89. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  90. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/config.py +0 -0
  91. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  92. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  93. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  94. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  95. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  96. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  97. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/helpers.py +0 -0
  98. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/message_log.py +0 -0
  99. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/__init__.py +0 -0
  100. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/bridge.py +0 -0
  101. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/mappers.py +0 -0
  102. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/query_elicitation.py +0 -0
  103. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/README.md +0 -0
  104. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  105. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/base.py +0 -0
  106. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/brave.py +0 -0
  107. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/custom_api.py +0 -0
  108. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/google.py +0 -0
  109. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  110. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/registry.py +0 -0
  111. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/schema.py +0 -0
  112. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  113. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  114. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  115. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  116. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  117. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  118. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  119. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  120. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  121. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  122. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  123. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  124. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  125. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/settings.py +0 -0
  126. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/skills/web-search-v3.md +0 -0
  127. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/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.32.0.dev0
3
+ Version: 2026.32.0.dev2
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>
@@ -14,9 +14,9 @@ Requires-Dist: fake-useragent>=2.2.0,<3
14
14
  Requires-Dist: firecrawl>=3.3.2,<4
15
15
  Requires-Dist: tavily-python>=0.7.11,<1
16
16
  Requires-Dist: unidecode>=1.4.0,<2
17
- Requires-Dist: azure-ai-projects>=1.0.0,<2
17
+ Requires-Dist: azure-ai-projects>=2.3,<3
18
18
  Requires-Dist: azure-identity>=1.25.0,<2
19
- Requires-Dist: unique-toolkit[monitoring]>=2026.32.0.dev0,<2026.32.0rc0
19
+ Requires-Dist: unique-toolkit[monitoring]>=2026.32.0.dev4,<2026.32.0rc0
20
20
  Requires-Dist: azure-core>=1.36.0,<2
21
21
  Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
22
22
  Requires-Dist: google-auth>=2.43.0,<3
@@ -25,8 +25,9 @@ Requires-Dist: rapidfuzz>=3.0.0,<4
25
25
  Requires-Dist: httpx>=0.28.1
26
26
  Requires-Dist: certifi>=2025.11.12
27
27
  Requires-Dist: jinja2>=3.1.6
28
- Requires-Dist: unique-search-proxy-core>=2026.32.0.dev0,<2026.32.0rc0
28
+ Requires-Dist: unique-search-proxy-core>=2026.32.0.dev3,<2026.32.0rc0
29
29
  Requires-Dist: unique-search-proxy-sdk>=2026.32.0.dev0,<2026.32.0rc0
30
+ Requires-Dist: openai>=1.105.0,<3
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.32.0.dev0"
3
+ version = "2026.32.0.dev2"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -20,9 +20,9 @@ dependencies = [
20
20
  "firecrawl>=3.3.2,<4",
21
21
  "tavily-python>=0.7.11,<1",
22
22
  "unidecode>=1.4.0,<2",
23
- "azure-ai-projects>=1.0.0,<2",
23
+ "azure-ai-projects>=2.3,<3",
24
24
  "azure-identity>=1.25.0,<2",
25
- "unique-toolkit[monitoring]>=2026.32.0.dev0,<2026.32.0rc0",
25
+ "unique-toolkit[monitoring]>=2026.32.0.dev4,<2026.32.0rc0",
26
26
  "azure-core>=1.36.0,<2",
27
27
  "google-cloud-aiplatform>=1.128.0,<2",
28
28
  "google-auth>=2.43.0,<3",
@@ -31,8 +31,9 @@ dependencies = [
31
31
  "httpx>=0.28.1",
32
32
  "certifi>=2025.11.12",
33
33
  "jinja2>=3.1.6",
34
- "unique-search-proxy-core>=2026.32.0.dev0,<2026.32.0rc0",
34
+ "unique-search-proxy-core>=2026.32.0.dev3,<2026.32.0rc0",
35
35
  "unique-search-proxy-sdk>=2026.32.0.dev0,<2026.32.0rc0",
36
+ "openai>=1.105.0,<3",
36
37
  ]
37
38
 
38
39
  [build-system]
@@ -91,4 +92,4 @@ extend-select = ["I"]
91
92
  [tool.uv.sources]
92
93
  unique-toolkit = { workspace = true }
93
94
  unique-search-proxy-sdk = { workspace = true }
94
- unique-search-proxy-core = { workspace = true }
95
+ unique-search-proxy-core = { workspace = true }
@@ -0,0 +1,113 @@
1
+ """Run-scoped collection of language-model invocation usage."""
2
+
3
+ import logging
4
+ from collections.abc import Iterable, Iterator
5
+ from contextlib import contextmanager
6
+ from contextvars import ContextVar, Token
7
+ from typing import Any
8
+
9
+ from pydantic import ValidationError
10
+ from unique_toolkit.language_model.invocation_stats import LanguageModelInvocationStats
11
+ from unique_toolkit.language_model.schemas import LanguageModelTokenUsage
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+ _CURRENT_INVOCATION_STATS: ContextVar[list[LanguageModelInvocationStats] | None] = (
16
+ ContextVar("web_search_invocation_stats", default=None)
17
+ )
18
+
19
+
20
+ @contextmanager
21
+ def invocation_stats_scope() -> Iterator[list[LanguageModelInvocationStats]]:
22
+ """Collect stats for one WebSearch run and isolate concurrent runs."""
23
+ invocation_stats: list[LanguageModelInvocationStats] = []
24
+ token: Token[list[LanguageModelInvocationStats] | None] = (
25
+ _CURRENT_INVOCATION_STATS.set(invocation_stats)
26
+ )
27
+ try:
28
+ yield invocation_stats
29
+ finally:
30
+ _CURRENT_INVOCATION_STATS.reset(token)
31
+
32
+
33
+ def record_invocation_stats(
34
+ invocation_stats: Iterable[LanguageModelInvocationStats],
35
+ ) -> None:
36
+ """Record already-built invocation stats when a run scope is active.
37
+
38
+ Used for usage produced by nested dependencies that hand back
39
+ ``LanguageModelInvocationStats`` objects directly (e.g. the chunk relevancy
40
+ sorter attaching per-chunk stats on its result), rather than a raw provider
41
+ usage payload.
42
+ """
43
+ current_invocation_stats = _CURRENT_INVOCATION_STATS.get()
44
+ if current_invocation_stats is not None:
45
+ current_invocation_stats.extend(invocation_stats)
46
+
47
+
48
+ def record_token_usage(
49
+ *,
50
+ model_name: str,
51
+ usage: Any,
52
+ source: str,
53
+ ) -> None:
54
+ """Record provider usage when both a run scope and usage are available."""
55
+ invocation_stats = _CURRENT_INVOCATION_STATS.get()
56
+ if invocation_stats is None or usage is None:
57
+ return
58
+
59
+ try:
60
+ token_usage = LanguageModelTokenUsage.model_validate(usage)
61
+ except ValidationError:
62
+ _LOGGER.warning(
63
+ "Unable to parse Web Search token usage for %s",
64
+ source,
65
+ exc_info=True,
66
+ )
67
+ return
68
+
69
+ invocation_stats.append(
70
+ LanguageModelInvocationStats.from_usage(
71
+ model_name=model_name,
72
+ token_usage=token_usage,
73
+ source=source,
74
+ )
75
+ )
76
+
77
+
78
+ def record_language_model_response(
79
+ *,
80
+ model_name: str,
81
+ response: Any,
82
+ source: str,
83
+ ) -> None:
84
+ """Record usage from a Unique Toolkit language-model response."""
85
+ record_token_usage(
86
+ model_name=model_name,
87
+ usage=getattr(response, "usage", None),
88
+ source=source,
89
+ )
90
+
91
+
92
+ def record_vertex_response(
93
+ *,
94
+ model_name: str,
95
+ response: Any,
96
+ source: str,
97
+ ) -> None:
98
+ """Record Google GenAI usage metadata using toolkit token names."""
99
+ usage = getattr(response, "usage_metadata", None)
100
+ if usage is None:
101
+ return
102
+
103
+ record_token_usage(
104
+ model_name=model_name,
105
+ usage={
106
+ "prompt_tokens": getattr(usage, "prompt_token_count", None),
107
+ "completion_tokens": getattr(usage, "candidates_token_count", None),
108
+ "total_tokens": getattr(usage, "total_token_count", None),
109
+ "reasoning_tokens": getattr(usage, "thoughts_token_count", None),
110
+ "cached_tokens": getattr(usage, "cached_content_token_count", None),
111
+ },
112
+ source=source,
113
+ )
@@ -25,6 +25,7 @@ from unique_toolkit.language_model.schemas import (
25
25
  from unique_toolkit.monitoring import metric_scope
26
26
 
27
27
  from unique_web_search.config import WebSearchConfig
28
+ from unique_web_search.invocation_stats import invocation_stats_scope
28
29
  from unique_web_search.metrics import tool_duration, tool_empty_results, tool_errors
29
30
  from unique_web_search.schema import WebSearchDebugInfo
30
31
  from unique_web_search.services.argument_screening import (
@@ -104,7 +105,6 @@ class WebSearchTool(Tool[WebSearchConfig]):
104
105
  self._mode_strategy = get_mode_strategy(self.config.web_search_mode_config)
105
106
 
106
107
  def content_reducer(web_page_chunks: list[WebPageChunk]) -> list[WebPageChunk]:
107
-
108
108
  return reduce_sources_to_token_limit(
109
109
  web_page_chunks,
110
110
  self.config.language_model_max_input_tokens,
@@ -153,6 +153,31 @@ class WebSearchTool(Tool[WebSearchConfig]):
153
153
 
154
154
  @override
155
155
  async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
156
+ with invocation_stats_scope() as invocation_stats:
157
+ try:
158
+ response = await self._run(tool_call)
159
+ except Exception as e:
160
+ # _run() already catches its own errors and returns a
161
+ # ToolCallResponse gracefully in the common case; this is a
162
+ # safety net for the rarer case where something raises while
163
+ # handling that failure (or otherwise escapes _run()
164
+ # uncaught). Without it, tokens already spent on screening/
165
+ # processing/relevancy before the failure would never reach
166
+ # analytics, since SafeTaskExecutor builds its own fresh,
167
+ # stats-less error response one level up.
168
+ # logger.exception() already attaches the active exception's
169
+ # type and traceback; don't stringify it into the message.
170
+ _LOGGER.exception("WebSearch tool run failed")
171
+ return ToolCallResponse(
172
+ id=tool_call.id, # type: ignore
173
+ name=self.name,
174
+ error_message=str(e),
175
+ invocation_stats=invocation_stats,
176
+ )
177
+ response.invocation_stats.extend(invocation_stats)
178
+ return response
179
+
180
+ async def _run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
156
181
  _LOGGER.info("Running the WebSearch tool")
157
182
  start_time = time()
158
183
  parameters = self.tool_parameter_calls.model_validate(
@@ -116,6 +116,7 @@ class ArgumentScreeningService:
116
116
  system_message=self._config.system_prompt,
117
117
  user_message=user_prompt,
118
118
  response_model=ArgumentScreeningResult,
119
+ invocation_source="web_search.argument_screening",
119
120
  )
120
121
  except StructuredLlmUnparseableResponseError as e:
121
122
  raise ArgumentScreeningUnparseableResponseException(
@@ -6,6 +6,7 @@ from unique_toolkit._common.utils.jinja.render import render_template
6
6
  from unique_toolkit.language_model import LanguageModelService
7
7
  from unique_toolkit.language_model.builder import MessagesBuilder
8
8
 
9
+ from unique_web_search.invocation_stats import record_language_model_response
9
10
  from unique_web_search.services.content_processing.processing_strategies.base import (
10
11
  ProcessingStrategyKwargs,
11
12
  WebSearchResult,
@@ -309,6 +310,18 @@ class LLMGuardJudge:
309
310
  structured_output_model=response_model,
310
311
  structured_output_enforce_schema=True,
311
312
  )
313
+ source_by_response_model = {
314
+ LLMGuardResponse: "web_search.content_processing.guard_sanitize",
315
+ JudgeResponse: "web_search.content_processing.guard_judge",
316
+ JudgeAndSanitizeResponse: (
317
+ "web_search.content_processing.guard_judge_and_sanitize"
318
+ ),
319
+ }
320
+ record_language_model_response(
321
+ model_name=self._config.language_model.name,
322
+ response=response,
323
+ source=source_by_response_model[response_model],
324
+ )
312
325
 
313
326
  if response.choices[0].message.parsed is None:
314
327
  raise ValueError("Judge call returned no parsed response")
@@ -8,6 +8,7 @@ from unique_toolkit._common.utils.jinja.render import render_template
8
8
  from unique_toolkit.language_model import LanguageModelService
9
9
  from unique_toolkit.language_model.builder import MessagesBuilder
10
10
 
11
+ from unique_web_search.invocation_stats import record_language_model_response
11
12
  from unique_web_search.services.content_processing.processing_strategies.base import (
12
13
  ProcessingStrategyKwargs,
13
14
  WebSearchResult,
@@ -180,6 +181,11 @@ class LLMKeywordRedact:
180
181
  structured_output_model=KeywordRedactResponse,
181
182
  structured_output_enforce_schema=True,
182
183
  )
184
+ record_language_model_response(
185
+ model_name=self._config.language_model.name,
186
+ response=response,
187
+ source="web_search.content_processing.keyword_redact",
188
+ )
183
189
 
184
190
  if response.choices[0].message.parsed is None:
185
191
  raise ValueError("Keyword extraction call returned no parsed response")
@@ -13,6 +13,7 @@ from unique_toolkit.language_model import (
13
13
  )
14
14
  from unique_toolkit.language_model.builder import MessagesBuilder
15
15
 
16
+ from unique_web_search.invocation_stats import record_language_model_response
16
17
  from unique_web_search.services.content_processing.processing_strategies.base import (
17
18
  ProcessingStrategyKwargs,
18
19
  WebSearchResult,
@@ -316,6 +317,11 @@ class LLMProcess:
316
317
  structured_output_model=LLMProcessorResponse,
317
318
  structured_output_enforce_schema=True,
318
319
  )
320
+ record_language_model_response(
321
+ model_name=self._config.language_model.name,
322
+ response=response,
323
+ source="web_search.content_processing.llm_process",
324
+ )
319
325
 
320
326
  if response.choices[0].message.parsed is None:
321
327
  raise ValueError("Failed to parse response")
@@ -12,6 +12,7 @@ from unique_toolkit.content import ContentChunk
12
12
  from unique_toolkit.language_model import LanguageModelFunction
13
13
  from unique_toolkit.monitoring import metric_scope
14
14
 
15
+ from unique_web_search.invocation_stats import record_invocation_stats
15
16
  from unique_web_search.metrics import llm_duration, llm_errors
16
17
  from unique_web_search.schema import (
17
18
  StepDebugInfo,
@@ -160,6 +161,16 @@ class BaseWebSearchExecutor(ABC, Generic[T]):
160
161
  chunks=content,
161
162
  config=self.chunk_relevancy_sort_config,
162
163
  )
164
+ # The relevancy sorter makes one LLM call per chunk and attaches the
165
+ # usage to each ChunkRelevancy.relevancy.invocation_stats; merge it
166
+ # into the active WebSearch stats scope so it isn't dropped from
167
+ # token analytics (Internal Search does the same after its sort).
168
+ record_invocation_stats(
169
+ invocation
170
+ for relevancy in sorted_chunks.relevancies
171
+ if relevancy.relevancy is not None
172
+ for invocation in relevancy.relevancy.invocation_stats
173
+ )
163
174
  _LOGGER.info(f"Sorting chunks message: {sorted_chunks.user_message}")
164
175
  return sorted_chunks.content_chunks
165
176
  else:
@@ -12,6 +12,7 @@ from unique_toolkit.language_model import LanguageModelFunction
12
12
  from unique_toolkit.language_model.builder import MessagesBuilder
13
13
  from unique_toolkit.monitoring import metric_scope
14
14
 
15
+ from unique_web_search.invocation_stats import record_language_model_response
15
16
  from unique_web_search.metrics import (
16
17
  crawl_duration,
17
18
  crawl_errors,
@@ -124,6 +125,11 @@ async def query_generation_agent(
124
125
  structured_output_model=structured_output_model,
125
126
  structured_output_enforce_schema=True,
126
127
  )
128
+ record_language_model_response(
129
+ model_name=language_model.name,
130
+ response=response,
131
+ source="web_search.query_refinement",
132
+ )
127
133
 
128
134
  parsed_response = response.choices[0].message.parsed
129
135
  if parsed_response is None:
@@ -100,13 +100,14 @@ class BingSearch(SearchEngine[BingSearchConfig]):
100
100
  del params
101
101
  agent_client = get_project_client(self.credentials, self.config.endpoint)
102
102
 
103
- search_results = await create_and_process_run(
104
- agent_client,
105
- agent_id=self.config.agent_id,
106
- query=query,
107
- fetch_size=self.config.fetch_size,
108
- response_parsers_strategies=self.response_parsers,
109
- generation_instructions=self.config.generation_instructions,
110
- )
103
+ async with agent_client:
104
+ search_results = await create_and_process_run(
105
+ agent_client,
106
+ agent_id=self.config.agent_id,
107
+ query=query,
108
+ fetch_size=self.config.fetch_size,
109
+ response_parsers_strategies=self.response_parsers,
110
+ generation_instructions=self.config.generation_instructions,
111
+ )
111
112
 
112
113
  return search_results
@@ -1,14 +1,17 @@
1
1
  import logging
2
2
 
3
3
  import certifi
4
+ import httpx
4
5
  from azure.ai.projects.aio import AIProjectClient
5
6
  from azure.core.credentials_async import AsyncTokenCredential
6
7
  from azure.core.pipeline.transport import AsyncioRequestsTransport
7
8
  from azure.identity.aio import DefaultAzureCredential, WorkloadIdentityCredential
9
+ from openai import AsyncOpenAI
8
10
 
9
11
  from unique_web_search.settings import env_settings
10
12
 
11
13
  _LOGGER = logging.getLogger(__name__)
14
+ _private_endpoint_http_client: httpx.AsyncClient | None = None
12
15
 
13
16
 
14
17
  def _get_workload_identity_credentials(
@@ -16,14 +19,11 @@ def _get_workload_identity_credentials(
16
19
  ) -> WorkloadIdentityCredential:
17
20
  if with_request_transport:
18
21
  transport = AsyncioRequestsTransport(connection_verify=certifi.where())
19
- credentials = WorkloadIdentityCredential(transport=transport)
20
- else:
21
- credentials = WorkloadIdentityCredential()
22
-
23
- return credentials
22
+ return WorkloadIdentityCredential(transport=transport)
23
+ return WorkloadIdentityCredential()
24
24
 
25
25
 
26
- def get_credentials():
26
+ def get_credentials() -> AsyncTokenCredential:
27
27
  match env_settings.azure_identity_credential_type:
28
28
  case "workload":
29
29
  return _get_workload_identity_credentials(
@@ -31,10 +31,8 @@ def get_credentials():
31
31
  )
32
32
  case "default":
33
33
  return DefaultAzureCredential()
34
- case _:
35
- raise ValueError(
36
- f"Invalid Azure identity credential type: {env_settings.azure_identity_credential_type}"
37
- )
34
+ case other:
35
+ raise ValueError(f"Invalid Azure identity credential type: {other}")
38
36
 
39
37
 
40
38
  async def credentials_are_valid(credentials: AsyncTokenCredential) -> bool:
@@ -72,3 +70,36 @@ def get_project_client(
72
70
  credential=credentials,
73
71
  endpoint=endpoint,
74
72
  )
73
+
74
+
75
+ def _get_private_endpoint_http_client() -> httpx.AsyncClient:
76
+ """Return a process-wide certifi-backed httpx client (created once)."""
77
+ global _private_endpoint_http_client
78
+ if _private_endpoint_http_client is None or _private_endpoint_http_client.is_closed:
79
+ _private_endpoint_http_client = httpx.AsyncClient(verify=certifi.where())
80
+ return _private_endpoint_http_client
81
+
82
+
83
+ async def aclose_private_endpoint_http_client() -> None:
84
+ """Close the shared private-endpoint httpx client if it was created."""
85
+ global _private_endpoint_http_client
86
+ if (
87
+ _private_endpoint_http_client is not None
88
+ and not _private_endpoint_http_client.is_closed
89
+ ):
90
+ await _private_endpoint_http_client.aclose()
91
+ _private_endpoint_http_client = None
92
+
93
+
94
+ def get_openai_client(project_client: AIProjectClient) -> AsyncOpenAI:
95
+ """Return an authenticated AsyncOpenAI client from the Foundry project client.
96
+
97
+ When private-endpoint transport is enabled, reuse a shared certifi-backed
98
+ ``httpx.AsyncClient`` so TLS verification matches the AIProjectClient path
99
+ without leaking a new client per request.
100
+ """
101
+ if env_settings.use_unique_private_endpoint_transport:
102
+ return project_client.get_openai_client(
103
+ http_client=_get_private_endpoint_http_client(),
104
+ )
105
+ return project_client.get_openai_client()