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.
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/PKG-INFO +4 -3
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/pyproject.toml +5 -4
- {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
- {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
- unique_web_search-2026.32.0.dev2/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +361 -0
- unique_web_search-2026.32.0.dev1/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -208
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/README.md +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/client_settings.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/config.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/invocation_stats.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/prompts.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/service.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/_registry.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/README.md +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/base.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/basic.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/jina.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/registry.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/tavily.py +0 -0
- {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
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/README.md +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/__init__.py +0 -0
- {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
- {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
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/context.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/modes.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/bridge.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/mappers.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/query_elicitation.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/structured_llm.py +0 -0
- {unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/settings.py +0 -0
- {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
- {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.
|
|
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>=
|
|
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.
|
|
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.
|
|
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>=
|
|
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.
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_web_search-2026.32.0.dev1 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/utils.py
RENAMED
|
File without changes
|