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