unique_web_search 2026.32.0.dev0__tar.gz → 2026.32.0.dev1__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 (126) hide show
  1. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/PKG-INFO +3 -3
  2. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/pyproject.toml +3 -3
  3. unique_web_search-2026.32.0.dev1/src/unique_web_search/invocation_stats.py +113 -0
  4. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/service.py +26 -1
  5. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/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.dev1}/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.dev1}/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.dev1}/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.dev1}/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.dev1}/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.dev1}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +7 -0
  12. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +6 -0
  13. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/vertexai.py +6 -0
  14. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/snippet_judge/service.py +6 -0
  15. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/structured_llm.py +8 -0
  16. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/README.md +0 -0
  17. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/__init__.py +0 -0
  18. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/client_settings.py +0 -0
  19. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/config.py +0 -0
  20. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/metrics.py +0 -0
  21. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/prompts.py +0 -0
  22. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/schema.py +0 -0
  23. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/_registry.py +0 -0
  24. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  25. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/config.py +0 -0
  26. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  27. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  28. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  29. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  30. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  31. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  32. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  33. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/client/proxy_config.py +0 -0
  34. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  35. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  36. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  37. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  38. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  39. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/config.py +0 -0
  40. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  41. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  42. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  43. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  44. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  45. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  46. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  47. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  48. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  49. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  50. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  51. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  52. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  53. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/content_processing/service.py +0 -0
  54. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/README.md +0 -0
  55. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  56. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/base.py +0 -0
  57. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/basic.py +0 -0
  58. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  59. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/jina.py +0 -0
  60. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/registry.py +0 -0
  61. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  62. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  63. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/crawlers/utils.py +0 -0
  64. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/README.md +0 -0
  65. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/__init__.py +0 -0
  66. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/base_config.py +0 -0
  67. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/context.py +0 -0
  68. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/modes.py +0 -0
  69. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  70. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/config.py +0 -0
  71. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  72. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  73. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  74. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  75. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  76. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  77. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  78. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/config.py +0 -0
  79. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/executor.py +0 -0
  80. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  81. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  82. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  83. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  84. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  85. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  86. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  87. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/config.py +0 -0
  88. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  89. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  90. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  91. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  92. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  93. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  94. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/helpers.py +0 -0
  95. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/message_log.py +0 -0
  96. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/proxy/__init__.py +0 -0
  97. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/proxy/bridge.py +0 -0
  98. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/proxy/mappers.py +0 -0
  99. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/query_elicitation.py +0 -0
  100. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/README.md +0 -0
  101. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  102. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/base.py +0 -0
  103. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/bing.py +0 -0
  104. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/brave.py +0 -0
  105. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/custom_api.py +0 -0
  106. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/google.py +0 -0
  107. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  108. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/registry.py +0 -0
  109. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/schema.py +0 -0
  110. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  111. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  112. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  113. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  114. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  115. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  116. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  117. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  118. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  119. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  120. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  121. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  122. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  123. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  124. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/settings.py +0 -0
  125. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/src/unique_web_search/skills/web-search-v3.md +0 -0
  126. {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev1}/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.dev1
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>
@@ -16,7 +16,7 @@ Requires-Dist: tavily-python>=0.7.11,<1
16
16
  Requires-Dist: unidecode>=1.4.0,<2
17
17
  Requires-Dist: azure-ai-projects>=1.0.0,<2
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,7 +25,7 @@ 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.dev2,<2026.32.0rc0
29
29
  Requires-Dist: unique-search-proxy-sdk>=2026.32.0.dev0,<2026.32.0rc0
30
30
  Requires-Python: >=3.12, <4.0
31
31
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_web_search"
3
- version = "2026.32.0.dev0"
3
+ version = "2026.32.0.dev1"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "unidecode>=1.4.0,<2",
23
23
  "azure-ai-projects>=1.0.0,<2",
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,7 +31,7 @@ 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.dev2,<2026.32.0rc0",
35
35
  "unique-search-proxy-sdk>=2026.32.0.dev0,<2026.32.0rc0",
36
36
  ]
37
37
 
@@ -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:
@@ -10,6 +10,7 @@ from azure.ai.agents.models import (
10
10
  )
11
11
  from azure.ai.projects.aio import AIProjectClient
12
12
 
13
+ from unique_web_search.invocation_stats import record_token_usage
13
14
  from unique_web_search.services.search_engine.schema import (
14
15
  WebSearchResult,
15
16
  )
@@ -129,6 +130,12 @@ async def create_and_process_run(
129
130
  _LOGGER.warning(f"Using existing agent ID: {agent_id}")
130
131
  thread = await _create_agent_run_with_agent_id(agent_client, agent_id, query)
131
132
 
133
+ record_token_usage(
134
+ model_name=env_settings.azure_ai_bing_agent_model,
135
+ usage=getattr(thread, "usage", None),
136
+ source="web_search.grounding.bing",
137
+ )
138
+
132
139
  if thread.status in [RunStatus.FAILED, RunStatus.CANCELLED, RunStatus.EXPIRED]:
133
140
  raise Exception(f"Run failed: {thread.last_error}")
134
141
 
@@ -6,6 +6,7 @@ from unique_toolkit._common.validators import LMI
6
6
  from unique_toolkit.language_model.builder import MessagesBuilder
7
7
  from unique_toolkit.language_model.service import LanguageModelService
8
8
 
9
+ from unique_web_search.invocation_stats import record_language_model_response
9
10
  from unique_web_search.services.search_engine.schema import WebSearchResult
10
11
  from unique_web_search.services.search_engine.utils.grounding.models import (
11
12
  GroundingSearchResults,
@@ -92,6 +93,11 @@ class LLMParserStrategy(ResponseParser):
92
93
  structured_output_model=GroundingSearchResults,
93
94
  structured_output_enforce_schema=True,
94
95
  )
96
+ record_language_model_response(
97
+ model_name=self.llm.name,
98
+ response=llm_response,
99
+ source="web_search.grounding.response_parser",
100
+ )
95
101
 
96
102
  if not llm_response.choices[0].message.parsed:
97
103
  raise ValueError("No JSON found in the response")
@@ -13,6 +13,7 @@ from unique_toolkit._common.default_language_model import DEFAULT_LANGUAGE_MODEL
13
13
  from unique_toolkit._common.validators import LMI, get_LMI_default_field
14
14
  from unique_toolkit.language_model import LanguageModelService
15
15
 
16
+ from unique_web_search.invocation_stats import record_vertex_response
16
17
  from unique_web_search.services.proxy.bridge import (
17
18
  search_proxy_client_enabled,
18
19
  )
@@ -114,6 +115,11 @@ class VertexAI(SearchEngine[VertexAIConfig]):
114
115
  ),
115
116
  contents=query,
116
117
  )
118
+ record_vertex_response(
119
+ model_name=self.config.vertexai_model_name,
120
+ response=response,
121
+ source="web_search.grounding.vertexai",
122
+ )
117
123
 
118
124
  answer_with_citations = add_citations(response)
119
125
 
@@ -9,6 +9,7 @@ from unique_toolkit._common.validators import LMI
9
9
  from unique_toolkit.language_model import LanguageModelService
10
10
  from unique_toolkit.language_model.builder import MessagesBuilder
11
11
 
12
+ from unique_web_search.invocation_stats import record_language_model_response
12
13
  from unique_web_search.services.helpers import extract_registered_domain
13
14
  from unique_web_search.services.search_engine.schema import (
14
15
  WebSearchResult,
@@ -99,6 +100,11 @@ async def score_and_explain(
99
100
  structured_output_model=SnippetJudgeResponse,
100
101
  structured_output_enforce_schema=True,
101
102
  )
103
+ record_language_model_response(
104
+ model_name=language_model.name,
105
+ response=response,
106
+ source="web_search.snippet_judge",
107
+ )
102
108
 
103
109
  parsed = response.choices[0].message.parsed
104
110
  if parsed is None:
@@ -7,6 +7,8 @@ from unique_toolkit._common.validators import LMI
7
7
  from unique_toolkit.language_model import LanguageModelService
8
8
  from unique_toolkit.language_model.builder import MessagesBuilder
9
9
 
10
+ from unique_web_search.invocation_stats import record_language_model_response
11
+
10
12
  T = TypeVar("T", bound=BaseModel)
11
13
 
12
14
 
@@ -21,6 +23,7 @@ async def complete_structured_llm(
21
23
  system_message: str,
22
24
  user_message: str,
23
25
  response_model: type[T],
26
+ invocation_source: str,
24
27
  structured_output_enforce_schema: bool = True,
25
28
  ) -> T:
26
29
  """Run a structured completion and return a validated model instance."""
@@ -37,6 +40,11 @@ async def complete_structured_llm(
37
40
  structured_output_model=response_model,
38
41
  structured_output_enforce_schema=structured_output_enforce_schema,
39
42
  )
43
+ record_language_model_response(
44
+ model_name=language_model.name,
45
+ response=response,
46
+ source=invocation_source,
47
+ )
40
48
 
41
49
  parsed = response.choices[0].message.parsed
42
50
  if parsed is None: