unique_web_search 2026.44.0.dev3__tar.gz → 2026.44.0.dev5__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 (130) hide show
  1. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/PKG-INFO +5 -5
  2. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/README.md +1 -1
  3. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/pyproject.toml +4 -4
  4. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/metrics.py +15 -0
  5. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +6 -18
  6. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +2 -2
  7. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +2 -2
  8. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +1 -1
  9. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/service.py +1 -1
  10. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/base.py +11 -9
  11. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/base_executor.py +1 -1
  12. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/schema.py +1 -0
  13. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/executor.py +1 -1
  14. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/schema.py +1 -1
  15. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/schema.py +1 -0
  16. unique_web_search-2026.44.0.dev5/src/unique_web_search/services/httpx_logging.py +56 -0
  17. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/proxy/mappers.py +10 -0
  18. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/query_elicitation.py +3 -3
  19. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/README.md +8 -2
  20. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/base.py +30 -11
  21. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/bing.py +33 -24
  22. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/custom_api.py +1 -1
  23. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +4 -0
  24. unique_web_search-2026.44.0.dev5/src/unique_web_search/services/search_engine/utils/grounding/bing/citation_instructions.j2 +18 -0
  25. unique_web_search-2026.44.0.dev5/src/unique_web_search/services/search_engine/utils/grounding/bing/citation_parsing.py +302 -0
  26. unique_web_search-2026.44.0.dev5/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +39 -0
  27. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +104 -60
  28. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +28 -5
  29. unique_web_search-2026.44.0.dev3/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -16
  30. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/__init__.py +0 -0
  31. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/client_settings.py +0 -0
  32. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/config.py +0 -0
  33. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/invocation_stats.py +0 -0
  34. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/prompts.py +0 -0
  35. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/schema.py +0 -0
  36. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/service.py +0 -0
  37. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/_registry.py +0 -0
  38. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  39. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/config.py +0 -0
  40. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  41. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  42. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  43. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  44. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  45. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  46. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  47. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/argument_screening/service.py +0 -0
  48. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/client/http_client.py +0 -0
  49. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  50. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  51. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  52. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  53. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  54. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/config.py +0 -0
  55. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  56. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  57. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  58. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  59. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  60. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  61. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  62. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  63. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  64. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  65. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  66. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  67. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/README.md +0 -0
  68. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  69. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/basic.py +0 -0
  70. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  71. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/jina.py +0 -0
  72. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/registry.py +0 -0
  73. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  74. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  75. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/crawlers/utils.py +0 -0
  76. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/README.md +0 -0
  77. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/__init__.py +0 -0
  78. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/base_config.py +0 -0
  79. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/context.py +0 -0
  80. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/modes.py +0 -0
  81. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  82. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/config.py +0 -0
  83. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/executor.py +0 -0
  84. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  85. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  86. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  87. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  88. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  89. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  90. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/config.py +0 -0
  91. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  92. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  93. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  94. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  95. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  96. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  97. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/config.py +0 -0
  98. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  99. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  100. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  101. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  102. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  103. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/helpers.py +0 -0
  104. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/message_log.py +0 -0
  105. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/proxy/__init__.py +0 -0
  106. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/proxy/bridge.py +0 -0
  107. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  108. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/brave.py +0 -0
  109. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/google.py +0 -0
  110. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  111. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/registry.py +0 -0
  112. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/schema.py +0 -0
  113. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  114. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  115. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +0 -0
  116. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  117. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  118. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  119. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  120. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  121. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  122. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
  123. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  124. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  125. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  126. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  127. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/services/structured_llm.py +0 -0
  128. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/settings.py +0 -0
  129. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/src/unique_web_search/skills/web-search-v3.md +0 -0
  130. {unique_web_search-2026.44.0.dev3 → unique_web_search-2026.44.0.dev5}/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.44.0.dev3
3
+ Version: 2026.44.0.dev5
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>=2.3,<3
18
18
  Requires-Dist: azure-identity>=1.25.0,<2
19
- Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev9,<2026.44.0rc0
19
+ Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.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,8 @@ 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.44.0.dev5,<2026.44.0rc0
29
- Requires-Dist: unique-search-proxy-sdk>=2026.44.0.dev3,<2026.44.0rc0
28
+ Requires-Dist: unique-search-proxy-core>=2026.44.0.dev10,<2026.44.0rc0
29
+ Requires-Dist: unique-search-proxy-sdk>=2026.44.0.dev5,<2026.44.0rc0
30
30
  Requires-Dist: openai>=1.105.0,<3
31
31
  Requires-Python: >=3.12, <4.0
32
32
  Description-Content-Type: text/markdown
@@ -73,7 +73,7 @@ Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `v
73
73
 
74
74
  ### Agent search (grounding)
75
75
 
76
- An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s.
76
+ An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s. Bing results take their URLs only from the citations Foundry attaches to the answer, never from the answer text.
77
77
 
78
78
  | Engine | Proxy | Notes |
79
79
  |--------|-------|-------|
@@ -40,7 +40,7 @@ Standard engines that live in proxy-core use **`ExposableParam`** (`expose` + `v
40
40
 
41
41
  ### Agent search (grounding)
42
42
 
43
- An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s.
43
+ An upstream AI agent / grounded model searches and returns opaque text that this tool parses into `WebSearchResult`s. Bing results take their URLs only from the citations Foundry attaches to the answer, never from the answer text.
44
44
 
45
45
  | Engine | Proxy | Notes |
46
46
  |--------|-------|-------|
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_web_search"
3
- version = "2026.44.0.dev3"
3
+ version = "2026.44.0.dev5"
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>=2.3,<3",
24
24
  "azure-identity>=1.25.0,<2",
25
- "unique-toolkit[monitoring]>=2026.44.0.dev9,<2026.44.0rc0",
25
+ "unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.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,8 @@ 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.44.0.dev5,<2026.44.0rc0",
35
- "unique-search-proxy-sdk>=2026.44.0.dev3,<2026.44.0rc0",
34
+ "unique-search-proxy-core>=2026.44.0.dev10,<2026.44.0rc0",
35
+ "unique-search-proxy-sdk>=2026.44.0.dev5,<2026.44.0rc0",
36
36
  "openai>=1.105.0,<3",
37
37
  ]
38
38
 
@@ -15,6 +15,21 @@ custom_api_url_safety_report = m.counter(
15
15
  "Report-only URL safety outcomes for legacy Custom API searches",
16
16
  ["outcome", "reason_category"],
17
17
  )
18
+ grounding_parse_failures = m.counter(
19
+ "grounding_parse_failures_total",
20
+ "Grounded agent answers that yielded no usable sources",
21
+ ["engine", "reason"],
22
+ )
23
+ grounding_dropped_citations = m.counter(
24
+ "grounding_dropped_citations_total",
25
+ "Citations dropped from grounded agent answers because they failed validation",
26
+ ["engine", "reason"],
27
+ )
28
+ grounding_dropped_lines = m.counter(
29
+ "grounding_dropped_lines_total",
30
+ "Answer lines dropped from grounded agent answers",
31
+ ["engine", "reason"],
32
+ )
18
33
 
19
34
  # End-to-End Tool (P1)
20
35
  tool_duration = m.histogram(
@@ -157,13 +157,9 @@ class LLMGuardJudge:
157
157
  needs_sanitization, _ = await self.judge_only(**kwargs)
158
158
  page = kwargs["page"]
159
159
  if not needs_sanitization:
160
- _LOGGER.info(
161
- f"Page {page.url} does not require sanitization — keeping as-is"
162
- )
160
+ _LOGGER.info("Page does not require sanitization — keeping as-is")
163
161
  return page
164
- _LOGGER.info(
165
- f"Page {page.url} flagged — replacing content with flag message"
166
- )
162
+ _LOGGER.info("Page flagged — replacing content with flag message")
167
163
  return page.model_copy(
168
164
  update={
169
165
  "content": "",
@@ -178,13 +174,9 @@ class LLMGuardJudge:
178
174
  if mode == SanitizeMode.JUDGE_THEN_SANITIZE:
179
175
  needs_sanitization, _ = await self.judge_only(**kwargs)
180
176
  if not needs_sanitization:
181
- _LOGGER.info(
182
- f"Page {kwargs['page'].url} does not require sanitization — keeping as-is"
183
- )
177
+ _LOGGER.info("Page does not require sanitization — keeping as-is")
184
178
  return kwargs["page"]
185
- _LOGGER.info(
186
- f"Page {kwargs['page'].url} flagged — running full sanitize call"
187
- )
179
+ _LOGGER.info("Page flagged — running full sanitize call")
188
180
  return await self.sanitize_page(**kwargs)
189
181
 
190
182
  raise ValueError(f"Unknown sanitize_mode: {mode}")
@@ -238,9 +230,7 @@ class LLMGuardJudge:
238
230
 
239
231
  parsed = await self._complete(JudgeResponse, system_prompt, user_prompt)
240
232
 
241
- _LOGGER.info(
242
- f"Judge result for {page.url}: needs_sanitization={parsed.needs_sanitization}"
243
- )
233
+ _LOGGER.info(f"Judge result: needs_sanitization={parsed.needs_sanitization}")
244
234
 
245
235
  return parsed.needs_sanitization, page
246
236
 
@@ -272,9 +262,7 @@ class LLMGuardJudge:
272
262
  JudgeAndSanitizeResponse, system_prompt, user_prompt
273
263
  )
274
264
 
275
- _LOGGER.info(
276
- f"Judge result for {page.url}: needs_sanitization={parsed.needs_sanitization}"
277
- )
265
+ _LOGGER.info(f"Judge result: needs_sanitization={parsed.needs_sanitization}")
278
266
 
279
267
  if parsed.needs_sanitization:
280
268
  page = parsed.apply_to_page(page, self._config.privacy_filter.flag_message)
@@ -171,7 +171,7 @@ class LLMKeywordRedact:
171
171
  )
172
172
 
173
173
  _LOGGER.info(
174
- f"Running keyword extraction for page {page.url} "
174
+ "Running keyword extraction for page "
175
175
  f"(model={self._config.language_model.name})"
176
176
  )
177
177
 
@@ -195,7 +195,7 @@ class LLMKeywordRedact:
195
195
  )
196
196
 
197
197
  _LOGGER.info(
198
- f"Extracted {len(parsed.sensitive_keywords)} sensitive keyword(s) from {page.url}"
198
+ f"Extracted {len(parsed.sensitive_keywords)} sensitive keyword(s) from page"
199
199
  )
200
200
 
201
201
  return parsed.apply_to_page(page)
@@ -246,12 +246,12 @@ class LLMProcess:
246
246
 
247
247
  if not self._config.should_run(self._encoder, page.content):
248
248
  _LOGGER.warning(
249
- f"Content is already short enough, skipping LLM processing for page {page.url}"
249
+ "Content is already short enough, skipping LLM processing for page"
250
250
  )
251
251
  return page
252
252
 
253
253
  _LOGGER.info(
254
- f"Processing page {page.url} with LLM processor "
254
+ "Processing page with LLM processor "
255
255
  f"(sanitize={self._config.privacy_filter.sanitize}, mode={self._config.privacy_filter.sanitize_mode})"
256
256
  )
257
257
 
@@ -48,7 +48,7 @@ class Truncate:
48
48
  _LOGGER.warning("Truncate strategy is disabled, skipping")
49
49
  return page
50
50
 
51
- _LOGGER.info(f"Truncating page {page.url} with truncate strategy")
51
+ _LOGGER.info("Truncating page with truncate strategy")
52
52
  tokens = self._encoder(page.content)
53
53
  if len(tokens) > self._config.max_tokens:
54
54
  page.content = self._decoder(tokens[: self._config.max_tokens])
@@ -161,7 +161,7 @@ class ContentProcessor:
161
161
  # Clear content to avoid overfilling the context; the chunker will
162
162
  # render the compliance placeholder via the empty-content branch.
163
163
  _LOGGER.error(
164
- f"Processing strategy failed for page {page.url}: {result.exception}",
164
+ f"Processing strategy failed for page: {result.exception}",
165
165
  exc_info=result.exception,
166
166
  )
167
167
  processed_pages.append(page.model_copy(update={"content": ""}))
@@ -12,6 +12,7 @@ from unique_search_proxy_core.url_safety import (
12
12
  )
13
13
 
14
14
  from unique_web_search.metrics import crawl_blocked
15
+ from unique_web_search.services.httpx_logging import hide_httpx_request_logs
15
16
  from unique_web_search.services.proxy.bridge import (
16
17
  open_search_proxy_client,
17
18
  search_proxy_client_enabled,
@@ -41,16 +42,17 @@ class BaseCrawler(ABC, Generic[CrawlerConfig]):
41
42
  self._request_context = request_context
42
43
 
43
44
  async def crawl(self, urls: list[str]) -> list[str]:
44
- if search_proxy_client_enabled:
45
- return await self._proxy_crawl(urls)
45
+ with hide_httpx_request_logs():
46
+ if search_proxy_client_enabled:
47
+ return await self._proxy_crawl(urls)
46
48
 
47
- try:
48
- targets = await UrlSafetyService.validate_batch_urls(urls)
49
- except CrawlTargetValidationError as exc:
50
- for target in exc.blocked_targets:
51
- crawl_blocked.labels(reason_category=target.category).inc()
52
- raise
53
- return await self._legacy_crawl(targets)
49
+ try:
50
+ targets = await UrlSafetyService.validate_batch_urls(urls)
51
+ except CrawlTargetValidationError as exc:
52
+ for target in exc.blocked_targets:
53
+ crawl_blocked.labels(reason_category=target.category).inc()
54
+ raise
55
+ return await self._legacy_crawl(targets)
54
56
 
55
57
  async def _proxy_crawl(self, urls: list[str]) -> list[str]:
56
58
  """Dump deployment config fields into the generic proxy crawl call."""
@@ -71,7 +71,7 @@ class BaseWebSearchExecutor(ABC, Generic[T]):
71
71
  self._notify_message = ""
72
72
 
73
73
  async def notify_callback() -> None:
74
- _LOGGER.debug(f"{self.notify_name}: {self.notify_message}")
74
+ _LOGGER.debug(f"Progress update: {self.notify_name}")
75
75
  if self.tool_progress_reporter:
76
76
  await self.tool_progress_reporter.notify_from_tool_call(
77
77
  tool_call=tool_call,
@@ -16,6 +16,7 @@ class WebSearchToolParameters(ExposedParams):
16
16
  alias_generator=to_camel,
17
17
  populate_by_name=True,
18
18
  extra="forbid",
19
+ hide_input_in_errors=True,
19
20
  )
20
21
 
21
22
  query: str = Field(description=_DEFAULT_QUERY_DESCRIPTION)
@@ -212,7 +212,7 @@ class WebSearchV2Executor(BaseWebSearchExecutor[WebSearchPlan]):
212
212
  )
213
213
  delta_time = time() - time_start
214
214
  _LOGGER.debug(
215
- f"Crawled {step.query_or_url} with {self.crawler_service} completed in {delta_time} seconds"
215
+ f"Crawled URL with {self.crawler_service} completed in {delta_time} seconds"
216
216
  )
217
217
 
218
218
  self.debug_info.steps.append(
@@ -26,7 +26,7 @@ class Step(BaseModel):
26
26
 
27
27
 
28
28
  class WebSearchPlan(BaseModel):
29
- model_config = ConfigDict(extra="forbid")
29
+ model_config = ConfigDict(extra="forbid", hide_input_in_errors=True)
30
30
  objective: str = Field(description="The objective of the plan")
31
31
  query_analysis: str = Field(
32
32
  description="Analysis of the user's query and what information is needed"
@@ -15,6 +15,7 @@ _LLM_TOOL_MODEL_CONFIG = ConfigDict(
15
15
  alias_generator=to_camel,
16
16
  populate_by_name=True,
17
17
  extra="forbid",
18
+ hide_input_in_errors=True,
18
19
  )
19
20
 
20
21
 
@@ -0,0 +1,56 @@
1
+ """Hide httpx request logs while a web search call runs.
2
+
3
+ httpx logs every request URL at INFO, and web search request URLs carry
4
+ search queries, crawled URLs and provider credentials. httpx logs from other
5
+ code in the same process are kept.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from collections.abc import Iterator
12
+ from contextlib import contextmanager
13
+ from contextvars import ContextVar
14
+
15
+ from unique_search_proxy_core.logging import HttpxRequestLogFilter
16
+
17
+ _hide_httpx_request_logs: ContextVar[bool] = ContextVar(
18
+ "unique_web_search_hide_httpx_request_logs", default=False
19
+ )
20
+
21
+
22
+ # Wraps instead of subclassing HttpxRequestLogFilter: the SDK's global
23
+ # suppress_httpx_request_logs() skips installing itself when it finds an
24
+ # instance of that class on the httpx logger.
25
+ class _WebSearchHttpxRequestLogFilter(logging.Filter):
26
+ _request_log_filter = HttpxRequestLogFilter()
27
+
28
+ def filter(self, record: logging.LogRecord) -> bool:
29
+ if not _hide_httpx_request_logs.get():
30
+ return True
31
+ return self._request_log_filter.filter(record)
32
+
33
+
34
+ def _install_filter() -> None:
35
+ httpx_logger = logging.getLogger("httpx")
36
+ if any(
37
+ isinstance(log_filter, _WebSearchHttpxRequestLogFilter)
38
+ for log_filter in httpx_logger.filters
39
+ ):
40
+ return
41
+ httpx_logger.addFilter(_WebSearchHttpxRequestLogFilter())
42
+
43
+
44
+ @contextmanager
45
+ def hide_httpx_request_logs() -> Iterator[None]:
46
+ """Drop httpx request logs from the current task and tasks it starts.
47
+
48
+ Scoping follows ``contextvars``: work handed to threads without a copied
49
+ context (e.g. ``loop.run_in_executor``) is not covered.
50
+ """
51
+ _install_filter()
52
+ token = _hide_httpx_request_logs.set(True)
53
+ try:
54
+ yield
55
+ finally:
56
+ _hide_httpx_request_logs.reset(token)
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Sequence
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from unique_search_proxy_core.schema import AnswerSpanCitation
6
7
  from unique_search_proxy_sdk._generated.models.agent_search_response import (
7
8
  AgentSearchResponse,
8
9
  )
@@ -71,3 +72,12 @@ def agent_answer_text(response: AgentSearchResponse) -> str:
71
72
  if isinstance(response.answer, str):
72
73
  return response.answer
73
74
  return ""
75
+
76
+
77
+ def agent_citations(response: AgentSearchResponse) -> list[AnswerSpanCitation]:
78
+ if not isinstance(response.citations, list):
79
+ return []
80
+ return [
81
+ AnswerSpanCitation.model_validate(citation.to_dict())
82
+ for citation in response.citations
83
+ ]
@@ -50,7 +50,7 @@ class QueryElicitationModel(BaseModel):
50
50
  creation with default values for form pre-population.
51
51
  """
52
52
 
53
- model_config = ConfigDict(title="Query Elicitation")
53
+ model_config = ConfigDict(title="Query Elicitation", hide_input_in_errors=True)
54
54
  queries: list[str] = Field(description="The queries to search the web for")
55
55
 
56
56
  @classmethod
@@ -160,13 +160,13 @@ class QueryElicitationService:
160
160
  elif elicitation.status == ElicitationStatus.DECLINED:
161
161
  _LOGGER.info(f"Query elicitation {elicitation.id} declined")
162
162
  raise ElicitationDeclinedException(
163
- f"Elicitation triggered with queries {queries} was declined"
163
+ f"Query elicitation {elicitation.id} was declined"
164
164
  )
165
165
 
166
166
  elif elicitation.status == ElicitationStatus.CANCELLED:
167
167
  _LOGGER.info(f"Query elicitation {elicitation.id} cancelled")
168
168
  raise ElicitationCancelledException(
169
- f"Elicitation triggered with queries {queries} was cancelled"
169
+ f"Query elicitation {elicitation.id} was cancelled"
170
170
  )
171
171
 
172
172
  raise ElicitationExpiredException(
@@ -31,7 +31,7 @@ Deployment configs for Google / Brave / Perplexity (and agent base fields) live
31
31
  | | Standard | Agent (grounding) |
32
32
  |--|----------|-------------------|
33
33
  | Proxy route | `POST /v1/search` | `POST /v1/agent-search` |
34
- | Result shape | `SearchResponse` → `WebSearchResult` list | Opaque `answer` text → parsed locally |
34
+ | Result shape | `SearchResponse` → `WebSearchResult` list | Opaque `answer` text (+ provider `citations`) → parsed locally |
35
35
  | LLM knobs | `ExposableParam` + `config.merge()` | `ExposableParam` (Bing only) |
36
36
  | Typical scrape | Often yes (Google) | Often no (`requires_scraping=false`) |
37
37
 
@@ -158,7 +158,13 @@ CUSTOM_WEB_SEARCH_API_HEADERS='{"Authorization": "Bearer ..."}'
158
158
 
159
159
  ### Bing (Grounding with Bing)
160
160
 
161
- **Provider:** Azure AI Foundry Agent with Bing grounding · Config extends `BingAgentConfig` with `requires_scraping`, `language_model`, etc.
161
+ **Provider:** Azure AI Foundry Agent with Bing grounding · Config extends `BingAgentConfig` with `requires_scraping`.
162
+
163
+ The agent answers with one bullet per source, each ending in a citation marker.
164
+ `BingCitationParser` (`utils/grounding/bing/citation_parsing.py`) turns each
165
+ cited source into one result, taking its URL and title only from Foundry's
166
+ `url_citation` annotations. Unusable citations and lines are dropped one by one
167
+ and counted in the `grounding_*` metrics rather than failing the whole answer.
162
168
 
163
169
  | Field | Notes |
164
170
  |-------|-------|
@@ -17,12 +17,14 @@ from unique_search_proxy_core.param_policy.exposable_param import (
17
17
  from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
18
18
  from unique_search_proxy_core.search_engines.base import SearchEngineType
19
19
 
20
+ from unique_web_search.services.httpx_logging import hide_httpx_request_logs
20
21
  from unique_web_search.services.proxy.bridge import (
21
22
  open_search_proxy_client,
22
23
  search_proxy_client_enabled,
23
24
  )
24
25
  from unique_web_search.services.proxy.mappers import (
25
26
  agent_answer_text,
27
+ agent_citations,
26
28
  map_agent_answer,
27
29
  map_search_response,
28
30
  )
@@ -31,6 +33,10 @@ from unique_web_search.services.search_engine.schema import (
31
33
  )
32
34
 
33
35
  if TYPE_CHECKING:
36
+ from collections.abc import Sequence
37
+
38
+ from unique_search_proxy_core.schema import AnswerSpanCitation
39
+
34
40
  from unique_web_search.services.search_engine.utils.grounding.response_parsing import (
35
41
  ResponseParser,
36
42
  )
@@ -71,7 +77,7 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
71
77
  it, otherwise it reroutes to the local legacy implementation.
72
78
  """
73
79
 
74
- # Set by agent engines (Bing/VertexAI) and consumed by the agent proxy path.
80
+ # Set by agent engines that use the default ``_parse_agent_answer``.
75
81
  response_parsers: list[ResponseParser]
76
82
 
77
83
  def __init__(
@@ -110,14 +116,15 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
110
116
  params: ExposedParams | None = None,
111
117
  ) -> list[WebSearchResult]:
112
118
  """Search the web for the given query using the search engine."""
113
- proxy_engine = self._proxy_engine
114
- if (
115
- search_proxy_client_enabled
116
- and proxy_engine is not None
117
- and await self._proxy_routing_enabled()
118
- ):
119
- return await self._proxy_search(query, params, proxy_engine)
120
- return await self._legacy_search(query=query, params=params)
119
+ with hide_httpx_request_logs():
120
+ proxy_engine = self._proxy_engine
121
+ if (
122
+ search_proxy_client_enabled
123
+ and proxy_engine is not None
124
+ and await self._proxy_routing_enabled()
125
+ ):
126
+ return await self._proxy_search(query, params, proxy_engine)
127
+ return await self._legacy_search(query=query, params=params)
121
128
 
122
129
  async def _proxy_search(
123
130
  self,
@@ -178,12 +185,24 @@ class SearchEngine(ABC, Generic[SearchEngineConfig]):
178
185
  engine=engine.value,
179
186
  **invocation,
180
187
  )
181
- results = await map_agent_answer(
188
+ results = await self._parse_agent_answer(
182
189
  agent_answer_text(response),
183
- self.response_parsers,
190
+ agent_citations(response),
184
191
  )
185
192
  return await self._postprocess_results(results)
186
193
 
194
+ async def _parse_agent_answer(
195
+ self,
196
+ answer: str,
197
+ citations: Sequence[AnswerSpanCitation],
198
+ ) -> list[WebSearchResult]:
199
+ """Turn an agent answer into results.
200
+
201
+ Parses only the answer text. Engines whose provider cites its sources
202
+ alongside the answer override this, as Bing does.
203
+ """
204
+ return await map_agent_answer(answer, self.response_parsers)
205
+
187
206
  def _agent_proxy_invocation(
188
207
  self,
189
208
  engine: AgentEngineType,
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from collections.abc import Sequence
2
3
  from typing import Any, override
3
4
 
4
5
  from pydantic import Field
@@ -10,10 +11,13 @@ from unique_search_proxy_core.agent_engines.bing.schema import BingAgentConfig
10
11
  from unique_search_proxy_core.agent_engines.bing.settings import resolve_market
11
12
  from unique_search_proxy_core.context import LOCAL_REQUEST_CONTEXT, RequestContext
12
13
  from unique_search_proxy_core.param_policy.exposed_params import ExposedParams
13
- from unique_toolkit._common.default_language_model import DEFAULT_LANGUAGE_MODEL
14
- from unique_toolkit._common.validators import LMI, get_LMI_default_field
15
- from unique_toolkit.language_model import LanguageModelService
14
+ from unique_search_proxy_core.schema import AnswerSpanCitation
16
15
 
16
+ from unique_web_search.metrics import (
17
+ grounding_dropped_citations,
18
+ grounding_dropped_lines,
19
+ grounding_parse_failures,
20
+ )
17
21
  from unique_web_search.services.proxy.bridge import (
18
22
  search_proxy_client_enabled,
19
23
  )
@@ -22,11 +26,8 @@ from unique_web_search.services.search_engine.registry import register_search_en
22
26
  from unique_web_search.services.search_engine.schema import (
23
27
  WebSearchResult,
24
28
  )
25
- from unique_web_search.services.search_engine.utils.grounding import (
26
- JsonConversionStrategy,
27
- LLMParserStrategy,
28
- )
29
29
  from unique_web_search.services.search_engine.utils.grounding.bing import (
30
+ BingCitationParser,
30
31
  create_and_process_run,
31
32
  credentials_are_valid,
32
33
  get_credentials,
@@ -35,10 +36,6 @@ from unique_web_search.services.search_engine.utils.grounding.bing import (
35
36
 
36
37
 
37
38
  class BingSearchConfig(BingAgentConfig):
38
- language_model: LMI = get_LMI_default_field(
39
- DEFAULT_LANGUAGE_MODEL,
40
- description="The language model to use in as a fallback parser if the agent response is not a valid JSON.",
41
- )
42
39
  requires_scraping: bool = Field(
43
40
  default=False,
44
41
  description="Whether the search engine requires scraping.",
@@ -51,26 +48,17 @@ class BingSearchConfig(BingAgentConfig):
51
48
  config_cls=BingSearchConfig,
52
49
  mode=SearchEngineMode.AGENT,
53
50
  config_display_name="Grounding with Bing",
54
- needs_language_model=True,
55
51
  )
56
52
  class BingSearch(SearchEngine[BingSearchConfig]):
57
53
  def __init__(
58
54
  self,
59
55
  config: BingSearchConfig,
60
- language_model_service: LanguageModelService,
61
56
  *,
62
57
  request_context: RequestContext = LOCAL_REQUEST_CONTEXT,
63
58
  ):
64
59
  super().__init__(config, request_context=request_context)
65
60
  self.credentials = get_credentials()
66
-
67
- self.response_parsers = [
68
- JsonConversionStrategy(),
69
- LLMParserStrategy(
70
- config.language_model,
71
- language_model_service,
72
- ),
73
- ]
61
+ self.citation_response_parser = BingCitationParser()
74
62
 
75
63
  @property
76
64
  def is_configured(self) -> bool:
@@ -116,12 +104,33 @@ class BingSearch(SearchEngine[BingSearchConfig]):
116
104
  agent_client = get_project_client(self.credentials)
117
105
 
118
106
  async with agent_client:
119
- search_results = await create_and_process_run(
107
+ answer, citations = await create_and_process_run(
120
108
  agent_client,
121
109
  query=query,
122
110
  grounding=self._grounding_configuration(),
123
- response_parsers_strategies=self.response_parsers,
124
111
  generation_instructions=self.config.generation_instructions,
125
112
  )
126
113
 
127
- return search_results
114
+ return await self._parse_agent_answer(answer, citations)
115
+
116
+ @override
117
+ async def _parse_agent_answer(
118
+ self,
119
+ answer: str,
120
+ citations: Sequence[AnswerSpanCitation],
121
+ ) -> list[WebSearchResult]:
122
+ parsed = await self.citation_response_parser(answer, citations)
123
+ engine = self.config.engine.value
124
+ for reason, count in parsed.dropped_citations.items():
125
+ grounding_dropped_citations.labels(engine=engine, reason=reason.value).inc(
126
+ count
127
+ )
128
+ for reason, count in parsed.dropped_lines.items():
129
+ grounding_dropped_lines.labels(engine=engine, reason=reason.value).inc(
130
+ count
131
+ )
132
+ if parsed.failure is not None:
133
+ grounding_parse_failures.labels(
134
+ engine=engine, reason=parsed.failure.value
135
+ ).inc()
136
+ return parsed.results
@@ -191,7 +191,7 @@ class CustomAPI(SearchEngine[CustomAPIConfig]):
191
191
 
192
192
  if not response.is_success:
193
193
  raise ValueError(
194
- f"Search engine request failed with status {response.status_code}: {response.text}"
194
+ f"Search engine request failed with status {response.status_code}"
195
195
  )
196
196
 
197
197
  validated_response = WebSearchResults.model_validate(response.json())
@@ -1,3 +1,6 @@
1
+ from unique_web_search.services.search_engine.utils.grounding.bing.citation_parsing import (
2
+ BingCitationParser,
3
+ )
1
4
  from unique_web_search.services.search_engine.utils.grounding.bing.client import (
2
5
  credentials_are_valid,
3
6
  get_credentials,
@@ -8,6 +11,7 @@ from unique_web_search.services.search_engine.utils.grounding.bing.runner import
8
11
  )
9
12
 
10
13
  __all__ = [
14
+ "BingCitationParser",
11
15
  "credentials_are_valid",
12
16
  "get_credentials",
13
17
  "get_project_client",