unique-search-proxy 2026.26.0.dev7__tar.gz → 2026.26.0.dev9__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.
Files changed (102) hide show
  1. unique_search_proxy-2026.26.0.dev9/PKG-INFO +326 -0
  2. unique_search_proxy-2026.26.0.dev9/README.md +301 -0
  3. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/pyproject.toml +9 -3
  4. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/api/health.py +2 -0
  5. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/api/v1/__init__.py +4 -0
  6. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/api/v1/agent_search.py +199 -0
  7. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/api/v1/configuration.py +1 -0
  8. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/api/v1/crawl.py +22 -8
  9. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/api/v1/openapi_examples.py +25 -0
  10. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/api/v1/search.py +16 -5
  11. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/app.py +16 -0
  12. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/__init__.py +54 -0
  13. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/bing/client.py +64 -0
  14. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +163 -0
  15. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/bing/service.py +97 -0
  16. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/factory.py +67 -0
  17. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/serialization.py +40 -0
  18. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/service_base.py +28 -0
  19. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/structured_output.py +31 -0
  20. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +56 -0
  21. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +66 -0
  22. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +99 -0
  23. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/client/__init__.py +1 -1
  24. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/client/service.py +1 -1
  25. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/__init__.py +70 -0
  26. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +2 -6
  27. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/service.py +29 -57
  28. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +44 -0
  29. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +60 -0
  30. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +323 -0
  31. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +47 -0
  32. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/jina/service.py +162 -0
  33. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +27 -0
  34. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/crawlers/tavily/service.py +165 -0
  35. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/provider_response.py +127 -0
  36. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/providers.py +5 -1
  37. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/registry.py +36 -3
  38. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/search_engines/__init__.py +28 -0
  39. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +18 -0
  40. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +31 -0
  41. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +27 -0
  42. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/brave/service.py +197 -0
  43. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/search_engines/factory.py +22 -0
  44. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +1 -3
  45. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/google/pagination.py +27 -0
  46. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/google/query_params.py +31 -0
  47. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/search_engines/google/service.py +43 -62
  48. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/pagination.py +12 -0
  49. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +18 -0
  50. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +39 -0
  51. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +140 -0
  52. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/core/search_engines/service_base.py +19 -0
  53. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/error_handlers.py +8 -5
  54. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/monitoring/metrics.py +35 -1
  55. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/monitoring/setup.py +1 -1
  56. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/presets/__init__.py +50 -0
  57. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/presets/common.py +53 -0
  58. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/presets/crawl.py +119 -0
  59. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/presets/search.py +50 -0
  60. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/presets/types.py +33 -0
  61. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/__init__.py +21 -0
  62. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/base.py +46 -0
  63. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/core/client/settings.py → unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/client.py +2 -13
  64. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/monitoring.py +16 -0
  65. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/__init__.py +17 -0
  66. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/base.py +75 -0
  67. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/bing_agent.py +36 -0
  68. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/brave.py +28 -0
  69. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/firecrawl.py +60 -0
  70. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/google.py +29 -0
  71. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/jina.py +49 -0
  72. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/perplexity.py +28 -0
  73. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/tavily.py +47 -0
  74. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +28 -0
  75. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/utils/__init__.py +0 -0
  76. unique_search_proxy-2026.26.0.dev9/unique_search_proxy_client/web/utils/url.py +12 -0
  77. unique_search_proxy-2026.26.0.dev7/PKG-INFO +0 -275
  78. unique_search_proxy-2026.26.0.dev7/README.md +0 -256
  79. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -75
  80. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -24
  81. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/core/search_engines/google/credentials.py +0 -88
  82. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/core/search_engines/google/settings.py +0 -71
  83. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/monitoring/settings.py +0 -25
  84. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/settings/__init__.py +0 -9
  85. unique_search_proxy-2026.26.0.dev7/unique_search_proxy_client/web/settings/base.py +0 -29
  86. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/__init__.py +0 -0
  87. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/__init__.py +0 -0
  88. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/api/__init__.py +0 -0
  89. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/__init__.py +0 -0
  90. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
  91. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
  92. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
  93. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
  94. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
  95. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
  96. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
  97. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
  98. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
  99. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
  100. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
  101. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
  102. {unique_search_proxy-2026.26.0.dev7 → unique_search_proxy-2026.26.0.dev9}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.3
2
+ Name: unique-search-proxy
3
+ Version: 2026.26.0.dev9
4
+ Summary: Web Search Proxy implementation
5
+ Author: ThePhilAz
6
+ Author-email: ThePhilAz <rami.azouz@philico.com>
7
+ Requires-Dist: fastapi>=0.115.0,<1.0.0
8
+ Requires-Dist: starlette>=0.41.0,<1.0.0
9
+ Requires-Dist: uvicorn[standard]>=0.32.0,<1.0.0
10
+ Requires-Dist: pydantic>=2.12.5,<3.0.0
11
+ Requires-Dist: httpx>=0.28.0,<0.29.0
12
+ Requires-Dist: python-dotenv>=1.2.1,<2.0.0
13
+ Requires-Dist: pydantic-settings>=2.12.0,<3.0.0
14
+ Requires-Dist: markdownify>=0.14.1,<1
15
+ Requires-Dist: azure-ai-projects>=1.0.0,<2
16
+ Requires-Dist: azure-identity>=1.25.0,<2
17
+ Requires-Dist: azure-core>=1.36.0,<2
18
+ Requires-Dist: certifi>=2025.11.12,<2027
19
+ Requires-Dist: google-genai>=1.73.0,<2
20
+ Requires-Dist: google-auth>=2.43.0,<3
21
+ Requires-Dist: unique-toolkit[monitoring]>=2026.26.0.dev11,<2026.26.0rc0
22
+ Requires-Dist: unique-search-proxy-core>=2026.26.0.dev5,<2026.26.0rc0
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/markdown
25
+
26
+ # unique-search-proxy (client)
27
+
28
+ Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy` · Helm: `unique-search-proxy`
29
+
30
+ ---
31
+
32
+ ## 1. What this package is
33
+
34
+ **The client is the proxy pod.** It is the only deployable service in the stack — a FastAPI application that owns secrets, outbound networking, provider adapters, and observability.
35
+
36
+ Platform services do not talk to Google or Tavily directly. They call this server's HTTP API (usually via the [SDK](../unique_search_proxy_sdk/README.md)). Shared types and config models come from [core](../unique_search_proxy_core/README.md).
37
+
38
+ | Package | Question it answers |
39
+ |---------|---------------------|
40
+ | [Core](../unique_search_proxy_core/README.md) | *What* can be configured and *what* does a valid request/response look like? |
41
+ | **Client** (this) | *How* are provider calls executed at runtime? |
42
+ | [SDK](../unique_search_proxy_sdk/README.md) | *How* do callers reach the proxy over HTTP? |
43
+
44
+ ---
45
+
46
+ ## 2. Role in the system
47
+
48
+ ```mermaid
49
+ flowchart LR
50
+ SDK["unique_search_proxy_sdk"]
51
+ Client["unique_search_proxy_client\n(this package)"]
52
+ Core["unique_search_proxy_core"]
53
+ Pool["HttpClientPool"]
54
+ Providers["Google · Brave · Bing · …"]
55
+
56
+ SDK -->|"POST /v1/*"| Client
57
+ Client --> Core
58
+ Client --> Pool
59
+ Pool --> Providers
60
+ Client --> Providers
61
+ ```
62
+
63
+ System overview → [../README.md](../README.md)
64
+
65
+ **What the client owns:** HTTP routes, provider registry, credential loading, httpx egress pool, Prometheus, Docker/Helm deployment.
66
+
67
+ **What it does not own:** Deployment config JSON Schema, LLM call-schema projection, config/invocation merge — those live in core and run in caller processes.
68
+
69
+ ---
70
+
71
+ ## 3. Internal architecture
72
+
73
+ Requests flow through four layers inside the FastAPI app:
74
+
75
+ ```mermaid
76
+ flowchart TB
77
+ subgraph client_pkg["unique_search_proxy_client.web"]
78
+ API["api/ — route handlers"]
79
+ Reg["core/registry.py — provider lookup"]
80
+ Svc["core/*/service.py — provider adapters"]
81
+ Pool["core/client/ — HttpClientPool"]
82
+ Settings["settings/ — env & secrets"]
83
+ Mon["monitoring/ — Prometheus"]
84
+ end
85
+
86
+ API --> Reg
87
+ Reg --> Svc
88
+ Svc --> Pool
89
+ Svc --> Settings
90
+ API --> Mon
91
+ ```
92
+
93
+ | Layer | Path | Responsibility |
94
+ |-------|------|----------------|
95
+ | **Entry** | `app.py` | App factory, lifespan (start/stop pool), router mount |
96
+ | **API** | `api/health.py`, `api/v1/*.py` | Validate body via core models, dispatch, record metrics |
97
+ | **Registry** | `core/registry.py`, `core/providers.py` | Register built-in engines/crawlers at startup |
98
+ | **Services** | `core/search_engines/`, `core/agent_engines/`, `core/crawlers/` | Provider-specific HTTP/SDK calls, response curation |
99
+ | **Egress** | `core/client/service.py` | Shared httpx client (optional corporate proxy / mTLS) |
100
+ | **Settings** | `settings/providers/*`, `settings/client.py` | Per-provider credentials; unset → `NOT_PROVIDED` → 503 |
101
+ | **Monitoring** | `monitoring/` | Search / agent / crawl latency and error counters |
102
+ | **Deploy** | `deploy/` | Dockerfile, Helm chart, uvicorn entrypoint |
103
+
104
+ Built-in providers register in `register_builtin_providers()` during `create_app()`.
105
+
106
+ ---
107
+
108
+ ## 4. HTTP API
109
+
110
+ | Endpoint | Description |
111
+ |----------|-------------|
112
+ | `GET /health` | Liveness |
113
+ | `GET /ready` | Readiness (httpx pool + registered providers) |
114
+ | `GET /v1/configuration/providers` | Registered search engine, agent engine, and crawler ids |
115
+ | `POST /v1/search` | Standard search (flat body: `engine`, `query`, provider params, `timeout`) |
116
+ | `POST /v1/agent-search` | Grounded agent search — opaque `answer` + `raw` |
117
+ | `POST /v1/agent-search/stream` | Same, streamed as SSE (`delta` + `done`) |
118
+ | `POST /v1/crawl` | Crawl URLs (flat body: `crawler`, `urls`, `timeout`, …) |
119
+ | `GET /metrics` | Prometheus (when enabled) |
120
+ | `/docs` | Swagger UI with preset examples |
121
+
122
+ OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
123
+
124
+ ---
125
+
126
+ ## 5. Providers
127
+
128
+ ### 5.1 Summary
129
+
130
+ | Provider | Type | Upstream | Credentials |
131
+ |----------|------|----------|-------------|
132
+ | Google | Search | Custom Search JSON API | `GOOGLE_SEARCH_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID` |
133
+ | Brave | Search | Brave Search API | `BRAVE_SEARCH_API_KEY` |
134
+ | Perplexity | Search | Perplexity API | `PERPLEXITY_SEARCH_API_KEY` |
135
+ | Bing | Agent | Azure AI Projects grounding | `BING_AGENT_*` |
136
+ | VertexAI | Agent | Google GenAI + grounding | `VERTEXAI_AGENT_*` or ADC |
137
+ | Basic | Crawl | Direct httpx + HTML/PDF processors | (none) |
138
+ | Tavily | Crawl | Tavily extract API | `TAVILY_API_KEY` |
139
+ | Jina | Crawl | Jina Reader API | `JINA_API_KEY` |
140
+ | Firecrawl | Crawl | Firecrawl scrape (with polling) | `FIRECRAWL_API_KEY` |
141
+
142
+ Unconfigured providers return **503** `ENGINE_NOT_CONFIGURED` with missing env var names. `GET /v1/configuration/providers` lists what is registered in the running pod.
143
+
144
+ ### 5.2 Search (`POST /v1/search`)
145
+
146
+ Flat request — tooling merges deployment config with LLM args in **core** before calling the proxy:
147
+
148
+ ```json
149
+ {
150
+ "engine": "google",
151
+ "query": "example query",
152
+ "fetchSize": 10,
153
+ "gl": "de",
154
+ "dateRestrict": "d7",
155
+ "timeout": 30
156
+ }
157
+ ```
158
+
159
+ Response includes **curated** normalised results and opaque **raw** provider payload:
160
+
161
+ ```json
162
+ {
163
+ "engine": "google",
164
+ "query": "example query",
165
+ "raw": { "pages": [{ "pageIndex": 1, "response": {} }] },
166
+ "curated": [
167
+ { "url": "https://example.com", "title": "Example", "snippet": "...", "content": "" }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ ### 5.3 Agent search (`POST /v1/agent-search`)
173
+
174
+ Thin egress — proxy returns opaque agent text; callers own parsing and citation extraction:
175
+
176
+ ```json
177
+ {
178
+ "engine": "bing",
179
+ "query": "latest EU AI Act timeline",
180
+ "generationInstructions": "...",
181
+ "fetchSize": 5,
182
+ "timeout": 120
183
+ }
184
+ ```
185
+
186
+ Streaming (`/v1/agent-search/stream`) emits SSE `{ "type": "delta", "text": "..." }` chunks and a terminal `{ "type": "done", "response": { … } }`.
187
+
188
+ ### 5.4 Crawl (`POST /v1/crawl`)
189
+
190
+ Per-URL outcomes — one URL failing does not fail the batch:
191
+
192
+ ```json
193
+ {
194
+ "urls": ["https://example.com"],
195
+ "crawler": "Basic",
196
+ "timeout": 30
197
+ }
198
+ ```
199
+
200
+ Crawler discriminators: `Basic`, `Tavily`, `Jina`, `Firecrawl`.
201
+
202
+ ### 5.5 Errors
203
+
204
+ Structured envelope on all non-2xx responses:
205
+
206
+ ```json
207
+ {
208
+ "error": {
209
+ "code": "ENGINE_NOT_CONFIGURED",
210
+ "message": "Provider is not configured. Set environment variable(s): GOOGLE_SEARCH_API_KEY",
211
+ "retryable": false
212
+ }
213
+ }
214
+ ```
215
+
216
+ Error types are defined in [core](../unique_search_proxy_core/README.md) and raised by the [SDK](../unique_search_proxy_sdk/README.md) on the caller side.
217
+
218
+ ---
219
+
220
+ ## 6. Configuration
221
+
222
+ Settings use pydantic-settings with per-provider env vars. Copy `.env.example` to `.env` for an annotated template.
223
+
224
+ | Component | Prefix / vars | Example |
225
+ |-----------|---------------|---------|
226
+ | Google search | (none) | `GOOGLE_SEARCH_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID` |
227
+ | Brave search | (none) | `BRAVE_SEARCH_API_KEY`, `BRAVE_SEARCH_API_ENDPOINT` |
228
+ | Perplexity | (none) | `PERPLEXITY_SEARCH_API_KEY` |
229
+ | Tavily | `TAVILY_` | `TAVILY_API_KEY` |
230
+ | Jina | `JINA_` | `JINA_API_KEY`, `JINA_DEPLOYMENT` |
231
+ | Firecrawl | `FIRECRAWL_` | `FIRECRAWL_API_KEY`, `FIRECRAWL_API_VERSION` |
232
+ | Bing agent | `BING_AGENT_` | `BING_AGENT_ENDPOINT`, `BING_AGENT_BING_RESOURCE_CONNECTION_STRING` |
233
+ | VertexAI agent | `VERTEXAI_AGENT_` | `VERTEXAI_AGENT_SERVICE_ACCOUNT_CREDENTIALS` (optional) |
234
+ | HTTP client | `HTTP_CLIENT_` | `HTTP_CLIENT_PROXY_HOST`, `HTTP_CLIENT_POOL_TIMEOUT_SECONDS` |
235
+ | Prometheus | `PROMETHEUS_` | `PROMETHEUS_ENABLED` |
236
+ | Container | (shell) | `HOST`, `PORT`, `WORKERS`, `LOG_LEVEL` |
237
+
238
+ With `WORKERS > 1`, the entrypoint sets `PROMETHEUS_MULTIPROC_DIR` for correct metric aggregation.
239
+
240
+ ---
241
+
242
+ ## 7. Quick start
243
+
244
+ **Prerequisites:** Python 3.12+, uv
245
+
246
+ ```bash
247
+ uv sync
248
+ cp .env.example .env
249
+ # Edit .env: set GOOGLE_SEARCH_API_KEY and GOOGLE_SEARCH_ENGINE_ID for live /v1/search
250
+ ```
251
+
252
+ ```bash
253
+ uv run python -m unique_search_proxy_client.web.app
254
+ # or
255
+ uv run uvicorn unique_search_proxy_client.web.app:app --reload --port 2349
256
+ ```
257
+
258
+ Call from Python via the [SDK](../unique_search_proxy_sdk/README.md):
259
+
260
+ ```python
261
+ from unique_search_proxy_sdk import UniqueSearchProxyClient
262
+
263
+ async with UniqueSearchProxyClient("http://localhost:2349") as client:
264
+ result = await client.search.search("unique ag", engine="google", fetchSize=10)
265
+ ```
266
+
267
+ Regenerate SDK after API changes:
268
+
269
+ ```bash
270
+ uv run python scripts/generate_sdk.py
271
+ ```
272
+
273
+ ---
274
+
275
+ ## 8. Dev testing
276
+
277
+ 1. Start the server and configure `.env`.
278
+ 2. **Swagger** — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset.
279
+ 3. **CLI** — same presets from the terminal:
280
+
281
+ ```bash
282
+ uv run python scripts/try_presets.py list
283
+ uv run python scripts/try_presets.py run google_minimal
284
+ uv run python scripts/try_presets.py run-all --kind crawl
285
+ uv run python scripts/try_presets.py run google_minimal --base-url http://127.0.0.1:8080
286
+ ```
287
+
288
+ Presets live in `web/presets/` (shared with Swagger examples). Add `--strict` to exit non-zero on any non-2xx response.
289
+
290
+ ---
291
+
292
+ ## 9. Project structure
293
+
294
+ ```
295
+ unique_search_proxy_client/
296
+ ├── openapi.json # Exported spec (SDK codegen input)
297
+ ├── scripts/
298
+ │ ├── generate_sdk.py
299
+ │ └── try_presets.py
300
+ ├── deploy/ # Dockerfile, Helm chart
301
+ ├── tests/
302
+ └── unique_search_proxy_client/web/
303
+ ├── app.py
304
+ ├── api/ # health + v1 routes
305
+ ├── core/ # registry, services, HttpClientPool
306
+ ├── settings/ # env & provider credentials
307
+ ├── monitoring/
308
+ └── presets/
309
+ ```
310
+
311
+ ---
312
+
313
+ ## 10. Development
314
+
315
+ ```bash
316
+ uv run ruff check .
317
+ uv run ruff format .
318
+ uv run pytest
319
+ uv run basedpyright
320
+ ```
321
+
322
+ ---
323
+
324
+ ## License
325
+
326
+ Proprietary — Unique AG
@@ -0,0 +1,301 @@
1
+ # unique-search-proxy (client)
2
+
3
+ Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy` · Helm: `unique-search-proxy`
4
+
5
+ ---
6
+
7
+ ## 1. What this package is
8
+
9
+ **The client is the proxy pod.** It is the only deployable service in the stack — a FastAPI application that owns secrets, outbound networking, provider adapters, and observability.
10
+
11
+ Platform services do not talk to Google or Tavily directly. They call this server's HTTP API (usually via the [SDK](../unique_search_proxy_sdk/README.md)). Shared types and config models come from [core](../unique_search_proxy_core/README.md).
12
+
13
+ | Package | Question it answers |
14
+ |---------|---------------------|
15
+ | [Core](../unique_search_proxy_core/README.md) | *What* can be configured and *what* does a valid request/response look like? |
16
+ | **Client** (this) | *How* are provider calls executed at runtime? |
17
+ | [SDK](../unique_search_proxy_sdk/README.md) | *How* do callers reach the proxy over HTTP? |
18
+
19
+ ---
20
+
21
+ ## 2. Role in the system
22
+
23
+ ```mermaid
24
+ flowchart LR
25
+ SDK["unique_search_proxy_sdk"]
26
+ Client["unique_search_proxy_client\n(this package)"]
27
+ Core["unique_search_proxy_core"]
28
+ Pool["HttpClientPool"]
29
+ Providers["Google · Brave · Bing · …"]
30
+
31
+ SDK -->|"POST /v1/*"| Client
32
+ Client --> Core
33
+ Client --> Pool
34
+ Pool --> Providers
35
+ Client --> Providers
36
+ ```
37
+
38
+ System overview → [../README.md](../README.md)
39
+
40
+ **What the client owns:** HTTP routes, provider registry, credential loading, httpx egress pool, Prometheus, Docker/Helm deployment.
41
+
42
+ **What it does not own:** Deployment config JSON Schema, LLM call-schema projection, config/invocation merge — those live in core and run in caller processes.
43
+
44
+ ---
45
+
46
+ ## 3. Internal architecture
47
+
48
+ Requests flow through four layers inside the FastAPI app:
49
+
50
+ ```mermaid
51
+ flowchart TB
52
+ subgraph client_pkg["unique_search_proxy_client.web"]
53
+ API["api/ — route handlers"]
54
+ Reg["core/registry.py — provider lookup"]
55
+ Svc["core/*/service.py — provider adapters"]
56
+ Pool["core/client/ — HttpClientPool"]
57
+ Settings["settings/ — env & secrets"]
58
+ Mon["monitoring/ — Prometheus"]
59
+ end
60
+
61
+ API --> Reg
62
+ Reg --> Svc
63
+ Svc --> Pool
64
+ Svc --> Settings
65
+ API --> Mon
66
+ ```
67
+
68
+ | Layer | Path | Responsibility |
69
+ |-------|------|----------------|
70
+ | **Entry** | `app.py` | App factory, lifespan (start/stop pool), router mount |
71
+ | **API** | `api/health.py`, `api/v1/*.py` | Validate body via core models, dispatch, record metrics |
72
+ | **Registry** | `core/registry.py`, `core/providers.py` | Register built-in engines/crawlers at startup |
73
+ | **Services** | `core/search_engines/`, `core/agent_engines/`, `core/crawlers/` | Provider-specific HTTP/SDK calls, response curation |
74
+ | **Egress** | `core/client/service.py` | Shared httpx client (optional corporate proxy / mTLS) |
75
+ | **Settings** | `settings/providers/*`, `settings/client.py` | Per-provider credentials; unset → `NOT_PROVIDED` → 503 |
76
+ | **Monitoring** | `monitoring/` | Search / agent / crawl latency and error counters |
77
+ | **Deploy** | `deploy/` | Dockerfile, Helm chart, uvicorn entrypoint |
78
+
79
+ Built-in providers register in `register_builtin_providers()` during `create_app()`.
80
+
81
+ ---
82
+
83
+ ## 4. HTTP API
84
+
85
+ | Endpoint | Description |
86
+ |----------|-------------|
87
+ | `GET /health` | Liveness |
88
+ | `GET /ready` | Readiness (httpx pool + registered providers) |
89
+ | `GET /v1/configuration/providers` | Registered search engine, agent engine, and crawler ids |
90
+ | `POST /v1/search` | Standard search (flat body: `engine`, `query`, provider params, `timeout`) |
91
+ | `POST /v1/agent-search` | Grounded agent search — opaque `answer` + `raw` |
92
+ | `POST /v1/agent-search/stream` | Same, streamed as SSE (`delta` + `done`) |
93
+ | `POST /v1/crawl` | Crawl URLs (flat body: `crawler`, `urls`, `timeout`, …) |
94
+ | `GET /metrics` | Prometheus (when enabled) |
95
+ | `/docs` | Swagger UI with preset examples |
96
+
97
+ OpenAPI spec: `openapi.json` (input for [SDK codegen](../unique_search_proxy_sdk/README.md)).
98
+
99
+ ---
100
+
101
+ ## 5. Providers
102
+
103
+ ### 5.1 Summary
104
+
105
+ | Provider | Type | Upstream | Credentials |
106
+ |----------|------|----------|-------------|
107
+ | Google | Search | Custom Search JSON API | `GOOGLE_SEARCH_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID` |
108
+ | Brave | Search | Brave Search API | `BRAVE_SEARCH_API_KEY` |
109
+ | Perplexity | Search | Perplexity API | `PERPLEXITY_SEARCH_API_KEY` |
110
+ | Bing | Agent | Azure AI Projects grounding | `BING_AGENT_*` |
111
+ | VertexAI | Agent | Google GenAI + grounding | `VERTEXAI_AGENT_*` or ADC |
112
+ | Basic | Crawl | Direct httpx + HTML/PDF processors | (none) |
113
+ | Tavily | Crawl | Tavily extract API | `TAVILY_API_KEY` |
114
+ | Jina | Crawl | Jina Reader API | `JINA_API_KEY` |
115
+ | Firecrawl | Crawl | Firecrawl scrape (with polling) | `FIRECRAWL_API_KEY` |
116
+
117
+ Unconfigured providers return **503** `ENGINE_NOT_CONFIGURED` with missing env var names. `GET /v1/configuration/providers` lists what is registered in the running pod.
118
+
119
+ ### 5.2 Search (`POST /v1/search`)
120
+
121
+ Flat request — tooling merges deployment config with LLM args in **core** before calling the proxy:
122
+
123
+ ```json
124
+ {
125
+ "engine": "google",
126
+ "query": "example query",
127
+ "fetchSize": 10,
128
+ "gl": "de",
129
+ "dateRestrict": "d7",
130
+ "timeout": 30
131
+ }
132
+ ```
133
+
134
+ Response includes **curated** normalised results and opaque **raw** provider payload:
135
+
136
+ ```json
137
+ {
138
+ "engine": "google",
139
+ "query": "example query",
140
+ "raw": { "pages": [{ "pageIndex": 1, "response": {} }] },
141
+ "curated": [
142
+ { "url": "https://example.com", "title": "Example", "snippet": "...", "content": "" }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ ### 5.3 Agent search (`POST /v1/agent-search`)
148
+
149
+ Thin egress — proxy returns opaque agent text; callers own parsing and citation extraction:
150
+
151
+ ```json
152
+ {
153
+ "engine": "bing",
154
+ "query": "latest EU AI Act timeline",
155
+ "generationInstructions": "...",
156
+ "fetchSize": 5,
157
+ "timeout": 120
158
+ }
159
+ ```
160
+
161
+ Streaming (`/v1/agent-search/stream`) emits SSE `{ "type": "delta", "text": "..." }` chunks and a terminal `{ "type": "done", "response": { … } }`.
162
+
163
+ ### 5.4 Crawl (`POST /v1/crawl`)
164
+
165
+ Per-URL outcomes — one URL failing does not fail the batch:
166
+
167
+ ```json
168
+ {
169
+ "urls": ["https://example.com"],
170
+ "crawler": "Basic",
171
+ "timeout": 30
172
+ }
173
+ ```
174
+
175
+ Crawler discriminators: `Basic`, `Tavily`, `Jina`, `Firecrawl`.
176
+
177
+ ### 5.5 Errors
178
+
179
+ Structured envelope on all non-2xx responses:
180
+
181
+ ```json
182
+ {
183
+ "error": {
184
+ "code": "ENGINE_NOT_CONFIGURED",
185
+ "message": "Provider is not configured. Set environment variable(s): GOOGLE_SEARCH_API_KEY",
186
+ "retryable": false
187
+ }
188
+ }
189
+ ```
190
+
191
+ Error types are defined in [core](../unique_search_proxy_core/README.md) and raised by the [SDK](../unique_search_proxy_sdk/README.md) on the caller side.
192
+
193
+ ---
194
+
195
+ ## 6. Configuration
196
+
197
+ Settings use pydantic-settings with per-provider env vars. Copy `.env.example` to `.env` for an annotated template.
198
+
199
+ | Component | Prefix / vars | Example |
200
+ |-----------|---------------|---------|
201
+ | Google search | (none) | `GOOGLE_SEARCH_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID` |
202
+ | Brave search | (none) | `BRAVE_SEARCH_API_KEY`, `BRAVE_SEARCH_API_ENDPOINT` |
203
+ | Perplexity | (none) | `PERPLEXITY_SEARCH_API_KEY` |
204
+ | Tavily | `TAVILY_` | `TAVILY_API_KEY` |
205
+ | Jina | `JINA_` | `JINA_API_KEY`, `JINA_DEPLOYMENT` |
206
+ | Firecrawl | `FIRECRAWL_` | `FIRECRAWL_API_KEY`, `FIRECRAWL_API_VERSION` |
207
+ | Bing agent | `BING_AGENT_` | `BING_AGENT_ENDPOINT`, `BING_AGENT_BING_RESOURCE_CONNECTION_STRING` |
208
+ | VertexAI agent | `VERTEXAI_AGENT_` | `VERTEXAI_AGENT_SERVICE_ACCOUNT_CREDENTIALS` (optional) |
209
+ | HTTP client | `HTTP_CLIENT_` | `HTTP_CLIENT_PROXY_HOST`, `HTTP_CLIENT_POOL_TIMEOUT_SECONDS` |
210
+ | Prometheus | `PROMETHEUS_` | `PROMETHEUS_ENABLED` |
211
+ | Container | (shell) | `HOST`, `PORT`, `WORKERS`, `LOG_LEVEL` |
212
+
213
+ With `WORKERS > 1`, the entrypoint sets `PROMETHEUS_MULTIPROC_DIR` for correct metric aggregation.
214
+
215
+ ---
216
+
217
+ ## 7. Quick start
218
+
219
+ **Prerequisites:** Python 3.12+, uv
220
+
221
+ ```bash
222
+ uv sync
223
+ cp .env.example .env
224
+ # Edit .env: set GOOGLE_SEARCH_API_KEY and GOOGLE_SEARCH_ENGINE_ID for live /v1/search
225
+ ```
226
+
227
+ ```bash
228
+ uv run python -m unique_search_proxy_client.web.app
229
+ # or
230
+ uv run uvicorn unique_search_proxy_client.web.app:app --reload --port 2349
231
+ ```
232
+
233
+ Call from Python via the [SDK](../unique_search_proxy_sdk/README.md):
234
+
235
+ ```python
236
+ from unique_search_proxy_sdk import UniqueSearchProxyClient
237
+
238
+ async with UniqueSearchProxyClient("http://localhost:2349") as client:
239
+ result = await client.search.search("unique ag", engine="google", fetchSize=10)
240
+ ```
241
+
242
+ Regenerate SDK after API changes:
243
+
244
+ ```bash
245
+ uv run python scripts/generate_sdk.py
246
+ ```
247
+
248
+ ---
249
+
250
+ ## 8. Dev testing
251
+
252
+ 1. Start the server and configure `.env`.
253
+ 2. **Swagger** — `/docs` → **Try it out** on `/v1/search` or `/v1/crawl` → pick an **Examples** preset.
254
+ 3. **CLI** — same presets from the terminal:
255
+
256
+ ```bash
257
+ uv run python scripts/try_presets.py list
258
+ uv run python scripts/try_presets.py run google_minimal
259
+ uv run python scripts/try_presets.py run-all --kind crawl
260
+ uv run python scripts/try_presets.py run google_minimal --base-url http://127.0.0.1:8080
261
+ ```
262
+
263
+ Presets live in `web/presets/` (shared with Swagger examples). Add `--strict` to exit non-zero on any non-2xx response.
264
+
265
+ ---
266
+
267
+ ## 9. Project structure
268
+
269
+ ```
270
+ unique_search_proxy_client/
271
+ ├── openapi.json # Exported spec (SDK codegen input)
272
+ ├── scripts/
273
+ │ ├── generate_sdk.py
274
+ │ └── try_presets.py
275
+ ├── deploy/ # Dockerfile, Helm chart
276
+ ├── tests/
277
+ └── unique_search_proxy_client/web/
278
+ ├── app.py
279
+ ├── api/ # health + v1 routes
280
+ ├── core/ # registry, services, HttpClientPool
281
+ ├── settings/ # env & provider credentials
282
+ ├── monitoring/
283
+ └── presets/
284
+ ```
285
+
286
+ ---
287
+
288
+ ## 10. Development
289
+
290
+ ```bash
291
+ uv run ruff check .
292
+ uv run ruff format .
293
+ uv run pytest
294
+ uv run basedpyright
295
+ ```
296
+
297
+ ---
298
+
299
+ ## License
300
+
301
+ Proprietary — Unique AG
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy"
3
- version = "2026.26.0.dev7"
3
+ version = "2026.26.0.dev9"
4
4
  description = "Web Search Proxy implementation"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -14,8 +14,14 @@ dependencies = [
14
14
  "python-dotenv>=1.2.1,<2.0.0",
15
15
  "pydantic-settings>=2.12.0,<3.0.0",
16
16
  "markdownify>=0.14.1,<1",
17
- "unique-toolkit[monitoring]>=2026.26.0.dev6,<2026.26.0rc0",
18
- "unique-search-proxy-core>=2026.26.0.dev3,<2026.26.0rc0",
17
+ "azure-ai-projects>=1.0.0,<2",
18
+ "azure-identity>=1.25.0,<2",
19
+ "azure-core>=1.36.0,<2",
20
+ "certifi>=2025.11.12,<2027",
21
+ "google-genai>=1.73.0,<2",
22
+ "google-auth>=2.43.0,<3",
23
+ "unique-toolkit[monitoring]>=2026.26.0.dev11,<2026.26.0rc0",
24
+ "unique-search-proxy-core>=2026.26.0.dev5,<2026.26.0rc0",
19
25
  ]
20
26
 
21
27
  [dependency-groups]
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Request
4
4
 
5
5
  from unique_search_proxy_client.web.core.client import get_http_client_pool
6
6
  from unique_search_proxy_client.web.core.registry import (
7
+ registered_agent_engines,
7
8
  registered_crawlers,
8
9
  registered_search_engines,
9
10
  )
@@ -23,5 +24,6 @@ async def ready(request: Request) -> dict[str, object]:
23
24
  "status": "ready",
24
25
  "httpClient": "ok" if not pool.client.is_closed else "closed",
25
26
  "searchEngines": sorted(registered_search_engines()),
27
+ "agentEngines": sorted(registered_agent_engines()),
26
28
  "crawlers": sorted(registered_crawlers()),
27
29
  }
@@ -1,5 +1,8 @@
1
1
  from fastapi import APIRouter
2
2
 
3
+ from unique_search_proxy_client.web.api.v1.agent_search import (
4
+ router as agent_search_router,
5
+ )
3
6
  from unique_search_proxy_client.web.api.v1.configuration import (
4
7
  router as configuration_router,
5
8
  )
@@ -9,6 +12,7 @@ from unique_search_proxy_client.web.api.v1.search import router as search_router
9
12
  v1_router = APIRouter(prefix="/v1")
10
13
  v1_router.include_router(configuration_router)
11
14
  v1_router.include_router(search_router)
15
+ v1_router.include_router(agent_search_router)
12
16
  v1_router.include_router(crawl_router)
13
17
 
14
18
  __all__ = ["v1_router"]