unique-search-proxy 2026.44.0.dev12__tar.gz → 2026.44.0.dev14__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.dev12 → unique_search_proxy-2026.44.0.dev14}/PKG-INFO +7 -7
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/README.md +4 -4
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/pyproject.toml +3 -3
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/app.py +4 -0
- unique_search_proxy-2026.44.0.dev14/unique_search_proxy_client/web/core/agent_engines/bing/citation_instructions.j2 +18 -0
- unique_search_proxy-2026.44.0.dev14/unique_search_proxy_client/web/core/agent_engines/bing/instructions.py +26 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +121 -37
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +23 -20
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/generator/schema.py +10 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/generator/template.py +66 -7
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/metadata.py +9 -1
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/app.py +1 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/client.py +2 -2
- unique_search_proxy-2026.44.0.dev12/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -31
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/health.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/v1/crawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/api/v1/search.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/bing/cleanup.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/vertexai/redirects.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/client/azure_transport.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/client/credentials.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/client/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawl_limiter.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/provider_response.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/providers.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/custom_api/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/custom_api/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/error_handlers.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/registry.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/logging_config.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/middleware/context.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/monitoring/metrics.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/presets/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/presets/common.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/presets/crawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/presets/search.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/presets/types.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/request_limits.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/startup_report.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/utils/__init__.py +0 -0
- {unique_search_proxy-2026.44.0.dev12 → unique_search_proxy-2026.44.0.dev14}/unique_search_proxy_client/web/utils/url.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-search-proxy
|
|
3
|
-
Version: 2026.44.0.
|
|
3
|
+
Version: 2026.44.0.dev14
|
|
4
4
|
Summary: Web Search Proxy implementation
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
@@ -20,8 +20,8 @@ Requires-Dist: certifi>=2025.11.12,<2027
|
|
|
20
20
|
Requires-Dist: google-genai>=1.73.0,<2
|
|
21
21
|
Requires-Dist: google-auth>=2.43.0,<3
|
|
22
22
|
Requires-Dist: jinja2>=3.1.6,<4
|
|
23
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.
|
|
24
|
-
Requires-Dist: unique-search-proxy-core>=2026.44.0.
|
|
23
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0
|
|
24
|
+
Requires-Dist: unique-search-proxy-core>=2026.44.0.dev10,<2026.44.0rc0
|
|
25
25
|
Requires-Dist: openai>=1.105.0,<3
|
|
26
26
|
Requires-Python: >=3.12
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
@@ -116,11 +116,11 @@ Built-in providers register in `register_builtin_providers()` during `create_app
|
|
|
116
116
|
| `GET /ready` | Readiness (httpx pool + registered providers) |
|
|
117
117
|
| `GET /v1/configuration/providers` | Registered search engine, agent engine, and crawler ids |
|
|
118
118
|
| `POST /v1/search` | Standard search (flat body: `engine`, `query`, provider params, `timeout`) |
|
|
119
|
-
| `POST /v1/agent-search` | Grounded agent search — opaque `answer` + `raw` |
|
|
119
|
+
| `POST /v1/agent-search` | Grounded agent search — opaque `answer` + provider `citations` + `raw` |
|
|
120
120
|
| `POST /v1/agent-search/stream` | Same, streamed as SSE (`delta` + `done`) |
|
|
121
121
|
| `POST /v1/crawl` | Crawl URLs (flat body: `crawler`, `urls`, `timeout`, …) |
|
|
122
122
|
| `GET /metrics` | Prometheus (when enabled) |
|
|
123
|
-
| `/docs` | Swagger UI with preset examples |
|
|
123
|
+
| `/docs` | Swagger UI with preset examples (only when `EXPOSE_DOCS=true`) |
|
|
124
124
|
|
|
125
125
|
OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
|
|
126
126
|
|
|
@@ -191,7 +191,7 @@ Each result requires `url`, `title`, and `snippet`; `content` is optional.
|
|
|
191
191
|
|
|
192
192
|
### 5.3 Agent search (`POST /v1/agent-search`)
|
|
193
193
|
|
|
194
|
-
Thin egress — proxy returns opaque agent text; callers own parsing
|
|
194
|
+
Thin egress — proxy returns opaque agent text and the citations the provider attached to it (Bing: Foundry `url_citation` annotations, with offsets into the answer); callers own parsing:
|
|
195
195
|
|
|
196
196
|
```json
|
|
197
197
|
{
|
|
@@ -310,7 +310,7 @@ uv run python scripts/generate_sdk.py
|
|
|
310
310
|
## 8. Dev testing
|
|
311
311
|
|
|
312
312
|
1. Start the server and configure `.env` (`.env.example` sets `REQUIRE_CONTEXT_HEADERS=false` for local use; production keeps enforcement enabled).
|
|
313
|
-
2. **Swagger** — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
313
|
+
2. **Swagger** (if `EXPOSE_DOCS=true`) — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
314
314
|
3. **CLI** — same presets from the terminal (`scripts/try_presets.py` sends the same local context headers):
|
|
315
315
|
|
|
316
316
|
```bash
|
|
@@ -88,11 +88,11 @@ Built-in providers register in `register_builtin_providers()` during `create_app
|
|
|
88
88
|
| `GET /ready` | Readiness (httpx pool + registered providers) |
|
|
89
89
|
| `GET /v1/configuration/providers` | Registered search engine, agent engine, and crawler ids |
|
|
90
90
|
| `POST /v1/search` | Standard search (flat body: `engine`, `query`, provider params, `timeout`) |
|
|
91
|
-
| `POST /v1/agent-search` | Grounded agent search — opaque `answer` + `raw` |
|
|
91
|
+
| `POST /v1/agent-search` | Grounded agent search — opaque `answer` + provider `citations` + `raw` |
|
|
92
92
|
| `POST /v1/agent-search/stream` | Same, streamed as SSE (`delta` + `done`) |
|
|
93
93
|
| `POST /v1/crawl` | Crawl URLs (flat body: `crawler`, `urls`, `timeout`, …) |
|
|
94
94
|
| `GET /metrics` | Prometheus (when enabled) |
|
|
95
|
-
| `/docs` | Swagger UI with preset examples |
|
|
95
|
+
| `/docs` | Swagger UI with preset examples (only when `EXPOSE_DOCS=true`) |
|
|
96
96
|
|
|
97
97
|
OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
|
|
98
98
|
|
|
@@ -163,7 +163,7 @@ Each result requires `url`, `title`, and `snippet`; `content` is optional.
|
|
|
163
163
|
|
|
164
164
|
### 5.3 Agent search (`POST /v1/agent-search`)
|
|
165
165
|
|
|
166
|
-
Thin egress — proxy returns opaque agent text; callers own parsing
|
|
166
|
+
Thin egress — proxy returns opaque agent text and the citations the provider attached to it (Bing: Foundry `url_citation` annotations, with offsets into the answer); callers own parsing:
|
|
167
167
|
|
|
168
168
|
```json
|
|
169
169
|
{
|
|
@@ -282,7 +282,7 @@ uv run python scripts/generate_sdk.py
|
|
|
282
282
|
## 8. Dev testing
|
|
283
283
|
|
|
284
284
|
1. Start the server and configure `.env` (`.env.example` sets `REQUIRE_CONTEXT_HEADERS=false` for local use; production keeps enforcement enabled).
|
|
285
|
-
2. **Swagger** — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
285
|
+
2. **Swagger** (if `EXPOSE_DOCS=true`) — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset. Context headers default to `local` in the UI.
|
|
286
286
|
3. **CLI** — same presets from the terminal (`scripts/try_presets.py` sends the same local context headers):
|
|
287
287
|
|
|
288
288
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy"
|
|
3
|
-
version = "2026.44.0.
|
|
3
|
+
version = "2026.44.0.dev14"
|
|
4
4
|
description = "Web Search Proxy implementation"
|
|
5
5
|
authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -22,8 +22,8 @@ dependencies = [
|
|
|
22
22
|
"google-genai>=1.73.0,<2",
|
|
23
23
|
"google-auth>=2.43.0,<3",
|
|
24
24
|
"jinja2>=3.1.6,<4",
|
|
25
|
-
"unique-toolkit[monitoring]>=2026.44.0.
|
|
26
|
-
"unique-search-proxy-core>=2026.44.0.
|
|
25
|
+
"unique-toolkit[monitoring]>=2026.44.0.dev10,<2026.44.0rc0",
|
|
26
|
+
"unique-search-proxy-core>=2026.44.0.dev10,<2026.44.0rc0",
|
|
27
27
|
"openai>=1.105.0,<3",
|
|
28
28
|
]
|
|
29
29
|
|
|
@@ -28,6 +28,7 @@ from unique_search_proxy_client.web.logging_config import (
|
|
|
28
28
|
)
|
|
29
29
|
from unique_search_proxy_client.web.middleware.context import RequestContextMiddleware
|
|
30
30
|
from unique_search_proxy_client.web.monitoring import setup_prometheus
|
|
31
|
+
from unique_search_proxy_client.web.settings.app import app_settings
|
|
31
32
|
from unique_search_proxy_client.web.settings.request_limits import (
|
|
32
33
|
request_limits_settings,
|
|
33
34
|
)
|
|
@@ -92,6 +93,9 @@ def create_app() -> FastAPI:
|
|
|
92
93
|
),
|
|
93
94
|
version="0.2.0",
|
|
94
95
|
lifespan=lifespan,
|
|
96
|
+
docs_url="/docs" if app_settings.expose_docs else None,
|
|
97
|
+
redoc_url="/redoc" if app_settings.expose_docs else None,
|
|
98
|
+
openapi_url="/openapi.json" if app_settings.expose_docs else None,
|
|
95
99
|
swagger_ui_parameters={
|
|
96
100
|
"defaultModelsExpandDepth": 1,
|
|
97
101
|
"defaultModelExpandDepth": 2,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{{ generation_instructions }}
|
|
2
|
+
|
|
3
|
+
## Source-isolated evidence list
|
|
4
|
+
Use Bing Grounding to select distinct useful sources.
|
|
5
|
+
The complete response must consist only of lines in this form:
|
|
6
|
+
- <a paragraph of facts supported only by one source> <that source's citation>
|
|
7
|
+
|
|
8
|
+
Each line is a paragraph, not a sentence. Write several sentences per line that report every relevant fact, figure, date, name and quote from that source, with as much detail as the source offers, often 100 to 200 words. Lines can be long; do not shorten them.
|
|
9
|
+
|
|
10
|
+
Hard requirements:
|
|
11
|
+
1. Exactly one line per distinct source and exactly one distinct source per line.
|
|
12
|
+
2. Every line begins with `- ` and contains exactly one citation marker as its final content.
|
|
13
|
+
3. Never synthesize, compare, or combine facts across sources within a line.
|
|
14
|
+
4. Put all useful facts from a source into that source's one line; never cite it again.
|
|
15
|
+
5. No headings, preface, conclusion, line breaks inside a paragraph, nested bullets, JSON, or uncited claims.
|
|
16
|
+
6. Never invent, reconstruct, type, or guess citation markers or source URLs.
|
|
17
|
+
|
|
18
|
+
Before responding, silently verify that the number of lines equals the number of distinct cited sources.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
from jinja2 import StrictUndefined, Template
|
|
6
|
+
|
|
7
|
+
# Describes the output format we want from the agent. Models do not always
|
|
8
|
+
# follow it, so downstream parsing should tolerate deviations and keep what it
|
|
9
|
+
# can rather than reject the whole answer.
|
|
10
|
+
_CITATION_INSTRUCTIONS = Template(
|
|
11
|
+
files(__package__)
|
|
12
|
+
.joinpath("citation_instructions.j2")
|
|
13
|
+
.read_text(encoding="utf-8")
|
|
14
|
+
.strip(),
|
|
15
|
+
undefined=StrictUndefined,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_bing_citation_instructions(generation_instructions: str) -> str:
|
|
20
|
+
"""Add citation-safe output rules without suppressing Foundry annotations."""
|
|
21
|
+
return _CITATION_INSTRUCTIONS.render(
|
|
22
|
+
generation_instructions=generation_instructions.rstrip()
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["build_bing_citation_instructions"]
|
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
|
-
from collections.abc import AsyncIterator
|
|
5
|
+
from collections.abc import AsyncIterator, Sequence
|
|
6
|
+
from typing import NamedTuple
|
|
6
7
|
|
|
7
8
|
import openai
|
|
8
9
|
from azure.ai.projects.aio import AIProjectClient
|
|
@@ -14,10 +15,8 @@ from azure.ai.projects.models import (
|
|
|
14
15
|
)
|
|
15
16
|
from httpx import AsyncClient
|
|
16
17
|
from openai.types.responses import ResponseStreamEvent
|
|
18
|
+
from openai.types.responses.response import Response
|
|
17
19
|
from openai.types.responses.response_completed_event import ResponseCompletedEvent
|
|
18
|
-
from openai.types.responses.response_output_item_done_event import (
|
|
19
|
-
ResponseOutputItemDoneEvent,
|
|
20
|
-
)
|
|
21
20
|
from openai.types.responses.response_output_message import ResponseOutputMessage
|
|
22
21
|
from openai.types.responses.response_output_text import (
|
|
23
22
|
AnnotationURLCitation,
|
|
@@ -28,6 +27,7 @@ from unique_search_proxy_core.agent_engines.bing.grounding import (
|
|
|
28
27
|
BingGroundingConfiguration,
|
|
29
28
|
bing_agent_name,
|
|
30
29
|
)
|
|
30
|
+
from unique_search_proxy_core.schema import AnswerSpanCitation
|
|
31
31
|
|
|
32
32
|
from unique_search_proxy_client.web.core.agent_engines.bing.client import (
|
|
33
33
|
get_openai_client,
|
|
@@ -38,6 +38,7 @@ from unique_search_proxy_client.web.settings.secret_str import NOT_PROVIDED, rea
|
|
|
38
38
|
_LOGGER = logging.getLogger(__name__)
|
|
39
39
|
|
|
40
40
|
__all__ = [
|
|
41
|
+
"BingStreamChunk",
|
|
41
42
|
"create_bing_agent",
|
|
42
43
|
"get_bing_grounding_tool",
|
|
43
44
|
"resolve_bing_agent_name",
|
|
@@ -45,6 +46,19 @@ __all__ = [
|
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
class BingStreamChunk(NamedTuple):
|
|
50
|
+
"""One step of a Bing Responses stream.
|
|
51
|
+
|
|
52
|
+
``answer`` and ``citations`` are set only for the completed response, and
|
|
53
|
+
the citation offsets point into ``answer``.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
delta: str
|
|
57
|
+
raw_event: dict
|
|
58
|
+
answer: str | None = None
|
|
59
|
+
citations: Sequence[AnswerSpanCitation] = ()
|
|
60
|
+
|
|
61
|
+
|
|
48
62
|
def resolve_bing_agent_name(
|
|
49
63
|
*,
|
|
50
64
|
model: str,
|
|
@@ -147,8 +161,8 @@ async def stream_bing_grounding_agent(
|
|
|
147
161
|
instructions: str,
|
|
148
162
|
grounding: BingGroundingConfiguration,
|
|
149
163
|
timeout: float,
|
|
150
|
-
) -> AsyncIterator[
|
|
151
|
-
"""Stream Bing-grounded Responses events as
|
|
164
|
+
) -> AsyncIterator[BingStreamChunk]:
|
|
165
|
+
"""Stream Bing-grounded Responses events as :class:`BingStreamChunk` items.
|
|
152
166
|
|
|
153
167
|
Optimistically calls Responses with a hash-derived agent name. If the agent
|
|
154
168
|
is missing, creates it once and retries.
|
|
@@ -181,13 +195,23 @@ async def stream_bing_grounding_agent(
|
|
|
181
195
|
resolved_name,
|
|
182
196
|
exc,
|
|
183
197
|
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
try:
|
|
199
|
+
await create_bing_agent(
|
|
200
|
+
project_client,
|
|
201
|
+
agent_name=resolved_name,
|
|
202
|
+
model=model,
|
|
203
|
+
instructions=instructions,
|
|
204
|
+
grounding=grounding,
|
|
205
|
+
)
|
|
206
|
+
except UnicodeDecodeError:
|
|
207
|
+
# Foundry can persist the agent but return a compressed create response
|
|
208
|
+
# that azure-core fails to decode. The Responses retry below verifies
|
|
209
|
+
# whether creation actually succeeded.
|
|
210
|
+
_LOGGER.warning(
|
|
211
|
+
"Could not decode create response for Bing agent %s; "
|
|
212
|
+
"verifying creation through Responses",
|
|
213
|
+
resolved_name,
|
|
214
|
+
)
|
|
191
215
|
stream = await _create_responses_stream(
|
|
192
216
|
openai_client,
|
|
193
217
|
agent_name=resolved_name,
|
|
@@ -201,21 +225,13 @@ async def stream_bing_grounding_agent(
|
|
|
201
225
|
if isinstance(event, ResponseTextDeltaEvent):
|
|
202
226
|
if event.delta:
|
|
203
227
|
emitted_text = True
|
|
204
|
-
yield
|
|
205
|
-
|
|
206
|
-
citations = _extract_url_citations(event)
|
|
207
|
-
if citations:
|
|
208
|
-
yield (
|
|
209
|
-
"",
|
|
210
|
-
{
|
|
211
|
-
"type": event.type,
|
|
212
|
-
"citations": citations,
|
|
213
|
-
"event": event.model_dump(mode="json", warnings="none"),
|
|
214
|
-
},
|
|
228
|
+
yield BingStreamChunk(
|
|
229
|
+
event.delta, event.model_dump(mode="json", warnings="none")
|
|
215
230
|
)
|
|
216
231
|
elif isinstance(event, ResponseCompletedEvent):
|
|
217
232
|
response = event.response
|
|
218
233
|
output_text = response.output_text
|
|
234
|
+
citations = _extract_response_url_citations(response)
|
|
219
235
|
raw_completed = {
|
|
220
236
|
"type": event.type,
|
|
221
237
|
"response": response.model_dump(mode="json", warnings="none"),
|
|
@@ -223,9 +239,16 @@ async def stream_bing_grounding_agent(
|
|
|
223
239
|
# Foundry sometimes delivers the full answer only on completion.
|
|
224
240
|
if output_text and not emitted_text:
|
|
225
241
|
emitted_text = True
|
|
226
|
-
yield
|
|
242
|
+
yield BingStreamChunk(
|
|
243
|
+
output_text,
|
|
244
|
+
raw_completed,
|
|
245
|
+
answer=output_text,
|
|
246
|
+
citations=citations,
|
|
247
|
+
)
|
|
227
248
|
else:
|
|
228
|
-
yield
|
|
249
|
+
yield BingStreamChunk(
|
|
250
|
+
"", raw_completed, answer=output_text, citations=citations
|
|
251
|
+
)
|
|
229
252
|
|
|
230
253
|
|
|
231
254
|
async def _create_responses_stream(
|
|
@@ -258,15 +281,76 @@ async def _create_responses_stream(
|
|
|
258
281
|
return stream
|
|
259
282
|
|
|
260
283
|
|
|
261
|
-
def
|
|
262
|
-
item
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
284
|
+
def _extract_message_url_citations(
|
|
285
|
+
item: ResponseOutputMessage,
|
|
286
|
+
*,
|
|
287
|
+
start_index_offset: int = 0,
|
|
288
|
+
) -> list[AnswerSpanCitation]:
|
|
289
|
+
"""Turn the URL annotations on one output message into citations.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
item: A completed assistant message. Its text can be split across
|
|
293
|
+
several ``output_text`` parts, and each annotation's offsets are
|
|
294
|
+
relative to the part it is attached to.
|
|
295
|
+
start_index_offset: Length of the answer text before this message, so
|
|
296
|
+
the returned offsets point into the full answer.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
One citation per URL annotation, carrying the marker text the
|
|
300
|
+
annotation spans and its offsets into the full answer. Annotations
|
|
301
|
+
without a URL or with an empty span are skipped.
|
|
302
|
+
"""
|
|
303
|
+
if not item.content:
|
|
267
304
|
return []
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
305
|
+
|
|
306
|
+
citations: list[AnswerSpanCitation] = []
|
|
307
|
+
text_offset = start_index_offset
|
|
308
|
+
for text_content in item.content:
|
|
309
|
+
if not isinstance(text_content, ResponseOutputText):
|
|
310
|
+
continue
|
|
311
|
+
for annotation in text_content.annotations or []:
|
|
312
|
+
if not isinstance(annotation, AnnotationURLCitation):
|
|
313
|
+
continue
|
|
314
|
+
marker = text_content.text[annotation.start_index : annotation.end_index]
|
|
315
|
+
if annotation.url and marker:
|
|
316
|
+
citations.append(
|
|
317
|
+
AnswerSpanCitation(
|
|
318
|
+
url=annotation.url,
|
|
319
|
+
title=annotation.title,
|
|
320
|
+
marker=marker,
|
|
321
|
+
start_index=text_offset + annotation.start_index,
|
|
322
|
+
end_index=text_offset + annotation.end_index,
|
|
323
|
+
output_item_id=item.id,
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
text_offset += len(text_content.text)
|
|
327
|
+
return citations
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _extract_response_url_citations(
|
|
331
|
+
response: Response,
|
|
332
|
+
) -> list[AnswerSpanCitation]:
|
|
333
|
+
"""Extract citations with offsets into ``response.output_text``."""
|
|
334
|
+
citations: list[AnswerSpanCitation] = []
|
|
335
|
+
text_offset = 0
|
|
336
|
+
for item in response.output or []:
|
|
337
|
+
if not isinstance(item, ResponseOutputMessage):
|
|
338
|
+
continue
|
|
339
|
+
citations.extend(
|
|
340
|
+
_extract_message_url_citations(
|
|
341
|
+
item,
|
|
342
|
+
start_index_offset=text_offset,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
text_offset += sum(
|
|
346
|
+
len(content.text)
|
|
347
|
+
for content in item.content or []
|
|
348
|
+
if isinstance(content, ResponseOutputText)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
_LOGGER.info(
|
|
352
|
+
"Foundry Bing citation annotations response_id=%s citation_count=%d",
|
|
353
|
+
response.id,
|
|
354
|
+
len(citations),
|
|
355
|
+
)
|
|
356
|
+
return citations
|
|
@@ -12,15 +12,13 @@ from unique_search_proxy_core.agent_engines.bing.grounding import (
|
|
|
12
12
|
)
|
|
13
13
|
from unique_search_proxy_core.agent_engines.bing.schema import BingAgentSearchRequest
|
|
14
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
15
|
from unique_search_proxy_core.errors import UpstreamError
|
|
19
16
|
from unique_search_proxy_core.schema import (
|
|
20
17
|
AgentSearchDelta,
|
|
21
18
|
AgentSearchDone,
|
|
22
19
|
AgentSearchResponse,
|
|
23
20
|
AgentSearchStreamEvent,
|
|
21
|
+
AnswerSpanCitation,
|
|
24
22
|
)
|
|
25
23
|
|
|
26
24
|
from unique_search_proxy_client.web.core.agent_engines.bing.client import (
|
|
@@ -28,15 +26,15 @@ from unique_search_proxy_client.web.core.agent_engines.bing.client import (
|
|
|
28
26
|
get_credentials,
|
|
29
27
|
get_project_client,
|
|
30
28
|
)
|
|
29
|
+
from unique_search_proxy_client.web.core.agent_engines.bing.instructions import (
|
|
30
|
+
build_bing_citation_instructions,
|
|
31
|
+
)
|
|
31
32
|
from unique_search_proxy_client.web.core.agent_engines.bing.runner import (
|
|
32
33
|
stream_bing_grounding_agent,
|
|
33
34
|
)
|
|
34
35
|
from unique_search_proxy_client.web.core.agent_engines.service_base import (
|
|
35
36
|
AgentSearchEngineService,
|
|
36
37
|
)
|
|
37
|
-
from unique_search_proxy_client.web.core.agent_engines.structured_output import (
|
|
38
|
-
build_agent_instructions,
|
|
39
|
-
)
|
|
40
38
|
from unique_search_proxy_client.web.core.provider_response import transport_error_raw
|
|
41
39
|
from unique_search_proxy_client.web.settings.providers.bing_agent import (
|
|
42
40
|
bing_agent_credentials,
|
|
@@ -110,12 +108,12 @@ class BingAgentSearchService(AgentSearchEngineService[BingAgentSearchRequest]):
|
|
|
110
108
|
raise RuntimeError("HTTP client is required for Bing")
|
|
111
109
|
|
|
112
110
|
answer_parts: list[str] = []
|
|
111
|
+
completed_answer: str | None = None
|
|
113
112
|
raw_chunks: list[dict] = []
|
|
113
|
+
citations: list[AnswerSpanCitation] = []
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
generation_instructions=request.generation_instructions,
|
|
118
|
-
output_schema=output_schema,
|
|
115
|
+
instructions = build_bing_citation_instructions(
|
|
116
|
+
request.generation_instructions,
|
|
119
117
|
)
|
|
120
118
|
grounding = BingGroundingConfiguration(
|
|
121
119
|
fetch_size=request.fetch_size,
|
|
@@ -132,10 +130,7 @@ class BingAgentSearchService(AgentSearchEngineService[BingAgentSearchRequest]):
|
|
|
132
130
|
endpoint=read_secret(creds.endpoint),
|
|
133
131
|
transport=azure_transport,
|
|
134
132
|
) as project_client:
|
|
135
|
-
async for (
|
|
136
|
-
delta,
|
|
137
|
-
raw_event,
|
|
138
|
-
) in stream_bing_grounding_agent(
|
|
133
|
+
async for chunk in stream_bing_grounding_agent(
|
|
139
134
|
project_client,
|
|
140
135
|
http_client=http_client,
|
|
141
136
|
query=request.query,
|
|
@@ -144,11 +139,14 @@ class BingAgentSearchService(AgentSearchEngineService[BingAgentSearchRequest]):
|
|
|
144
139
|
grounding=grounding,
|
|
145
140
|
timeout=request.timeout,
|
|
146
141
|
):
|
|
147
|
-
if delta:
|
|
148
|
-
answer_parts.append(delta)
|
|
149
|
-
yield AgentSearchDelta(text=delta)
|
|
150
|
-
if raw_event:
|
|
151
|
-
raw_chunks.append(raw_event)
|
|
142
|
+
if chunk.delta:
|
|
143
|
+
answer_parts.append(chunk.delta)
|
|
144
|
+
yield AgentSearchDelta(text=chunk.delta)
|
|
145
|
+
if chunk.raw_event:
|
|
146
|
+
raw_chunks.append(chunk.raw_event)
|
|
147
|
+
if chunk.answer is not None:
|
|
148
|
+
completed_answer = chunk.answer
|
|
149
|
+
citations = list(chunk.citations)
|
|
152
150
|
break
|
|
153
151
|
except Exception as exc:
|
|
154
152
|
retryable = (
|
|
@@ -163,6 +161,8 @@ class BingAgentSearchService(AgentSearchEngineService[BingAgentSearchRequest]):
|
|
|
163
161
|
) from exc
|
|
164
162
|
|
|
165
163
|
raw_chunks.clear()
|
|
164
|
+
completed_answer = None
|
|
165
|
+
citations = []
|
|
166
166
|
retry_delay = _STREAM_RETRY_DELAYS_SECONDS[attempt - 1]
|
|
167
167
|
_LOGGER.warning(
|
|
168
168
|
"Bing response failed before output; retrying attempt %d/%d "
|
|
@@ -175,11 +175,14 @@ class BingAgentSearchService(AgentSearchEngineService[BingAgentSearchRequest]):
|
|
|
175
175
|
)
|
|
176
176
|
await asyncio.sleep(retry_delay)
|
|
177
177
|
|
|
178
|
-
answer =
|
|
178
|
+
answer = (
|
|
179
|
+
completed_answer if completed_answer is not None else "".join(answer_parts)
|
|
180
|
+
)
|
|
179
181
|
response = AgentSearchResponse(
|
|
180
182
|
engine=AgentEngineType.BING.value,
|
|
181
183
|
query=request.query,
|
|
182
184
|
answer=answer,
|
|
185
|
+
citations=citations,
|
|
183
186
|
raw=raw_chunks if len(raw_chunks) != 1 else raw_chunks[0],
|
|
184
187
|
)
|
|
185
188
|
yield AgentSearchDone(response=response)
|
|
@@ -129,6 +129,16 @@ def build_additional_schema(groups: tuple[HelmSettingsGroup, ...]) -> dict[str,
|
|
|
129
129
|
for group in groups
|
|
130
130
|
if group.helm_key is not None
|
|
131
131
|
}
|
|
132
|
+
properties["networkPolicy"] = {
|
|
133
|
+
"properties": {
|
|
134
|
+
"enabled": {"type": "boolean", "default": True},
|
|
135
|
+
"allowDisabled": {
|
|
136
|
+
"type": "boolean",
|
|
137
|
+
"default": False,
|
|
138
|
+
"description": "Allow networkPolicy.enabled=false. Set only if another control restricts who can reach search-proxy.",
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
132
142
|
return {
|
|
133
143
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
134
144
|
"properties": properties,
|
|
@@ -7,6 +7,7 @@ from unique_search_proxy_client.web.helm.generator.introspect import (
|
|
|
7
7
|
from unique_search_proxy_client.web.helm.metadata import (
|
|
8
8
|
EgressDomainWildcard,
|
|
9
9
|
EgressEndpointField,
|
|
10
|
+
EgressProxyHost,
|
|
10
11
|
)
|
|
11
12
|
from unique_search_proxy_client.web.helm.registry import HelmSettingsGroup
|
|
12
13
|
|
|
@@ -199,25 +200,83 @@ def _emit_domain_wildcard_egress(
|
|
|
199
200
|
]
|
|
200
201
|
|
|
201
202
|
|
|
203
|
+
def _proxy_values_paths(
|
|
204
|
+
group: HelmSettingsGroup, egress: EgressProxyHost
|
|
205
|
+
) -> tuple[str, str] | None:
|
|
206
|
+
host = _find_helm_field(group, egress.host_field)
|
|
207
|
+
port = _find_helm_field(group, egress.port_field)
|
|
208
|
+
if host is None or port is None:
|
|
209
|
+
return None
|
|
210
|
+
root = f".Values.{group.helm_key}"
|
|
211
|
+
return (
|
|
212
|
+
f"{root}.{host.section}.{host.helm_name}",
|
|
213
|
+
f"{root}.{port.section}.{port.helm_name}",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _emit_proxy_host_egress(
|
|
218
|
+
group: HelmSettingsGroup,
|
|
219
|
+
egress: EgressProxyHost,
|
|
220
|
+
) -> list[str]:
|
|
221
|
+
paths = _proxy_values_paths(group, egress)
|
|
222
|
+
if paths is None:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
host, port = paths
|
|
226
|
+
ipv4_octet = r"(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})"
|
|
227
|
+
return [
|
|
228
|
+
f"{{{{ if and {host} {port} }}}}",
|
|
229
|
+
f"{{{{- $host := {host} -}}}}",
|
|
230
|
+
f"{{{{- $port := {port} -}}}}",
|
|
231
|
+
r'{{- if not (and (kindIs "string" $host) (regexMatch "^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$" $host)) -}}',
|
|
232
|
+
'{{- fail "httpClient.connection.proxyHost must be a plain hostname or IPv4 address (no scheme, port, path or IPv6 literal)." -}}',
|
|
233
|
+
"{{- end -}}",
|
|
234
|
+
'{{- if not (regexMatch "^[0-9]+$" (toString $port)) -}}',
|
|
235
|
+
'{{- fail "httpClient.connection.proxyPort must be a number." -}}',
|
|
236
|
+
"{{- else if or (lt (int $port) 1) (gt (int $port) 65535) -}}",
|
|
237
|
+
'{{- fail "httpClient.connection.proxyPort must be between 1 and 65535." -}}',
|
|
238
|
+
"{{- end -}}",
|
|
239
|
+
f'{{{{- if regexMatch "^({ipv4_octet}\\\\.){{3}}{ipv4_octet}$" $host }}}}',
|
|
240
|
+
"- toCIDR:",
|
|
241
|
+
' - {{ printf "%s/32" $host | quote }}',
|
|
242
|
+
"{{- else }}",
|
|
243
|
+
'{{- if regexMatch "^[0-9.]+$" $host -}}',
|
|
244
|
+
'{{- fail "httpClient.connection.proxyHost is not a valid IPv4 address." -}}',
|
|
245
|
+
"{{- end }}",
|
|
246
|
+
"- toFQDNs:",
|
|
247
|
+
" - matchName: {{ $host | quote }}",
|
|
248
|
+
"{{- end }}",
|
|
249
|
+
" toPorts:",
|
|
250
|
+
" - ports:",
|
|
251
|
+
" - port: {{ $port | quote }}",
|
|
252
|
+
" protocol: TCP",
|
|
253
|
+
"{{- end }}",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
|
|
202
257
|
def _emit_egress_block(group: HelmSettingsGroup) -> list[str]:
|
|
203
258
|
egress = group.egress
|
|
204
259
|
if isinstance(egress, EgressEndpointField):
|
|
205
260
|
return _emit_endpoint_field_egress(group, egress)
|
|
206
261
|
if isinstance(egress, EgressDomainWildcard):
|
|
207
262
|
return _emit_domain_wildcard_egress(group, egress)
|
|
263
|
+
if isinstance(egress, EgressProxyHost):
|
|
264
|
+
return _emit_proxy_host_egress(group, egress)
|
|
208
265
|
return []
|
|
209
266
|
|
|
210
267
|
|
|
268
|
+
def _egress_active_check(group: HelmSettingsGroup) -> str:
|
|
269
|
+
if isinstance(group.egress, EgressProxyHost):
|
|
270
|
+
paths = _proxy_values_paths(group, group.egress)
|
|
271
|
+
if paths is not None:
|
|
272
|
+
return f"(and {paths[0]} {paths[1]})"
|
|
273
|
+
return f"(and .Values.{group.helm_key} .Values.{group.helm_key}.enabled)"
|
|
274
|
+
|
|
275
|
+
|
|
211
276
|
def _emit_has_rules_block(groups: tuple[HelmSettingsGroup, ...]) -> list[str]:
|
|
212
277
|
checks = [
|
|
213
|
-
|
|
214
|
-
for group in groups
|
|
215
|
-
if group.egress is not None
|
|
278
|
+
_egress_active_check(group) for group in groups if group.egress is not None
|
|
216
279
|
]
|
|
217
|
-
if not checks:
|
|
218
|
-
return ["{{/* no auto egress rules */}}"]
|
|
219
|
-
if len(checks) == 1:
|
|
220
|
-
return [f"{{{{- if {checks[0]} -}}}}true{{{{- end -}}}}"]
|
|
221
280
|
joined = " ".join(checks)
|
|
222
281
|
return [f"{{{{- if or {joined} -}}}}true{{{{- end -}}}}"]
|
|
223
282
|
|
|
@@ -43,7 +43,15 @@ class EgressDomainWildcard:
|
|
|
43
43
|
port: str = "443"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class EgressProxyHost:
|
|
48
|
+
"""Proxy egress when host and port are set: /32 ``toCIDR`` for IPv4, else ``toFQDNs``."""
|
|
49
|
+
|
|
50
|
+
host_field: str = "proxy_host"
|
|
51
|
+
port_field: str = "proxy_port"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
EgressRule = EgressEndpointField | EgressDomainWildcard | EgressProxyHost
|
|
47
55
|
"""How a settings group's outbound egress hosts are derived from its settings."""
|
|
48
56
|
|
|
49
57
|
_DEFAULT_EGRESS: EgressRule = EgressEndpointField()
|
|
@@ -7,7 +7,7 @@ from unique_search_proxy_core.http_client import (
|
|
|
7
7
|
ProxyUsernameSource,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
from unique_search_proxy_client.web.helm.metadata import helm_settings
|
|
10
|
+
from unique_search_proxy_client.web.helm.metadata import EgressProxyHost, helm_settings
|
|
11
11
|
from unique_search_proxy_client.web.settings.base import get_settings
|
|
12
12
|
|
|
13
13
|
HTTP_CLIENT_ENV_PREFIX = "HTTP_CLIENT_"
|
|
@@ -21,7 +21,7 @@ HTTP_CLIENT_ENV_PREFIX = "HTTP_CLIENT_"
|
|
|
21
21
|
# the proxy fields, not by a chart-only toggle. Gating it would silently drop
|
|
22
22
|
# proxy config from overlays that set it without ``enabled: true``.
|
|
23
23
|
gated=False,
|
|
24
|
-
egress=
|
|
24
|
+
egress=EgressProxyHost(),
|
|
25
25
|
env_prefix=HTTP_CLIENT_ENV_PREFIX,
|
|
26
26
|
sections={
|
|
27
27
|
"tuning": [
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def build_json_output_format_rule(output_schema: type[BaseModel]) -> str:
|
|
9
|
-
"""Instruction block that pins agent output to a JSON schema (Bing path)."""
|
|
10
|
-
schema_json = json.dumps(output_schema.model_json_schema(), indent=2)
|
|
11
|
-
return (
|
|
12
|
-
"## Output Format\n"
|
|
13
|
-
"Respond with a JSON object matching the schema below. "
|
|
14
|
-
"Do NOT include any text outside the JSON.\n\n"
|
|
15
|
-
f"JSON Schema:\n```json\n{schema_json}\n```"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def build_agent_instructions(
|
|
20
|
-
*,
|
|
21
|
-
generation_instructions: str,
|
|
22
|
-
output_schema: type[BaseModel],
|
|
23
|
-
) -> str:
|
|
24
|
-
"""Merge generation instructions with structured-output formatting rules."""
|
|
25
|
-
return (
|
|
26
|
-
f"{generation_instructions.rstrip()}\n\n"
|
|
27
|
-
f"{build_json_output_format_rule(output_schema)}"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
__all__ = ["build_agent_instructions", "build_json_output_format_rule"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|