unique_web_search 2026.32.0.dev0__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.dev0 → unique_web_search-2026.32.0.dev2}/PKG-INFO +5 -4
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/pyproject.toml +6 -5
- unique_web_search-2026.32.0.dev2/src/unique_web_search/invocation_stats.py +113 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/service.py +26 -1
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/service.py +1 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_guard_judge.py +13 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_keyword_redact.py +6 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/processing_strategies/llm_process.py +6 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/base_executor.py +11 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/executor.py +6 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/bing.py +9 -8
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/utils/grounding/response_parsing.py +6 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/vertexai.py +6 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/service.py +6 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/structured_llm.py +8 -0
- unique_web_search-2026.32.0.dev0/src/unique_web_search/services/search_engine/utils/grounding/bing/runner.py +0 -201
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/README.md +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/client_settings.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/metrics.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/prompts.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/schema.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/_registry.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/argument_screening/exceptions.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/client/proxy_config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/content_processing/service.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/README.md +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/base.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/basic.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/firecrawl.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/jina.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/registry.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/tavily.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/crawlers/utils.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/README.md +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/base_config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/context.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/modes.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/schema.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v1/strategy.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/executor.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/schema.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v2/strategy.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/config.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/executor.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/schema.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/executors/v3/strategy.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/helpers.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/message_log.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/bridge.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/proxy/mappers.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/query_elicitation.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/README.md +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/base.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/brave.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/google.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/perplexity.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/registry.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/search_engine/schema.py +0 -0
- {unique_web_search-2026.32.0.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → 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.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/__init__.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/prompts.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/services/snippet_judge/schema.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/settings.py +0 -0
- {unique_web_search-2026.32.0.dev0 → unique_web_search-2026.32.0.dev2}/src/unique_web_search/skills/web-search-v3.md +0 -0
- {unique_web_search-2026.32.0.dev0 → 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,9 +14,9 @@ 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
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.32.0.
|
|
19
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.32.0.dev4,<2026.32.0rc0
|
|
20
20
|
Requires-Dist: azure-core>=1.36.0,<2
|
|
21
21
|
Requires-Dist: google-cloud-aiplatform>=1.128.0,<2
|
|
22
22
|
Requires-Dist: google-auth>=2.43.0,<3
|
|
@@ -25,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,9 +20,9 @@ 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
|
-
"unique-toolkit[monitoring]>=2026.32.0.
|
|
25
|
+
"unique-toolkit[monitoring]>=2026.32.0.dev4,<2026.32.0rc0",
|
|
26
26
|
"azure-core>=1.36.0,<2",
|
|
27
27
|
"google-cloud-aiplatform>=1.128.0,<2",
|
|
28
28
|
"google-auth>=2.43.0,<3",
|
|
@@ -31,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 }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Run-scoped collection of language-model invocation usage."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterable, Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar, Token
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
from unique_toolkit.language_model.invocation_stats import LanguageModelInvocationStats
|
|
11
|
+
from unique_toolkit.language_model.schemas import LanguageModelTokenUsage
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_CURRENT_INVOCATION_STATS: ContextVar[list[LanguageModelInvocationStats] | None] = (
|
|
16
|
+
ContextVar("web_search_invocation_stats", default=None)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def invocation_stats_scope() -> Iterator[list[LanguageModelInvocationStats]]:
|
|
22
|
+
"""Collect stats for one WebSearch run and isolate concurrent runs."""
|
|
23
|
+
invocation_stats: list[LanguageModelInvocationStats] = []
|
|
24
|
+
token: Token[list[LanguageModelInvocationStats] | None] = (
|
|
25
|
+
_CURRENT_INVOCATION_STATS.set(invocation_stats)
|
|
26
|
+
)
|
|
27
|
+
try:
|
|
28
|
+
yield invocation_stats
|
|
29
|
+
finally:
|
|
30
|
+
_CURRENT_INVOCATION_STATS.reset(token)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def record_invocation_stats(
|
|
34
|
+
invocation_stats: Iterable[LanguageModelInvocationStats],
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Record already-built invocation stats when a run scope is active.
|
|
37
|
+
|
|
38
|
+
Used for usage produced by nested dependencies that hand back
|
|
39
|
+
``LanguageModelInvocationStats`` objects directly (e.g. the chunk relevancy
|
|
40
|
+
sorter attaching per-chunk stats on its result), rather than a raw provider
|
|
41
|
+
usage payload.
|
|
42
|
+
"""
|
|
43
|
+
current_invocation_stats = _CURRENT_INVOCATION_STATS.get()
|
|
44
|
+
if current_invocation_stats is not None:
|
|
45
|
+
current_invocation_stats.extend(invocation_stats)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def record_token_usage(
|
|
49
|
+
*,
|
|
50
|
+
model_name: str,
|
|
51
|
+
usage: Any,
|
|
52
|
+
source: str,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Record provider usage when both a run scope and usage are available."""
|
|
55
|
+
invocation_stats = _CURRENT_INVOCATION_STATS.get()
|
|
56
|
+
if invocation_stats is None or usage is None:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
token_usage = LanguageModelTokenUsage.model_validate(usage)
|
|
61
|
+
except ValidationError:
|
|
62
|
+
_LOGGER.warning(
|
|
63
|
+
"Unable to parse Web Search token usage for %s",
|
|
64
|
+
source,
|
|
65
|
+
exc_info=True,
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
invocation_stats.append(
|
|
70
|
+
LanguageModelInvocationStats.from_usage(
|
|
71
|
+
model_name=model_name,
|
|
72
|
+
token_usage=token_usage,
|
|
73
|
+
source=source,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def record_language_model_response(
|
|
79
|
+
*,
|
|
80
|
+
model_name: str,
|
|
81
|
+
response: Any,
|
|
82
|
+
source: str,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Record usage from a Unique Toolkit language-model response."""
|
|
85
|
+
record_token_usage(
|
|
86
|
+
model_name=model_name,
|
|
87
|
+
usage=getattr(response, "usage", None),
|
|
88
|
+
source=source,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def record_vertex_response(
|
|
93
|
+
*,
|
|
94
|
+
model_name: str,
|
|
95
|
+
response: Any,
|
|
96
|
+
source: str,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Record Google GenAI usage metadata using toolkit token names."""
|
|
99
|
+
usage = getattr(response, "usage_metadata", None)
|
|
100
|
+
if usage is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
record_token_usage(
|
|
104
|
+
model_name=model_name,
|
|
105
|
+
usage={
|
|
106
|
+
"prompt_tokens": getattr(usage, "prompt_token_count", None),
|
|
107
|
+
"completion_tokens": getattr(usage, "candidates_token_count", None),
|
|
108
|
+
"total_tokens": getattr(usage, "total_token_count", None),
|
|
109
|
+
"reasoning_tokens": getattr(usage, "thoughts_token_count", None),
|
|
110
|
+
"cached_tokens": getattr(usage, "cached_content_token_count", None),
|
|
111
|
+
},
|
|
112
|
+
source=source,
|
|
113
|
+
)
|
|
@@ -25,6 +25,7 @@ from unique_toolkit.language_model.schemas import (
|
|
|
25
25
|
from unique_toolkit.monitoring import metric_scope
|
|
26
26
|
|
|
27
27
|
from unique_web_search.config import WebSearchConfig
|
|
28
|
+
from unique_web_search.invocation_stats import invocation_stats_scope
|
|
28
29
|
from unique_web_search.metrics import tool_duration, tool_empty_results, tool_errors
|
|
29
30
|
from unique_web_search.schema import WebSearchDebugInfo
|
|
30
31
|
from unique_web_search.services.argument_screening import (
|
|
@@ -104,7 +105,6 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
104
105
|
self._mode_strategy = get_mode_strategy(self.config.web_search_mode_config)
|
|
105
106
|
|
|
106
107
|
def content_reducer(web_page_chunks: list[WebPageChunk]) -> list[WebPageChunk]:
|
|
107
|
-
|
|
108
108
|
return reduce_sources_to_token_limit(
|
|
109
109
|
web_page_chunks,
|
|
110
110
|
self.config.language_model_max_input_tokens,
|
|
@@ -153,6 +153,31 @@ class WebSearchTool(Tool[WebSearchConfig]):
|
|
|
153
153
|
|
|
154
154
|
@override
|
|
155
155
|
async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
|
|
156
|
+
with invocation_stats_scope() as invocation_stats:
|
|
157
|
+
try:
|
|
158
|
+
response = await self._run(tool_call)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
# _run() already catches its own errors and returns a
|
|
161
|
+
# ToolCallResponse gracefully in the common case; this is a
|
|
162
|
+
# safety net for the rarer case where something raises while
|
|
163
|
+
# handling that failure (or otherwise escapes _run()
|
|
164
|
+
# uncaught). Without it, tokens already spent on screening/
|
|
165
|
+
# processing/relevancy before the failure would never reach
|
|
166
|
+
# analytics, since SafeTaskExecutor builds its own fresh,
|
|
167
|
+
# stats-less error response one level up.
|
|
168
|
+
# logger.exception() already attaches the active exception's
|
|
169
|
+
# type and traceback; don't stringify it into the message.
|
|
170
|
+
_LOGGER.exception("WebSearch tool run failed")
|
|
171
|
+
return ToolCallResponse(
|
|
172
|
+
id=tool_call.id, # type: ignore
|
|
173
|
+
name=self.name,
|
|
174
|
+
error_message=str(e),
|
|
175
|
+
invocation_stats=invocation_stats,
|
|
176
|
+
)
|
|
177
|
+
response.invocation_stats.extend(invocation_stats)
|
|
178
|
+
return response
|
|
179
|
+
|
|
180
|
+
async def _run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
|
|
156
181
|
_LOGGER.info("Running the WebSearch tool")
|
|
157
182
|
start_time = time()
|
|
158
183
|
parameters = self.tool_parameter_calls.model_validate(
|
|
@@ -116,6 +116,7 @@ class ArgumentScreeningService:
|
|
|
116
116
|
system_message=self._config.system_prompt,
|
|
117
117
|
user_message=user_prompt,
|
|
118
118
|
response_model=ArgumentScreeningResult,
|
|
119
|
+
invocation_source="web_search.argument_screening",
|
|
119
120
|
)
|
|
120
121
|
except StructuredLlmUnparseableResponseError as e:
|
|
121
122
|
raise ArgumentScreeningUnparseableResponseException(
|
|
@@ -6,6 +6,7 @@ from unique_toolkit._common.utils.jinja.render import render_template
|
|
|
6
6
|
from unique_toolkit.language_model import LanguageModelService
|
|
7
7
|
from unique_toolkit.language_model.builder import MessagesBuilder
|
|
8
8
|
|
|
9
|
+
from unique_web_search.invocation_stats import record_language_model_response
|
|
9
10
|
from unique_web_search.services.content_processing.processing_strategies.base import (
|
|
10
11
|
ProcessingStrategyKwargs,
|
|
11
12
|
WebSearchResult,
|
|
@@ -309,6 +310,18 @@ class LLMGuardJudge:
|
|
|
309
310
|
structured_output_model=response_model,
|
|
310
311
|
structured_output_enforce_schema=True,
|
|
311
312
|
)
|
|
313
|
+
source_by_response_model = {
|
|
314
|
+
LLMGuardResponse: "web_search.content_processing.guard_sanitize",
|
|
315
|
+
JudgeResponse: "web_search.content_processing.guard_judge",
|
|
316
|
+
JudgeAndSanitizeResponse: (
|
|
317
|
+
"web_search.content_processing.guard_judge_and_sanitize"
|
|
318
|
+
),
|
|
319
|
+
}
|
|
320
|
+
record_language_model_response(
|
|
321
|
+
model_name=self._config.language_model.name,
|
|
322
|
+
response=response,
|
|
323
|
+
source=source_by_response_model[response_model],
|
|
324
|
+
)
|
|
312
325
|
|
|
313
326
|
if response.choices[0].message.parsed is None:
|
|
314
327
|
raise ValueError("Judge call returned no parsed response")
|
|
@@ -8,6 +8,7 @@ from unique_toolkit._common.utils.jinja.render import render_template
|
|
|
8
8
|
from unique_toolkit.language_model import LanguageModelService
|
|
9
9
|
from unique_toolkit.language_model.builder import MessagesBuilder
|
|
10
10
|
|
|
11
|
+
from unique_web_search.invocation_stats import record_language_model_response
|
|
11
12
|
from unique_web_search.services.content_processing.processing_strategies.base import (
|
|
12
13
|
ProcessingStrategyKwargs,
|
|
13
14
|
WebSearchResult,
|
|
@@ -180,6 +181,11 @@ class LLMKeywordRedact:
|
|
|
180
181
|
structured_output_model=KeywordRedactResponse,
|
|
181
182
|
structured_output_enforce_schema=True,
|
|
182
183
|
)
|
|
184
|
+
record_language_model_response(
|
|
185
|
+
model_name=self._config.language_model.name,
|
|
186
|
+
response=response,
|
|
187
|
+
source="web_search.content_processing.keyword_redact",
|
|
188
|
+
)
|
|
183
189
|
|
|
184
190
|
if response.choices[0].message.parsed is None:
|
|
185
191
|
raise ValueError("Keyword extraction call returned no parsed response")
|
|
@@ -13,6 +13,7 @@ from unique_toolkit.language_model import (
|
|
|
13
13
|
)
|
|
14
14
|
from unique_toolkit.language_model.builder import MessagesBuilder
|
|
15
15
|
|
|
16
|
+
from unique_web_search.invocation_stats import record_language_model_response
|
|
16
17
|
from unique_web_search.services.content_processing.processing_strategies.base import (
|
|
17
18
|
ProcessingStrategyKwargs,
|
|
18
19
|
WebSearchResult,
|
|
@@ -316,6 +317,11 @@ class LLMProcess:
|
|
|
316
317
|
structured_output_model=LLMProcessorResponse,
|
|
317
318
|
structured_output_enforce_schema=True,
|
|
318
319
|
)
|
|
320
|
+
record_language_model_response(
|
|
321
|
+
model_name=self._config.language_model.name,
|
|
322
|
+
response=response,
|
|
323
|
+
source="web_search.content_processing.llm_process",
|
|
324
|
+
)
|
|
319
325
|
|
|
320
326
|
if response.choices[0].message.parsed is None:
|
|
321
327
|
raise ValueError("Failed to parse response")
|
|
@@ -12,6 +12,7 @@ from unique_toolkit.content import ContentChunk
|
|
|
12
12
|
from unique_toolkit.language_model import LanguageModelFunction
|
|
13
13
|
from unique_toolkit.monitoring import metric_scope
|
|
14
14
|
|
|
15
|
+
from unique_web_search.invocation_stats import record_invocation_stats
|
|
15
16
|
from unique_web_search.metrics import llm_duration, llm_errors
|
|
16
17
|
from unique_web_search.schema import (
|
|
17
18
|
StepDebugInfo,
|
|
@@ -160,6 +161,16 @@ class BaseWebSearchExecutor(ABC, Generic[T]):
|
|
|
160
161
|
chunks=content,
|
|
161
162
|
config=self.chunk_relevancy_sort_config,
|
|
162
163
|
)
|
|
164
|
+
# The relevancy sorter makes one LLM call per chunk and attaches the
|
|
165
|
+
# usage to each ChunkRelevancy.relevancy.invocation_stats; merge it
|
|
166
|
+
# into the active WebSearch stats scope so it isn't dropped from
|
|
167
|
+
# token analytics (Internal Search does the same after its sort).
|
|
168
|
+
record_invocation_stats(
|
|
169
|
+
invocation
|
|
170
|
+
for relevancy in sorted_chunks.relevancies
|
|
171
|
+
if relevancy.relevancy is not None
|
|
172
|
+
for invocation in relevancy.relevancy.invocation_stats
|
|
173
|
+
)
|
|
163
174
|
_LOGGER.info(f"Sorting chunks message: {sorted_chunks.user_message}")
|
|
164
175
|
return sorted_chunks.content_chunks
|
|
165
176
|
else:
|
|
@@ -12,6 +12,7 @@ from unique_toolkit.language_model import LanguageModelFunction
|
|
|
12
12
|
from unique_toolkit.language_model.builder import MessagesBuilder
|
|
13
13
|
from unique_toolkit.monitoring import metric_scope
|
|
14
14
|
|
|
15
|
+
from unique_web_search.invocation_stats import record_language_model_response
|
|
15
16
|
from unique_web_search.metrics import (
|
|
16
17
|
crawl_duration,
|
|
17
18
|
crawl_errors,
|
|
@@ -124,6 +125,11 @@ async def query_generation_agent(
|
|
|
124
125
|
structured_output_model=structured_output_model,
|
|
125
126
|
structured_output_enforce_schema=True,
|
|
126
127
|
)
|
|
128
|
+
record_language_model_response(
|
|
129
|
+
model_name=language_model.name,
|
|
130
|
+
response=response,
|
|
131
|
+
source="web_search.query_refinement",
|
|
132
|
+
)
|
|
127
133
|
|
|
128
134
|
parsed_response = response.choices[0].message.parsed
|
|
129
135
|
if parsed_response is None:
|
|
@@ -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()
|