unique-search-proxy 2026.44.0.dev0__tar.gz → 2026.44.0.dev1__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_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/PKG-INFO +4 -3
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/pyproject.toml +4 -3
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/v1/agent_search.py +17 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/app.py +1 -1
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +19 -7
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +38 -7
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +13 -1
- unique_search_proxy-2026.44.0.dev1/unique_search_proxy_client/web/core/agent_engines/bing/service.py +185 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/factory.py +7 -2
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/service_base.py +1 -11
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +18 -6
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +1 -1
- unique_search_proxy-2026.44.0.dev1/unique_search_proxy_client/web/core/client/azure_transport.py +146 -0
- unique_search_proxy-2026.44.0.dev0/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -109
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/README.md +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/health.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/v1/crawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/api/v1/search.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/client/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/provider_response.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/providers.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/error_handlers.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/generator/schema.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/generator/template.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/metadata.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/logging_config.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/middleware/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/presets/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/presets/common.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/presets/crawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/presets/search.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/presets/types.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/app.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/startup_report.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/utils/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev0 → unique_search_proxy-2026.44.0.dev1}/unique_search_proxy_client/web/utils/url.py +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-search-proxy
|
|
3
|
-
Version: 2026.44.0.
|
|
3
|
+
Version: 2026.44.0.dev1
|
|
4
4
|
Summary: Web Search Proxy implementation
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
7
|
+
Requires-Dist: aiohttp>=3.14.1,<4
|
|
7
8
|
Requires-Dist: fastapi>=0.133.0,<1.0.0
|
|
8
9
|
Requires-Dist: starlette>=1.3.1,<2.0.0
|
|
9
10
|
Requires-Dist: uvicorn[standard]>=0.32.0,<1.0.0
|
|
@@ -19,8 +20,8 @@ Requires-Dist: certifi>=2025.11.12,<2027
|
|
|
19
20
|
Requires-Dist: google-genai>=1.73.0,<2
|
|
20
21
|
Requires-Dist: google-auth>=2.43.0,<3
|
|
21
22
|
Requires-Dist: jinja2>=3.1.6,<4
|
|
22
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.
|
|
23
|
-
Requires-Dist: unique-search-proxy-core>=2026.44.0.
|
|
23
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev2,<2026.44.0rc0
|
|
24
|
+
Requires-Dist: unique-search-proxy-core>=2026.44.0.dev1,<2026.44.0rc0
|
|
24
25
|
Requires-Dist: openai>=1.105.0,<3
|
|
25
26
|
Requires-Python: >=3.12
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy"
|
|
3
|
-
version = "2026.44.0.
|
|
3
|
+
version = "2026.44.0.dev1"
|
|
4
4
|
description = "Web Search Proxy implementation"
|
|
5
5
|
authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.12"
|
|
8
8
|
dependencies = [
|
|
9
|
+
"aiohttp>=3.14.1,<4",
|
|
9
10
|
"fastapi>=0.133.0,<1.0.0",
|
|
10
11
|
"starlette>=1.3.1,<2.0.0",
|
|
11
12
|
"uvicorn[standard]>=0.32.0,<1.0.0",
|
|
@@ -21,8 +22,8 @@ dependencies = [
|
|
|
21
22
|
"google-genai>=1.73.0,<2",
|
|
22
23
|
"google-auth>=2.43.0,<3",
|
|
23
24
|
"jinja2>=3.1.6,<4",
|
|
24
|
-
"unique-toolkit[monitoring]>=2026.44.0.
|
|
25
|
-
"unique-search-proxy-core>=2026.44.0.
|
|
25
|
+
"unique-toolkit[monitoring]>=2026.44.0.dev2,<2026.44.0rc0",
|
|
26
|
+
"unique-search-proxy-core>=2026.44.0.dev1,<2026.44.0rc0",
|
|
26
27
|
"openai>=1.105.0,<3",
|
|
27
28
|
]
|
|
28
29
|
|
|
@@ -23,9 +23,11 @@ from unique_search_proxy_core.schema import (
|
|
|
23
23
|
ProxyErrorCode,
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
+
from unique_search_proxy_client.web.context import get_request_context
|
|
26
27
|
from unique_search_proxy_client.web.core.agent_engines.factory import (
|
|
27
28
|
get_agent_engine_service,
|
|
28
29
|
)
|
|
30
|
+
from unique_search_proxy_client.web.core.client import get_http_client_registry
|
|
29
31
|
from unique_search_proxy_client.web.monitoring.metrics import (
|
|
30
32
|
record_agent_search_error,
|
|
31
33
|
record_agent_search_success,
|
|
@@ -84,8 +86,12 @@ async def agent_search(
|
|
|
84
86
|
)
|
|
85
87
|
|
|
86
88
|
try:
|
|
89
|
+
registry = get_http_client_registry(request.app)
|
|
90
|
+
egress = await registry.egress_for(get_request_context())
|
|
87
91
|
engine_service = get_agent_engine_service(
|
|
88
92
|
AgentEngineType(engine_id) if isinstance(engine, str) else engine,
|
|
93
|
+
http_client=egress.http_client,
|
|
94
|
+
egress_route=egress.route,
|
|
89
95
|
)
|
|
90
96
|
async with asyncio.timeout(timeout):
|
|
91
97
|
result = await engine_service.search(body)
|
|
@@ -162,9 +168,20 @@ async def agent_search_stream(
|
|
|
162
168
|
)
|
|
163
169
|
|
|
164
170
|
try:
|
|
171
|
+
registry = get_http_client_registry(request.app)
|
|
172
|
+
egress = await registry.egress_for(get_request_context())
|
|
165
173
|
engine_service = get_agent_engine_service(
|
|
166
174
|
AgentEngineType(engine_id) if isinstance(engine, str) else engine,
|
|
175
|
+
http_client=egress.http_client,
|
|
176
|
+
egress_route=egress.route,
|
|
167
177
|
)
|
|
178
|
+
except ProxyError as exc:
|
|
179
|
+
record_agent_search_error(
|
|
180
|
+
engine_id,
|
|
181
|
+
exc.code.value if hasattr(exc.code, "value") else str(exc.code),
|
|
182
|
+
time.perf_counter() - started,
|
|
183
|
+
)
|
|
184
|
+
raise _agent_search_request_context(exc, engine_id=engine_id) from exc
|
|
168
185
|
except Exception as exc:
|
|
169
186
|
record_agent_search_error(engine_id, "INTERNAL_ERROR", 0.0)
|
|
170
187
|
_LOGGER.exception(
|
|
@@ -59,10 +59,10 @@ logging.getLogger("uvicorn.access").addFilter(HealthCheckFilter())
|
|
|
59
59
|
async def lifespan(app: FastAPI):
|
|
60
60
|
_LOGGER.info("Starting Unique Search Proxy...")
|
|
61
61
|
log_startup_settings_report(_LOGGER)
|
|
62
|
-
await maybe_cleanup_auto_provisioned_bing_agents_on_start()
|
|
63
62
|
registry = await create_http_client_registry()
|
|
64
63
|
app.state.http_client_registry = registry
|
|
65
64
|
try:
|
|
65
|
+
await maybe_cleanup_auto_provisioned_bing_agents_on_start(registry)
|
|
66
66
|
yield
|
|
67
67
|
finally:
|
|
68
68
|
await aclose_private_endpoint_http_client()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from azure.ai.projects.aio import AIProjectClient
|
|
6
7
|
from azure.core.exceptions import ResourceNotFoundError
|
|
@@ -10,6 +11,7 @@ from unique_search_proxy_core.agent_engines.bing.grounding import (
|
|
|
10
11
|
from unique_search_proxy_core.errors import EngineNotConfiguredError
|
|
11
12
|
|
|
12
13
|
from unique_search_proxy_client.web.core.agent_engines.bing.client import (
|
|
14
|
+
bing_azure_transport,
|
|
13
15
|
get_credentials,
|
|
14
16
|
get_project_client,
|
|
15
17
|
)
|
|
@@ -18,6 +20,9 @@ from unique_search_proxy_client.web.settings.providers.bing_agent import (
|
|
|
18
20
|
)
|
|
19
21
|
from unique_search_proxy_client.web.settings.secret_str import read_secret
|
|
20
22
|
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from unique_search_proxy_core.http_client import HttpClientRegistry
|
|
25
|
+
|
|
21
26
|
_LOGGER = logging.getLogger(__name__)
|
|
22
27
|
_LIST_PAGE_SIZE = 100
|
|
23
28
|
|
|
@@ -53,11 +58,15 @@ async def cleanup_auto_provisioned_bing_agents(project_client: AIProjectClient)
|
|
|
53
58
|
return deleted
|
|
54
59
|
|
|
55
60
|
|
|
56
|
-
async def maybe_cleanup_auto_provisioned_bing_agents_on_start(
|
|
61
|
+
async def maybe_cleanup_auto_provisioned_bing_agents_on_start(
|
|
62
|
+
registry: HttpClientRegistry,
|
|
63
|
+
) -> None:
|
|
57
64
|
"""Best-effort startup cleanup when ``BING_AGENT_CLEANUP_ON_START`` is enabled.
|
|
58
65
|
|
|
59
66
|
Never raises: credential, client, list, or delete failures are logged and
|
|
60
67
|
swallowed so multi-worker/replica races cannot block process startup.
|
|
68
|
+
Uses settings-backed proxy credentials because no request identity exists
|
|
69
|
+
during process startup; missing technical credentials fail closed.
|
|
61
70
|
Transient Bing request failures during a race are recovered by create-on-miss.
|
|
62
71
|
"""
|
|
63
72
|
if not bing_agent_credentials.cleanup_on_start:
|
|
@@ -73,12 +82,15 @@ async def maybe_cleanup_auto_provisioned_bing_agents_on_start() -> None:
|
|
|
73
82
|
return
|
|
74
83
|
|
|
75
84
|
try:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
egress = await registry.settings_egress()
|
|
86
|
+
async with bing_azure_transport(egress.route) as azure_transport:
|
|
87
|
+
async with get_credentials(transport=azure_transport) as credential:
|
|
88
|
+
async with get_project_client(
|
|
89
|
+
credential,
|
|
90
|
+
endpoint=read_secret(bing_agent_credentials.endpoint),
|
|
91
|
+
transport=azure_transport,
|
|
92
|
+
) as project_client:
|
|
93
|
+
deleted = await cleanup_auto_provisioned_bing_agents(project_client)
|
|
82
94
|
_LOGGER.info(
|
|
83
95
|
"Bing agent startup cleanup finished; deleted %s auto-provisioned agent(s)",
|
|
84
96
|
deleted,
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
4
6
|
|
|
5
7
|
import certifi
|
|
6
8
|
import httpx
|
|
7
9
|
from azure.ai.projects.aio import AIProjectClient
|
|
8
10
|
from azure.core.credentials_async import AsyncTokenCredential
|
|
9
|
-
from azure.core.pipeline.transport import AsyncioRequestsTransport
|
|
11
|
+
from azure.core.pipeline.transport import AsyncHttpTransport, AsyncioRequestsTransport
|
|
10
12
|
from azure.identity.aio import DefaultAzureCredential, WorkloadIdentityCredential
|
|
11
13
|
from openai import AsyncOpenAI
|
|
14
|
+
from unique_search_proxy_core.http_client import EgressRoute
|
|
12
15
|
|
|
16
|
+
from unique_search_proxy_client.web.core.client.azure_transport import (
|
|
17
|
+
azure_transport_for_route,
|
|
18
|
+
)
|
|
13
19
|
from unique_search_proxy_client.web.settings.providers.bing_agent import (
|
|
14
20
|
bing_agent_credentials,
|
|
15
21
|
)
|
|
@@ -19,15 +25,32 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
19
25
|
_private_endpoint_http_client: httpx.AsyncClient | None = None
|
|
20
26
|
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
@asynccontextmanager
|
|
29
|
+
async def bing_azure_transport(
|
|
30
|
+
route: EgressRoute,
|
|
31
|
+
) -> AsyncIterator[AsyncHttpTransport | None]:
|
|
32
|
+
"""Yield proxied Azure transport, preserving explicit private endpoints."""
|
|
33
|
+
if bing_agent_credentials.use_private_endpoint_transport:
|
|
34
|
+
yield None
|
|
35
|
+
return
|
|
36
|
+
async with azure_transport_for_route(route) as transport:
|
|
37
|
+
yield transport
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_credentials(
|
|
41
|
+
*,
|
|
42
|
+
transport: AsyncHttpTransport | None = None,
|
|
43
|
+
) -> AsyncTokenCredential:
|
|
23
44
|
match bing_agent_credentials.azure_identity_credential_type:
|
|
24
45
|
case "workload":
|
|
25
46
|
if bing_agent_credentials.use_private_endpoint_transport:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
private_transport = AsyncioRequestsTransport(
|
|
48
|
+
connection_verify=certifi.where()
|
|
49
|
+
)
|
|
50
|
+
return WorkloadIdentityCredential(transport=private_transport)
|
|
51
|
+
return WorkloadIdentityCredential(transport=transport)
|
|
29
52
|
case "default":
|
|
30
|
-
return DefaultAzureCredential()
|
|
53
|
+
return DefaultAzureCredential(transport=transport)
|
|
31
54
|
case other:
|
|
32
55
|
msg = f"Invalid Azure identity credential type: {other}"
|
|
33
56
|
raise ValueError(msg)
|
|
@@ -37,6 +60,7 @@ def get_project_client(
|
|
|
37
60
|
credential: AsyncTokenCredential,
|
|
38
61
|
*,
|
|
39
62
|
endpoint: str | None = None,
|
|
63
|
+
transport: AsyncHttpTransport | None = None,
|
|
40
64
|
) -> AIProjectClient:
|
|
41
65
|
resolved_endpoint = endpoint or read_secret(bing_agent_credentials.endpoint)
|
|
42
66
|
if not resolved_endpoint or resolved_endpoint == NOT_PROVIDED:
|
|
@@ -53,6 +77,7 @@ def get_project_client(
|
|
|
53
77
|
return AIProjectClient(
|
|
54
78
|
credential=credential,
|
|
55
79
|
endpoint=resolved_endpoint,
|
|
80
|
+
transport=transport,
|
|
56
81
|
)
|
|
57
82
|
|
|
58
83
|
|
|
@@ -75,7 +100,11 @@ async def aclose_private_endpoint_http_client() -> None:
|
|
|
75
100
|
_private_endpoint_http_client = None
|
|
76
101
|
|
|
77
102
|
|
|
78
|
-
def get_openai_client(
|
|
103
|
+
def get_openai_client(
|
|
104
|
+
project_client: AIProjectClient,
|
|
105
|
+
*,
|
|
106
|
+
http_client: httpx.AsyncClient | None = None,
|
|
107
|
+
) -> AsyncOpenAI:
|
|
79
108
|
"""Return an authenticated AsyncOpenAI client from the Foundry project client.
|
|
80
109
|
|
|
81
110
|
When private-endpoint transport is enabled, reuse a shared certifi-backed
|
|
@@ -86,6 +115,8 @@ def get_openai_client(project_client: AIProjectClient) -> AsyncOpenAI:
|
|
|
86
115
|
return project_client.get_openai_client(
|
|
87
116
|
http_client=_get_private_endpoint_http_client(),
|
|
88
117
|
)
|
|
118
|
+
if http_client is not None:
|
|
119
|
+
return project_client.get_openai_client(http_client=http_client)
|
|
89
120
|
return project_client.get_openai_client()
|
|
90
121
|
|
|
91
122
|
|
|
@@ -12,6 +12,7 @@ from azure.ai.projects.models import (
|
|
|
12
12
|
BingGroundingTool,
|
|
13
13
|
PromptAgentDefinition,
|
|
14
14
|
)
|
|
15
|
+
from httpx import AsyncClient
|
|
15
16
|
from openai.types.responses import ResponseStreamEvent
|
|
16
17
|
from openai.types.responses.response_completed_event import ResponseCompletedEvent
|
|
17
18
|
from openai.types.responses.response_output_item_done_event import (
|
|
@@ -140,10 +141,12 @@ def _is_missing_agent_error(exc: BaseException, *, agent_name: str) -> bool:
|
|
|
140
141
|
async def stream_bing_grounding_agent(
|
|
141
142
|
project_client: AIProjectClient,
|
|
142
143
|
*,
|
|
144
|
+
http_client: AsyncClient | None = None,
|
|
143
145
|
query: str,
|
|
144
146
|
model: str,
|
|
145
147
|
instructions: str,
|
|
146
148
|
grounding: BingGroundingConfiguration,
|
|
149
|
+
timeout: float,
|
|
147
150
|
) -> AsyncIterator[tuple[str, dict]]:
|
|
148
151
|
"""Stream Bing-grounded Responses events as ``(delta_text, raw_event)`` pairs.
|
|
149
152
|
|
|
@@ -158,13 +161,17 @@ async def stream_bing_grounding_agent(
|
|
|
158
161
|
instructions=instructions,
|
|
159
162
|
grounding=grounding,
|
|
160
163
|
)
|
|
161
|
-
openai_client = get_openai_client(
|
|
164
|
+
openai_client = get_openai_client(
|
|
165
|
+
project_client,
|
|
166
|
+
http_client=http_client,
|
|
167
|
+
)
|
|
162
168
|
|
|
163
169
|
try:
|
|
164
170
|
stream = await _create_responses_stream(
|
|
165
171
|
openai_client,
|
|
166
172
|
agent_name=resolved_name,
|
|
167
173
|
query=query,
|
|
174
|
+
timeout=timeout,
|
|
168
175
|
)
|
|
169
176
|
except Exception as exc:
|
|
170
177
|
if not _is_missing_agent_error(exc, agent_name=resolved_name):
|
|
@@ -185,6 +192,7 @@ async def stream_bing_grounding_agent(
|
|
|
185
192
|
openai_client,
|
|
186
193
|
agent_name=resolved_name,
|
|
187
194
|
query=query,
|
|
195
|
+
timeout=timeout,
|
|
188
196
|
)
|
|
189
197
|
|
|
190
198
|
emitted_text = False
|
|
@@ -225,6 +233,7 @@ async def _create_responses_stream(
|
|
|
225
233
|
*,
|
|
226
234
|
agent_name: str,
|
|
227
235
|
query: str,
|
|
236
|
+
timeout: float,
|
|
228
237
|
) -> openai.AsyncStream[ResponseStreamEvent]:
|
|
229
238
|
started = time.perf_counter()
|
|
230
239
|
stream = await openai_client.responses.create(
|
|
@@ -236,6 +245,9 @@ async def _create_responses_stream(
|
|
|
236
245
|
"type": "agent_reference",
|
|
237
246
|
}
|
|
238
247
|
},
|
|
248
|
+
# The shared egress client defaults to 30s, while Bing requests allow
|
|
249
|
+
# longer runs. The API's outer deadline still caps total request time.
|
|
250
|
+
timeout=timeout,
|
|
239
251
|
)
|
|
240
252
|
elapsed_ms = (time.perf_counter() - started) * 1000
|
|
241
253
|
_LOGGER.info(
|
unique_search_proxy-2026.44.0.dev1/unique_search_proxy_client/web/core/agent_engines/bing/service.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import openai
|
|
9
|
+
from unique_search_proxy_core.agent_engines.base import AgentEngineType
|
|
10
|
+
from unique_search_proxy_core.agent_engines.bing.grounding import (
|
|
11
|
+
BingGroundingConfiguration,
|
|
12
|
+
)
|
|
13
|
+
from unique_search_proxy_core.agent_engines.bing.schema import BingAgentSearchRequest
|
|
14
|
+
from unique_search_proxy_core.agent_engines.bing.settings import resolve_market
|
|
15
|
+
from unique_search_proxy_core.agent_engines.resolve import (
|
|
16
|
+
resolve_output_schema_for_engine,
|
|
17
|
+
)
|
|
18
|
+
from unique_search_proxy_core.errors import UpstreamError
|
|
19
|
+
from unique_search_proxy_core.schema import (
|
|
20
|
+
AgentSearchDelta,
|
|
21
|
+
AgentSearchDone,
|
|
22
|
+
AgentSearchResponse,
|
|
23
|
+
AgentSearchStreamEvent,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from unique_search_proxy_client.web.core.agent_engines.bing.client import (
|
|
27
|
+
bing_azure_transport,
|
|
28
|
+
get_credentials,
|
|
29
|
+
get_project_client,
|
|
30
|
+
)
|
|
31
|
+
from unique_search_proxy_client.web.core.agent_engines.bing.runner import (
|
|
32
|
+
stream_bing_grounding_agent,
|
|
33
|
+
)
|
|
34
|
+
from unique_search_proxy_client.web.core.agent_engines.service_base import (
|
|
35
|
+
AgentSearchEngineService,
|
|
36
|
+
)
|
|
37
|
+
from unique_search_proxy_client.web.core.agent_engines.structured_output import (
|
|
38
|
+
build_agent_instructions,
|
|
39
|
+
)
|
|
40
|
+
from unique_search_proxy_client.web.core.provider_response import transport_error_raw
|
|
41
|
+
from unique_search_proxy_client.web.settings.providers.bing_agent import (
|
|
42
|
+
bing_agent_credentials,
|
|
43
|
+
)
|
|
44
|
+
from unique_search_proxy_client.web.settings.secret_str import read_secret
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from httpx import AsyncClient
|
|
48
|
+
from unique_search_proxy_core.http_client import EgressRoute
|
|
49
|
+
|
|
50
|
+
_LOGGER = logging.getLogger(__name__)
|
|
51
|
+
# Foundry can report transient backend failures inside an HTTP 200 SSE stream,
|
|
52
|
+
# beyond the reach of the OpenAI client's HTTP retry policy.
|
|
53
|
+
_RETRYABLE_STREAM_ERROR_CODES = frozenset({"server_error", "internal_error"})
|
|
54
|
+
_RETRYABLE_STREAM_ERROR_MESSAGES = (
|
|
55
|
+
"model deployment encountered an error processing your request",
|
|
56
|
+
"invalid or expired token",
|
|
57
|
+
)
|
|
58
|
+
_STREAM_RETRY_DELAYS_SECONDS = (2.0, 8.0)
|
|
59
|
+
_STREAM_MAX_ATTEMPTS = len(_STREAM_RETRY_DELAYS_SECONDS) + 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_retryable_bing_stream_error(exc: BaseException) -> bool:
|
|
63
|
+
"""Return whether a pre-output Foundry stream failure is safe to retry."""
|
|
64
|
+
if isinstance(exc, openai.APIStatusError):
|
|
65
|
+
# The OpenAI client already retries retryable HTTP statuses. This
|
|
66
|
+
# classifier is only for errors embedded in an HTTP 200 SSE stream.
|
|
67
|
+
return False
|
|
68
|
+
if not isinstance(exc, openai.APIError):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
code = exc.code.lower() if isinstance(exc.code, str) else None
|
|
72
|
+
if code in _RETRYABLE_STREAM_ERROR_CODES:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
message = str(exc).lower()
|
|
76
|
+
return any(marker in message for marker in _RETRYABLE_STREAM_ERROR_MESSAGES)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BingAgentSearchService(AgentSearchEngineService[BingAgentSearchRequest]):
|
|
80
|
+
engine_id = AgentEngineType.BING.value
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
http_client: AsyncClient,
|
|
86
|
+
egress_route: EgressRoute,
|
|
87
|
+
) -> None:
|
|
88
|
+
super().__init__(http_client=http_client)
|
|
89
|
+
self._egress_route = egress_route
|
|
90
|
+
|
|
91
|
+
async def search(self, request: BingAgentSearchRequest) -> AgentSearchResponse: # type: ignore[override]
|
|
92
|
+
response: AgentSearchResponse | None = None
|
|
93
|
+
async for event in self.stream(request):
|
|
94
|
+
if isinstance(event, AgentSearchDone):
|
|
95
|
+
response = event.response
|
|
96
|
+
if response is None:
|
|
97
|
+
msg = "Bing agent search stream ended without a done event"
|
|
98
|
+
raise UpstreamError(msg)
|
|
99
|
+
return response
|
|
100
|
+
|
|
101
|
+
async def stream(
|
|
102
|
+
self,
|
|
103
|
+
request: BingAgentSearchRequest, # type: ignore[override]
|
|
104
|
+
) -> AsyncIterator[AgentSearchStreamEvent]:
|
|
105
|
+
bing_agent_credentials.check_credentials()
|
|
106
|
+
creds = bing_agent_credentials
|
|
107
|
+
http_client = self._http_client
|
|
108
|
+
egress_route = self._egress_route
|
|
109
|
+
if http_client is None:
|
|
110
|
+
raise RuntimeError("HTTP client is required for Bing")
|
|
111
|
+
|
|
112
|
+
answer_parts: list[str] = []
|
|
113
|
+
raw_chunks: list[dict] = []
|
|
114
|
+
|
|
115
|
+
output_schema = resolve_output_schema_for_engine(request.engine)
|
|
116
|
+
instructions = build_agent_instructions(
|
|
117
|
+
generation_instructions=request.generation_instructions,
|
|
118
|
+
output_schema=output_schema,
|
|
119
|
+
)
|
|
120
|
+
grounding = BingGroundingConfiguration(
|
|
121
|
+
fetch_size=request.fetch_size,
|
|
122
|
+
market=resolve_market(request.market),
|
|
123
|
+
freshness=request.freshness,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
for attempt in range(1, _STREAM_MAX_ATTEMPTS + 1):
|
|
127
|
+
try:
|
|
128
|
+
async with bing_azure_transport(egress_route) as azure_transport:
|
|
129
|
+
async with get_credentials(transport=azure_transport) as credential:
|
|
130
|
+
async with get_project_client(
|
|
131
|
+
credential,
|
|
132
|
+
endpoint=read_secret(creds.endpoint),
|
|
133
|
+
transport=azure_transport,
|
|
134
|
+
) as project_client:
|
|
135
|
+
async for (
|
|
136
|
+
delta,
|
|
137
|
+
raw_event,
|
|
138
|
+
) in stream_bing_grounding_agent(
|
|
139
|
+
project_client,
|
|
140
|
+
http_client=http_client,
|
|
141
|
+
query=request.query,
|
|
142
|
+
model=read_secret(creds.bing_agent_model),
|
|
143
|
+
instructions=instructions,
|
|
144
|
+
grounding=grounding,
|
|
145
|
+
timeout=request.timeout,
|
|
146
|
+
):
|
|
147
|
+
if delta:
|
|
148
|
+
answer_parts.append(delta)
|
|
149
|
+
yield AgentSearchDelta(text=delta)
|
|
150
|
+
if raw_event:
|
|
151
|
+
raw_chunks.append(raw_event)
|
|
152
|
+
break
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
retryable = (
|
|
155
|
+
attempt < _STREAM_MAX_ATTEMPTS
|
|
156
|
+
and not answer_parts
|
|
157
|
+
and _is_retryable_bing_stream_error(exc)
|
|
158
|
+
)
|
|
159
|
+
if not retryable:
|
|
160
|
+
raise UpstreamError(
|
|
161
|
+
f"Bing agent search failed: {exc}",
|
|
162
|
+
upstream_raw=transport_error_raw(exc),
|
|
163
|
+
) from exc
|
|
164
|
+
|
|
165
|
+
raw_chunks.clear()
|
|
166
|
+
retry_delay = _STREAM_RETRY_DELAYS_SECONDS[attempt - 1]
|
|
167
|
+
_LOGGER.warning(
|
|
168
|
+
"Bing response failed before output; retrying attempt %d/%d "
|
|
169
|
+
"after %.1fs (type=%s, code=%s)",
|
|
170
|
+
attempt + 1,
|
|
171
|
+
_STREAM_MAX_ATTEMPTS,
|
|
172
|
+
retry_delay,
|
|
173
|
+
type(exc).__name__,
|
|
174
|
+
getattr(exc, "code", None),
|
|
175
|
+
)
|
|
176
|
+
await asyncio.sleep(retry_delay)
|
|
177
|
+
|
|
178
|
+
answer = "".join(answer_parts)
|
|
179
|
+
response = AgentSearchResponse(
|
|
180
|
+
engine=AgentEngineType.BING.value,
|
|
181
|
+
query=request.query,
|
|
182
|
+
answer=answer,
|
|
183
|
+
raw=raw_chunks if len(raw_chunks) != 1 else raw_chunks[0],
|
|
184
|
+
)
|
|
185
|
+
yield AgentSearchDone(response=response)
|
|
@@ -17,17 +17,22 @@ from unique_search_proxy_client.web.core.agent_engines.vertexai.service import (
|
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from httpx import AsyncClient
|
|
20
|
+
from unique_search_proxy_core.http_client import EgressRoute
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def get_agent_engine_service(
|
|
23
24
|
engine: AgentEngineType,
|
|
24
25
|
*,
|
|
25
|
-
http_client: AsyncClient
|
|
26
|
+
http_client: AsyncClient,
|
|
27
|
+
egress_route: EgressRoute,
|
|
26
28
|
) -> AgentSearchEngine[Any]:
|
|
27
29
|
"""Instantiate an agent search engine by registered id."""
|
|
28
30
|
match engine:
|
|
29
31
|
case AgentEngineType.BING:
|
|
30
|
-
return BingAgentSearchService(
|
|
32
|
+
return BingAgentSearchService(
|
|
33
|
+
http_client=http_client,
|
|
34
|
+
egress_route=egress_route,
|
|
35
|
+
)
|
|
31
36
|
case AgentEngineType.VERTEXAI:
|
|
32
37
|
return VertexAIAgentSearchService(http_client=http_client)
|
|
33
38
|
case _:
|
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Generic
|
|
4
4
|
|
|
5
5
|
from unique_search_proxy_core.agent_engines.base import (
|
|
6
6
|
AgentRequestT,
|
|
7
7
|
AgentSearchEngine,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from httpx import AsyncClient
|
|
12
|
-
|
|
13
10
|
|
|
14
11
|
class AgentSearchEngineService(
|
|
15
12
|
AgentSearchEngine[AgentRequestT], Generic[AgentRequestT]
|
|
16
13
|
):
|
|
17
14
|
"""Client-side agent search engine base (HTTP client wiring)."""
|
|
18
15
|
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
*,
|
|
22
|
-
http_client: AsyncClient | None = None,
|
|
23
|
-
) -> None:
|
|
24
|
-
super().__init__(http_client=http_client)
|
|
25
|
-
|
|
26
16
|
@property
|
|
27
17
|
def mode(self) -> str:
|
|
28
18
|
return "agent"
|
|
@@ -7,6 +7,8 @@ from base64 import b64decode
|
|
|
7
7
|
from google.auth import load_credentials_from_dict
|
|
8
8
|
from google.genai._api_client import BaseApiClient
|
|
9
9
|
from google.genai.client import AsyncClient
|
|
10
|
+
from google.genai.types import HttpOptions
|
|
11
|
+
from httpx import AsyncClient as HttpxAsyncClient
|
|
10
12
|
|
|
11
13
|
from unique_search_proxy_client.web.settings.providers.vertexai_agent import (
|
|
12
14
|
vertexai_agent_credentials,
|
|
@@ -19,7 +21,9 @@ from unique_search_proxy_client.web.settings.secret_str import (
|
|
|
19
21
|
_LOGGER = logging.getLogger(__name__)
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def _get_base_api_client_from_service_account(
|
|
24
|
+
def _get_base_api_client_from_service_account(
|
|
25
|
+
http_client: HttpxAsyncClient | None = None,
|
|
26
|
+
) -> BaseApiClient:
|
|
23
27
|
if not is_secret_configured(vertexai_agent_credentials.service_account_credentials):
|
|
24
28
|
msg = (
|
|
25
29
|
"VERTEXAI_AGENT_CREDENTIAL_TYPE is 'service_account' but "
|
|
@@ -43,20 +47,28 @@ def _get_base_api_client_from_service_account() -> BaseApiClient:
|
|
|
43
47
|
vertexai=True,
|
|
44
48
|
credentials=credentials,
|
|
45
49
|
project=project_id,
|
|
50
|
+
http_options=HttpOptions(httpx_async_client=http_client),
|
|
46
51
|
)
|
|
47
52
|
|
|
48
53
|
|
|
49
|
-
def _get_base_api_client_from_adc(
|
|
50
|
-
|
|
54
|
+
def _get_base_api_client_from_adc(
|
|
55
|
+
http_client: HttpxAsyncClient | None = None,
|
|
56
|
+
) -> BaseApiClient:
|
|
57
|
+
return BaseApiClient(
|
|
58
|
+
vertexai=True,
|
|
59
|
+
http_options=HttpOptions(httpx_async_client=http_client),
|
|
60
|
+
)
|
|
51
61
|
|
|
52
62
|
|
|
53
|
-
def get_vertex_client(
|
|
63
|
+
def get_vertex_client(
|
|
64
|
+
http_client: HttpxAsyncClient | None = None,
|
|
65
|
+
) -> AsyncClient:
|
|
54
66
|
if vertexai_agent_credentials.credential_type == "service_account":
|
|
55
67
|
_LOGGER.info("Using explicit service account credentials for VertexAI agent")
|
|
56
|
-
base_api_client = _get_base_api_client_from_service_account()
|
|
68
|
+
base_api_client = _get_base_api_client_from_service_account(http_client)
|
|
57
69
|
else:
|
|
58
70
|
_LOGGER.info("Using workload identity (ADC) for VertexAI agent")
|
|
59
|
-
base_api_client = _get_base_api_client_from_adc()
|
|
71
|
+
base_api_client = _get_base_api_client_from_adc(http_client)
|
|
60
72
|
return AsyncClient(api_client=base_api_client)
|
|
61
73
|
|
|
62
74
|
|
|
@@ -42,7 +42,7 @@ class VertexAIAgentSearchService(
|
|
|
42
42
|
|
|
43
43
|
def __init__(self, **kwargs: Any) -> None:
|
|
44
44
|
super().__init__(**kwargs)
|
|
45
|
-
self._client = get_vertex_client()
|
|
45
|
+
self._client = get_vertex_client(self._http_client)
|
|
46
46
|
|
|
47
47
|
async def search(self, request: VertexAIAgentSearchRequest) -> AgentSearchResponse: # type: ignore[override]
|
|
48
48
|
response: AgentSearchResponse | None = None
|