unique_web_search 2026.32.0.dev1__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.dev1 → unique_web_search-2026.32.0.dev2}/PKG-INFO +4 -3
  2. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/pyproject.toml +5 -4
  3. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/bing.py +9 -8
  4. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/client.py +41 -10
  5. unique_web_search-2026.32.0.dev2/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +361 -0
  6. unique_web_search-2026.32.0.dev1/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -208
  7. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/README.md +0 -0
  8. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/__init__.py +0 -0
  9. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/client_settings.py +0 -0
  10. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/config.py +0 -0
  11. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/invocation_stats.py +0 -0
  12. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/metrics.py +0 -0
  13. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/prompts.py +0 -0
  14. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/schema.py +0 -0
  15. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/service.py +0 -0
  16. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/_registry.py +0 -0
  17. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
  18. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/config.py +0 -0
  19. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
  20. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/__init__.py +0 -0
  21. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/guidelines.j2 +0 -0
  22. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/guidelines_with_keywords.j2 +0 -0
  23. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/rejection_response.j2 +0 -0
  24. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/system_prompt.j2 +0 -0
  25. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/prompts/user_prompt.j2 +0 -0
  26. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/service.py +0 -0
  27. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/client/proxy_config.py +0 -0
  28. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/__init__.py +0 -0
  29. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/__init__.py +0 -0
  30. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/character_sanitize.py +0 -0
  31. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/clean.py +0 -0
  32. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/cleaning/config.py +0 -0
  33. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/config.py +0 -0
  34. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/base.py +0 -0
  35. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/config.py +0 -0
  36. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +0 -0
  37. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +0 -0
  38. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +0 -0
  39. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/__init__.py +0 -0
  40. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_and_sanitize_prompt.j2 +0 -0
  41. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/judge_prompt.j2 +0 -0
  42. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/keyword_extract_prompt.j2 +0 -0
  43. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/page_context.j2 +0 -0
  44. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/sanitize_rules.j2 +0 -0
  45. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/system_prompt.j2 +0 -0
  46. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/prompts/user_prompt.j2 +0 -0
  47. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/schema.py +0 -0
  48. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/settings.py +0 -0
  49. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/truncate.py +0 -0
  50. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/service.py +0 -0
  51. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/README.md +0 -0
  52. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/__init__.py +0 -0
  53. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/base.py +0 -0
  54. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/basic.py +0 -0
  55. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
  56. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/jina.py +0 -0
  57. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/registry.py +0 -0
  58. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/tavily.py +0 -0
  59. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/url_safety/__init__.py +0 -0
  60. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/utils.py +0 -0
  61. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/README.md +0 -0
  62. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/__init__.py +0 -0
  63. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/base_config.py +0 -0
  64. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/base_executor.py +0 -0
  65. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/context.py +0 -0
  66. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/modes.py +0 -0
  67. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
  68. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/config.py +0 -0
  69. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/executor.py +0 -0
  70. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/__init__.py +0 -0
  71. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/refine_query_system_prompt.j2 +0 -0
  72. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/tool_description.j2 +0 -0
  73. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/prompts/tool_description_for_system_prompt.j2 +0 -0
  74. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/schema.py +0 -0
  75. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
  76. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
  77. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/config.py +0 -0
  78. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/executor.py +0 -0
  79. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/__init__.py +0 -0
  80. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/step_query_description.j2 +0 -0
  81. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/tool_description.j2 +0 -0
  82. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/prompts/tool_description_for_system_prompt.j2 +0 -0
  83. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/schema.py +0 -0
  84. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
  85. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
  86. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/config.py +0 -0
  87. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/executor.py +0 -0
  88. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/prompts/__init__.py +0 -0
  89. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/prompts/tool_description.j2 +0 -0
  90. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/prompts/tool_description_for_system_prompt.j2 +0 -0
  91. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/schema.py +0 -0
  92. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
  93. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/helpers.py +0 -0
  94. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/message_log.py +0 -0
  95. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/__init__.py +0 -0
  96. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/bridge.py +0 -0
  97. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/mappers.py +0 -0
  98. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/query_elicitation.py +0 -0
  99. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/README.md +0 -0
  100. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/__init__.py +0 -0
  101. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/base.py +0 -0
  102. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/brave.py +0 -0
  103. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/custom_api.py +0 -0
  104. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/google.py +0 -0
  105. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
  106. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/registry.py +0 -0
  107. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/schema.py +0 -0
  108. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/google/schema.py +0 -0
  109. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/__init__.py +0 -0
  110. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/__init__.py +0 -0
  111. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/bing/models.py +0 -0
  112. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/models.py +0 -0
  113. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +0 -0
  114. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/__init__.py +0 -0
  115. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/client.py +0 -0
  116. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/exceptions.py +0 -0
  117. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/gemini.py +0 -0
  118. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/vertexai/response_handler.py +0 -0
  119. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/vertexai.py +0 -0
  120. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
  121. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
  122. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
  123. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/service.py +0 -0
  124. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/structured_llm.py +0 -0
  125. {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/settings.py +0 -0
  126. {unique_web_search-2026.32.0.dev1 → 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.dev1 → 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.dev1
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,7 +14,7 @@ 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
19
  Requires-Dist: unique-toolkit[monitoring]>=2026.32.0.dev4,<2026.32.0rc0
20
20
  Requires-Dist: azure-core>=1.36.0,<2
@@ -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.dev2,<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.dev1"
3
+ version = "2026.32.0.dev2"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<4.0"
@@ -20,7 +20,7 @@ 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
25
  "unique-toolkit[monitoring]>=2026.32.0.dev4,<2026.32.0rc0",
26
26
  "azure-core>=1.36.0,<2",
@@ -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.dev2,<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 }
@@ -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()
@@ -0,0 +1,361 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import logging
5
+ import time
6
+
7
+ import openai
8
+ from azure.ai.projects.aio import AIProjectClient
9
+ from azure.ai.projects.models import (
10
+ BingGroundingSearchConfiguration,
11
+ BingGroundingSearchToolParameters,
12
+ BingGroundingTool,
13
+ PromptAgentDefinition,
14
+ )
15
+ from openai.types.responses import ResponseStreamEvent
16
+ from openai.types.responses.response_completed_event import ResponseCompletedEvent
17
+ from openai.types.responses.response_output_item_done_event import (
18
+ ResponseOutputItemDoneEvent,
19
+ )
20
+ from openai.types.responses.response_output_message import ResponseOutputMessage
21
+ from openai.types.responses.response_output_text import (
22
+ AnnotationURLCitation,
23
+ ResponseOutputText,
24
+ )
25
+ from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent
26
+
27
+ from unique_web_search.invocation_stats import record_token_usage
28
+ from unique_web_search.services.search_engine.schema import (
29
+ WebSearchResult,
30
+ )
31
+ from unique_web_search.services.search_engine.utils.grounding import (
32
+ ResponseParser,
33
+ convert_response_to_search_results,
34
+ )
35
+ from unique_web_search.services.search_engine.utils.grounding.bing.client import (
36
+ get_openai_client,
37
+ )
38
+ from unique_web_search.services.search_engine.utils.grounding.bing.models import (
39
+ RESPONSE_RULE,
40
+ )
41
+ from unique_web_search.settings import env_settings
42
+
43
+ _LOGGER = logging.getLogger(__name__)
44
+ BING_AUTO_AGENT_NAME_PREFIX = "unique-grounding-with-bing"
45
+ _CONFIG_HASH_LENGTH = 12
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Agent naming
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ def _config_hash(*, model: str, fetch_size: int, instructions: str) -> str:
54
+ """Return a short hex digest of model + fetch_size + instructions for agent naming."""
55
+ payload = f"{model}\0{fetch_size}\0{instructions}".encode()
56
+ return hashlib.sha256(payload).hexdigest()[:_CONFIG_HASH_LENGTH]
57
+
58
+
59
+ def _agent_name_for_config(*, model: str, fetch_size: int, instructions: str) -> str:
60
+ """Build a Foundry-safe agent name unique to this config."""
61
+ return (
62
+ f"{BING_AUTO_AGENT_NAME_PREFIX}-"
63
+ f"{_config_hash(model=model, fetch_size=fetch_size, instructions=instructions)}"
64
+ )
65
+
66
+
67
+ def resolve_bing_agent_name(
68
+ *,
69
+ model: str,
70
+ fetch_size: int,
71
+ instructions: str,
72
+ agent_name: str | None = None,
73
+ ) -> str:
74
+ """Return the agent name to use for Responses (no Foundry round-trip).
75
+
76
+ Prefers an explicit / env-preconfigured name; otherwise derives a stable
77
+ hash-based name from ``model`` + ``fetch_size`` + ``instructions``.
78
+ """
79
+ resolved = agent_name or env_settings.azure_ai_assistant_id or None
80
+ if resolved:
81
+ return resolved
82
+ return _agent_name_for_config(
83
+ model=model, fetch_size=fetch_size, instructions=instructions
84
+ )
85
+
86
+
87
+ async def create_bing_agent(
88
+ agent_client: AIProjectClient,
89
+ *,
90
+ agent_name: str,
91
+ model: str,
92
+ fetch_size: int,
93
+ instructions: str,
94
+ ) -> str:
95
+ """Create a Foundry agent version and return its name."""
96
+ started = time.perf_counter()
97
+ agent = await agent_client.agents.create_version(
98
+ agent_name=agent_name,
99
+ definition=PromptAgentDefinition(
100
+ model=model,
101
+ instructions=instructions,
102
+ tools=[get_bing_grounding_tool(fetch_size)],
103
+ tool_choice="required",
104
+ ),
105
+ description="Unique Bing grounding agent",
106
+ )
107
+ elapsed_ms = (time.perf_counter() - started) * 1000
108
+ _LOGGER.info(
109
+ "Created Bing grounding agent version %s (id=%s) in %.0fms",
110
+ agent.name,
111
+ agent.id,
112
+ elapsed_ms,
113
+ )
114
+ return agent.name
115
+
116
+
117
+ async def get_or_create_agent_id(agent_client: AIProjectClient) -> str:
118
+ """Compatibility wrapper: return the resolved Bing grounding agent name.
119
+
120
+ Does not call Foundry. Creation happens lazily on Responses miss.
121
+ """
122
+ del agent_client # kept for call-site compatibility
123
+ return resolve_bing_agent_name(
124
+ model=env_settings.azure_ai_bing_agent_model,
125
+ fetch_size=5,
126
+ instructions=RESPONSE_RULE,
127
+ )
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Bing grounding tool
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ def get_bing_grounding_tool(fetch_size: int) -> BingGroundingTool:
136
+ """Build a BingGroundingTool configured with the environment connection string.
137
+
138
+ Args:
139
+ fetch_size: Maximum number of search results the tool should return.
140
+
141
+ Raises:
142
+ ValueError: If the Bing resource connection string is not configured.
143
+ """
144
+ connection_id = env_settings.azure_ai_bing_resource_connection_string
145
+ if not connection_id:
146
+ raise ValueError("Azure AI Bing Resource Connection String is not set")
147
+ return BingGroundingTool(
148
+ bing_grounding=BingGroundingSearchToolParameters(
149
+ search_configurations=[
150
+ BingGroundingSearchConfiguration(
151
+ project_connection_id=connection_id,
152
+ count=fetch_size,
153
+ )
154
+ ]
155
+ )
156
+ )
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Run orchestration
161
+ # ---------------------------------------------------------------------------
162
+
163
+
164
+ def _is_missing_agent_error(exc: BaseException, *, agent_name: str) -> bool:
165
+ """Return True when Responses failed because the agent does not exist."""
166
+ status_code = getattr(exc, "status_code", None)
167
+ message = str(exc).lower()
168
+ name_lower = agent_name.lower()
169
+
170
+ if isinstance(exc, openai.NotFoundError) or status_code == 404:
171
+ return True
172
+
173
+ missing_markers = (
174
+ "not found",
175
+ "does not exist",
176
+ "unknown agent",
177
+ "agent_not_found",
178
+ "no agent",
179
+ )
180
+ if any(marker in message for marker in missing_markers) and name_lower in message:
181
+ return True
182
+ if status_code in {400, 404} and any(
183
+ marker in message for marker in missing_markers
184
+ ):
185
+ return True
186
+ return False
187
+
188
+
189
+ async def create_and_process_run(
190
+ agent_client: AIProjectClient,
191
+ agent_id: str,
192
+ query: str,
193
+ fetch_size: int,
194
+ response_parsers_strategies: list[ResponseParser],
195
+ generation_instructions: str,
196
+ ) -> list[WebSearchResult]:
197
+ """Execute a Bing-grounded agent run and return parsed search results.
198
+
199
+ Optimistically calls Responses with a hashed (or preconfigured) agent name.
200
+ If the agent is missing and the name was auto-derived, creates the agent once
201
+ and retries. Preconfigured agent names are never auto-created.
202
+
203
+ Args:
204
+ agent_client: Azure AI project client used to manage agents.
205
+ agent_id: Optional Foundry agent name; empty triggers hash-based naming.
206
+ query: The search query to send to the agent.
207
+ fetch_size: Maximum number of Bing results the agent should retrieve.
208
+ response_parsers_strategies: Ordered list of parsing strategies to try
209
+ when converting the agent's free-text response into structured results.
210
+ generation_instructions: Per-request system instructions for the agent.
211
+
212
+ Raises:
213
+ Exception: If the agent run fails.
214
+ ValueError: If none of the parser strategies can parse the response.
215
+ """
216
+ instructions = f"{generation_instructions}\n{RESPONSE_RULE}"
217
+ model = env_settings.azure_ai_bing_agent_model
218
+ preconfigured = agent_id or env_settings.azure_ai_assistant_id or None
219
+ answer = await _run_responses_agent(
220
+ agent_client,
221
+ query=query,
222
+ model=model,
223
+ fetch_size=fetch_size,
224
+ instructions=instructions,
225
+ agent_name=preconfigured,
226
+ )
227
+
228
+ return await convert_response_to_search_results(answer, response_parsers_strategies)
229
+
230
+
231
+ async def _run_responses_agent(
232
+ agent_client: AIProjectClient,
233
+ *,
234
+ query: str,
235
+ model: str,
236
+ fetch_size: int,
237
+ instructions: str,
238
+ agent_name: str | None = None,
239
+ ) -> str:
240
+ """Invoke the agent via Responses API and return the full output text.
241
+
242
+ Instructions must already be baked into the agent version — Foundry returns
243
+ ``invalid_payload`` if ``instructions`` is passed alongside ``agent_reference``.
244
+ """
245
+ # Treat empty string like unset so auto-provisioning still works.
246
+ # Env assistant id is also preconfigured (same as resolve_bing_agent_name).
247
+ preconfigured = agent_name or env_settings.azure_ai_assistant_id or None
248
+ resolved_name = resolve_bing_agent_name(
249
+ model=model,
250
+ fetch_size=fetch_size,
251
+ instructions=instructions,
252
+ agent_name=preconfigured,
253
+ )
254
+ allow_create = preconfigured is None
255
+ openai_client = get_openai_client(agent_client)
256
+
257
+ try:
258
+ stream = await _create_responses_stream(
259
+ openai_client,
260
+ agent_name=resolved_name,
261
+ query=query,
262
+ )
263
+ except Exception as exc:
264
+ if not allow_create or not _is_missing_agent_error(
265
+ exc, agent_name=resolved_name
266
+ ):
267
+ raise
268
+ _LOGGER.info(
269
+ "Responses failed for missing Bing agent %s; creating then retrying: %s",
270
+ resolved_name,
271
+ exc,
272
+ )
273
+ await create_bing_agent(
274
+ agent_client,
275
+ agent_name=resolved_name,
276
+ model=model,
277
+ fetch_size=fetch_size,
278
+ instructions=instructions,
279
+ )
280
+ stream = await _create_responses_stream(
281
+ openai_client,
282
+ agent_name=resolved_name,
283
+ query=query,
284
+ )
285
+
286
+ answer_parts: list[str] = []
287
+ citation_replacements: list[tuple[str, str]] = []
288
+ emitted_text = False
289
+
290
+ async with stream:
291
+ async for event in stream:
292
+ if isinstance(event, ResponseTextDeltaEvent):
293
+ if event.delta:
294
+ emitted_text = True
295
+ answer_parts.append(event.delta)
296
+ elif isinstance(event, ResponseOutputItemDoneEvent):
297
+ citation_replacements.extend(_extract_markdown_citations(event))
298
+ elif isinstance(event, ResponseCompletedEvent):
299
+ output_text = event.response.output_text
300
+ # Foundry sometimes delivers the full answer only on completion.
301
+ if output_text and not emitted_text:
302
+ emitted_text = True
303
+ answer_parts.append(output_text)
304
+ record_token_usage(
305
+ model_name=model,
306
+ usage=event.response.usage,
307
+ source="web_search.grounding.bing",
308
+ )
309
+
310
+ answer = "".join(answer_parts)
311
+ for marker, markdown_link in citation_replacements:
312
+ answer = answer.replace(marker, markdown_link)
313
+ return answer
314
+
315
+
316
+ async def _create_responses_stream(
317
+ openai_client: openai.AsyncOpenAI,
318
+ *,
319
+ agent_name: str,
320
+ query: str,
321
+ ) -> openai.AsyncStream[ResponseStreamEvent]:
322
+ started = time.perf_counter()
323
+ stream = await openai_client.responses.create(
324
+ stream=True,
325
+ input=query,
326
+ extra_body={
327
+ "agent_reference": {
328
+ "name": agent_name,
329
+ "type": "agent_reference",
330
+ }
331
+ },
332
+ )
333
+ elapsed_ms = (time.perf_counter() - started) * 1000
334
+ _LOGGER.info(
335
+ "Opened Bing Responses stream for agent %s in %.0fms",
336
+ agent_name,
337
+ elapsed_ms,
338
+ )
339
+ return stream
340
+
341
+
342
+ def _extract_markdown_citations(
343
+ event: ResponseOutputItemDoneEvent,
344
+ ) -> list[tuple[str, str]]:
345
+ """Extract (placeholder, markdown-link) pairs from a message output item."""
346
+ item = event.item
347
+ if not isinstance(item, ResponseOutputMessage) or not item.content:
348
+ return []
349
+ text_content = item.content[-1]
350
+ if not isinstance(text_content, ResponseOutputText):
351
+ return []
352
+
353
+ replacements: list[tuple[str, str]] = []
354
+ for annotation in text_content.annotations:
355
+ if not isinstance(annotation, AnnotationURLCitation):
356
+ continue
357
+ marker = text_content.text[annotation.start_index : annotation.end_index]
358
+ title = annotation.title or annotation.url
359
+ if annotation.url and marker:
360
+ replacements.append((marker, f"[{title}]({annotation.url})"))
361
+ return replacements
@@ -1,208 +0,0 @@
1
- import logging
2
-
3
- from azure.ai.agents.models import (
4
- AgentThreadCreationOptions,
5
- BingGroundingTool,
6
- MessageTextContent,
7
- MessageTextUrlCitationAnnotation,
8
- RunStatus,
9
- ThreadMessageOptions,
10
- )
11
- from azure.ai.projects.aio import AIProjectClient
12
-
13
- from unique_web_search.invocation_stats import record_token_usage
14
- from unique_web_search.services.search_engine.schema import (
15
- WebSearchResult,
16
- )
17
- from unique_web_search.services.search_engine.utils.grounding import (
18
- ResponseParser,
19
- convert_response_to_search_results,
20
- )
21
- from unique_web_search.services.search_engine.utils.grounding.bing.models import (
22
- RESPONSE_RULE,
23
- )
24
- from unique_web_search.settings import env_settings
25
-
26
- _LOGGER = logging.getLogger(__name__)
27
- _AGENT_NAME_IDENTIFIER = "UNIQUE_GROUNDING_WITH_BING_AGENT"
28
-
29
-
30
- # ---------------------------------------------------------------------------
31
- # Agent management
32
- # ---------------------------------------------------------------------------
33
-
34
-
35
- async def _get_agent_id(agent_client: AIProjectClient) -> str:
36
- """Look up the existing Bing grounding agent by name.
37
-
38
- Raises:
39
- Exception: If no agent with the expected name exists.
40
- """
41
- list_agents = agent_client.agents.list_agents()
42
-
43
- async for agent in list_agents:
44
- if agent.name == _AGENT_NAME_IDENTIFIER:
45
- _LOGGER.info(f"Found agent {agent.id} with name {agent.name}")
46
- return agent.id
47
-
48
- raise Exception(f"Agent {_AGENT_NAME_IDENTIFIER} not found")
49
-
50
-
51
- async def _create_agent_id(agent_client: AIProjectClient) -> str:
52
- """Provision a new Bing grounding agent using the configured model."""
53
- agent = await agent_client.agents.create_agent(
54
- name=_AGENT_NAME_IDENTIFIER,
55
- model=env_settings.azure_ai_bing_agent_model,
56
- )
57
- return agent.id
58
-
59
-
60
- async def get_or_create_agent_id(agent_client: AIProjectClient) -> str:
61
- """Return the Bing grounding agent id, creating one if it doesn't exist yet."""
62
- if env_settings.azure_ai_assistant_id:
63
- return env_settings.azure_ai_assistant_id
64
-
65
- try:
66
- _LOGGER.info("Looking for existing agent")
67
- return await _get_agent_id(agent_client)
68
- except Exception as e:
69
- _LOGGER.exception(f"Error getting agent: {e}")
70
- _LOGGER.info("No existing agent found, creating a new one")
71
- return await _create_agent_id(agent_client)
72
-
73
-
74
- # ---------------------------------------------------------------------------
75
- # Bing grounding tool
76
- # ---------------------------------------------------------------------------
77
-
78
-
79
- def get_bing_grounding_tool(fetch_size: int) -> BingGroundingTool:
80
- """Build a BingGroundingTool configured with the environment connection string.
81
-
82
- Args:
83
- fetch_size: Maximum number of search results the tool should return.
84
-
85
- Raises:
86
- ValueError: If the Bing resource connection string is not configured.
87
- """
88
- connection_id = env_settings.azure_ai_bing_resource_connection_string
89
- if not connection_id:
90
- raise ValueError("Azure AI Bing Resource Connection String is not set")
91
- return BingGroundingTool(connection_id=connection_id, count=fetch_size)
92
-
93
-
94
- # ---------------------------------------------------------------------------
95
- # Run orchestration
96
- # ---------------------------------------------------------------------------
97
-
98
-
99
- async def create_and_process_run(
100
- agent_client: AIProjectClient,
101
- agent_id: str,
102
- query: str,
103
- fetch_size: int,
104
- response_parsers_strategies: list[ResponseParser],
105
- generation_instructions: str,
106
- ) -> list[WebSearchResult]:
107
- """Execute a Bing-grounded agent run and return parsed search results.
108
-
109
- Creates a thread with the user query, runs the agent with Bing grounding,
110
- then converts the unstructured response into ``WebSearchResult`` objects by
111
- trying each parser strategy in order until one succeeds.
112
-
113
- Args:
114
- agent_client: Azure AI project client used to manage agents and threads.
115
- query: The search query to send to the agent.
116
- fetch_size: Maximum number of Bing results the agent should retrieve.
117
- response_parsers_strategies: Ordered list of parsing strategies to try
118
- when converting the agent's free-text response into structured results.
119
-
120
- Raises:
121
- Exception: If the agent run fails, is cancelled, or expires.
122
- ValueError: If none of the parser strategies can parse the response.
123
- """
124
- if not agent_id:
125
- _LOGGER.warning("No agent ID provided")
126
- thread = await _create_agent_and_run_thread(
127
- agent_client, query, fetch_size, generation_instructions
128
- )
129
- else:
130
- _LOGGER.warning(f"Using existing agent ID: {agent_id}")
131
- thread = await _create_agent_run_with_agent_id(agent_client, agent_id, query)
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
-
139
- if thread.status in [RunStatus.FAILED, RunStatus.CANCELLED, RunStatus.EXPIRED]:
140
- raise Exception(f"Run failed: {thread.last_error}")
141
-
142
- answer = await _get_answer_from_thread(thread.thread_id, agent_client)
143
-
144
- search_results = await convert_response_to_search_results(
145
- answer, response_parsers_strategies
146
- )
147
-
148
- return search_results
149
-
150
-
151
- async def _create_agent_run_with_agent_id(
152
- agent_client: AIProjectClient,
153
- agent_id: str,
154
- query: str,
155
- ):
156
- """Execute a Bing-grounded agent run and return parsed search results."""
157
- agent_run = await agent_client.agents.create_thread_and_process_run(
158
- agent_id=agent_id,
159
- thread=AgentThreadCreationOptions(
160
- messages=[ThreadMessageOptions(role="user", content=query)]
161
- ),
162
- )
163
- return agent_run
164
-
165
-
166
- async def _create_agent_and_run_thread(
167
- agent_client: AIProjectClient,
168
- query: str,
169
- fetch_size: int,
170
- generation_instructions: str,
171
- ):
172
- agent_id = await get_or_create_agent_id(agent_client)
173
-
174
- instructions = f"{generation_instructions}\n{RESPONSE_RULE}"
175
-
176
- agent_run = await agent_client.agents.create_thread_and_process_run(
177
- agent_id=agent_id,
178
- model=env_settings.azure_ai_bing_agent_model,
179
- toolset=get_bing_grounding_tool(fetch_size=fetch_size), # type: ignore
180
- instructions=instructions,
181
- thread=AgentThreadCreationOptions(
182
- messages=[ThreadMessageOptions(role="user", content=query)]
183
- ),
184
- )
185
- return agent_run
186
-
187
-
188
- async def _get_answer_from_thread(thread_id: str, agent_client: AIProjectClient) -> str:
189
- """Concatenate all assistant text messages from a thread into a single string."""
190
- messages = agent_client.agents.messages.list(thread_id=thread_id)
191
- answer = ""
192
- citations: list[MessageTextUrlCitationAnnotation] = []
193
-
194
- async for message in messages:
195
- if message.role == "assistant":
196
- for content in message.content:
197
- if isinstance(content, MessageTextContent):
198
- answer += content.text.value
199
- elif isinstance(content, MessageTextUrlCitationAnnotation):
200
- citations.append(content)
201
-
202
- for citation in citations:
203
- answer = answer.replace(
204
- citation.text,
205
- f"[{citation.url_citation.title}]({citation.url_citation.url})",
206
- )
207
-
208
- return answer