unique-search-proxy-core 2026.26.0.dev4__tar.gz → 2026.26.0.dev5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unique_search_proxy_core-2026.26.0.dev5/PKG-INFO +287 -0
- unique_search_proxy_core-2026.26.0.dev5/README.md +276 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/pyproject.toml +1 -1
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/__init__.py +27 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/base.py +100 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/bing/__init__.py +6 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/bing/schema.py +44 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/config_types.py +91 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/output_schema.py +62 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/projection.py +47 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/resolve.py +35 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/response_parsing.py +200 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/vertexai/__init__.py +6 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/agent_engines/vertexai/schema.py +43 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/crawlers/__init__.py +4 -4
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/crawlers/base.py +9 -6
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/crawlers/basic/__init__.py +4 -6
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/crawlers/basic/schema.py +40 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/crawlers/config_types.py +103 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/crawlers/firecrawl/schema.py +97 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/crawlers/jina/schema.py +129 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/crawlers/params.py +14 -8
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/crawlers/projection.py +43 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/crawlers/tavily/schema.py +81 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/errors.py +43 -14
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/logging.py +31 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/model_derivation/__init__.py +21 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/model_derivation/derive.py +52 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/model_derivation/fields.py +144 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/projection.py +133 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/providers/schema.py +2 -3
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/schema.py +73 -8
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/search_engines/__init__.py +18 -2
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/search_engines/base.py +11 -8
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/search_engines/brave/__init__.py +11 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/search_engines/brave/schema.py +162 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/search_engines/call_schema.py +0 -6
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/search_engines/config_types.py +5 -1
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/search_engines/google/__init__.py +2 -2
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/search_engines/google/schema.py +2 -2
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/search_engines/params.py +2 -1
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/search_engines/perplexity/__init__.py +11 -0
- unique_search_proxy_core-2026.26.0.dev5/unique_search_proxy_core/search_engines/perplexity/schema.py +199 -0
- unique_search_proxy_core-2026.26.0.dev4/PKG-INFO +0 -16
- unique_search_proxy_core-2026.26.0.dev4/README.md +0 -5
- unique_search_proxy_core-2026.26.0.dev4/unique_search_proxy_core/crawlers/basic/schema.py +0 -60
- unique_search_proxy_core-2026.26.0.dev4/unique_search_proxy_core/crawlers/call_schema.py +0 -75
- unique_search_proxy_core-2026.26.0.dev4/unique_search_proxy_core/crawlers/config_types.py +0 -66
- unique_search_proxy_core-2026.26.0.dev4/unique_search_proxy_core/projection.py +0 -313
- unique_search_proxy_core-2026.26.0.dev4/unique_search_proxy_core/search_engines/pagination.py +0 -36
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/__init__.py +0 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/param_policy/__init__.py +0 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/param_policy/exposable_param.py +0 -0
- {unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/unique_search_proxy_core/param_policy/policy.py +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: unique-search-proxy-core
|
|
3
|
+
Version: 2026.26.0.dev5
|
|
4
|
+
Summary: Shared Pydantic types for the Unique Search Proxy API
|
|
5
|
+
Author: ThePhilAz
|
|
6
|
+
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
7
|
+
Requires-Dist: pydantic>=2.12.5,<3.0.0
|
|
8
|
+
Requires-Dist: pyhumps>=3.8.0,<4
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# unique-search-proxy-core
|
|
13
|
+
|
|
14
|
+
Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy-core`
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. What this package is
|
|
19
|
+
|
|
20
|
+
**Core is the contract layer.** It defines every shared type — deployment configs, HTTP request/response shapes, error codes, and LLM tool schemas — without importing FastAPI, httpx pools, or provider SDKs.
|
|
21
|
+
|
|
22
|
+
Install it anywhere you need to **describe** or **validate** proxy behaviour: the proxy server, the HTTP SDK, assistants-core tool manifests, deployment UIs.
|
|
23
|
+
|
|
24
|
+
| Package | Question it answers |
|
|
25
|
+
|---------|---------------------|
|
|
26
|
+
| **Core** (this) | *What* can be configured and *what* does a valid request/response look like? |
|
|
27
|
+
| [Client](../unique_search_proxy_client/README.md) | *How* are provider calls executed at runtime? |
|
|
28
|
+
| [SDK](../unique_search_proxy_sdk/README.md) | *How* do callers reach the proxy over HTTP? |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Role in the system
|
|
33
|
+
|
|
34
|
+
Core sits at the centre of **Path A** (schema & config). It is imported by both the proxy pod and caller services; it never makes HTTP calls itself.
|
|
35
|
+
|
|
36
|
+
```mermaid
|
|
37
|
+
flowchart TB
|
|
38
|
+
subgraph consumers["Consumers of core"]
|
|
39
|
+
Client["unique_search_proxy_client"]
|
|
40
|
+
SDK["unique_search_proxy_sdk"]
|
|
41
|
+
AC["assistants-core / deployment UI"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
subgraph core["unique_search_proxy_core"]
|
|
45
|
+
Contracts["Response & error models"]
|
|
46
|
+
Config["*Config deployment models"]
|
|
47
|
+
Project["Schema projection & merge"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
AC --> Config
|
|
51
|
+
AC --> Project
|
|
52
|
+
Client --> Contracts
|
|
53
|
+
Client --> Config
|
|
54
|
+
SDK --> Contracts
|
|
55
|
+
Project -->|"flat request body"| SDK
|
|
56
|
+
Project -->|"flat request body"| Client
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
System overview → [../README.md](../README.md)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. Key concepts
|
|
64
|
+
|
|
65
|
+
Core uses **three different config patterns** depending on provider kind. Only search engines use `ExposableParam` and the full projection pipeline.
|
|
66
|
+
|
|
67
|
+
### 3.1 Three provider patterns (search vs agent vs crawl)
|
|
68
|
+
|
|
69
|
+
| Kind | Config → request | `ExposableParam` | LLM call schema | Config/invocation merge |
|
|
70
|
+
|------|------------------|------------------|-----------------|-------------------------|
|
|
71
|
+
| **Search engines** | `GoogleConfig` → `GoogleSearchRequest` via `projection.build_request_model` | Yes | `build_llm_call_model` + `call_schema.resolve_*` | `merge_config_and_invocation` |
|
|
72
|
+
| **Agent engines** | `BingAgentConfig` → `BingAgentSearchRequest` via `agent_engines/projection.build_agent_request_model` | No (plain fields) | Not implemented in core | Not implemented in core |
|
|
73
|
+
| **Crawlers** | `BasicConfig` → `BasicCrawlRequest` via `crawlers/projection.build_crawl_request_model` | No | Not implemented in core | `merge_crawler_config_and_invocation` |
|
|
74
|
+
|
|
75
|
+
```mermaid
|
|
76
|
+
flowchart TB
|
|
77
|
+
subgraph search["Search engines only"]
|
|
78
|
+
GC["GoogleConfig\n(ExposableParam knobs)"]
|
|
79
|
+
Proj["projection.py\nbuild_request_model\nbuild_llm_call_model"]
|
|
80
|
+
GSR["GoogleSearchRequest"]
|
|
81
|
+
LLM["LLM call schema"]
|
|
82
|
+
GC --> Proj
|
|
83
|
+
Proj --> GSR
|
|
84
|
+
Proj --> LLM
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
subgraph agent["Agent engines"]
|
|
88
|
+
BC["BingAgentConfig\n(plain fields)"]
|
|
89
|
+
AProj["agent_engines/projection.py\nbuild_agent_request_model"]
|
|
90
|
+
BSR["BingAgentSearchRequest"]
|
|
91
|
+
BC --> AProj
|
|
92
|
+
AProj -->|"via model_derivation"| BSR
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
subgraph crawl["Crawlers"]
|
|
96
|
+
BCrawl["BasicConfig\n(no urls)"]
|
|
97
|
+
CProj["crawlers/projection.py\nbuild_crawl_request_model"]
|
|
98
|
+
BCR["BasicCrawlRequest\n(urls injected)"]
|
|
99
|
+
BCrawl --> CProj
|
|
100
|
+
CProj -->|"via model_derivation"| BCR
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3.2 Search engines — three surfaces from one config
|
|
105
|
+
|
|
106
|
+
Search is the only kind where a single `*Config` fans out into three derived surfaces:
|
|
107
|
+
|
|
108
|
+
| Surface | Audience | Example |
|
|
109
|
+
|---------|----------|---------|
|
|
110
|
+
| **Deployment config** | Admin / deployment UI | `{ "engine": "google", "gl": { "expose": true, "value": "de" }, … }` |
|
|
111
|
+
| **LLM call schema** | Tool manifest shown to the model | `{ "query", "gl" }` — only fields where `expose: true` |
|
|
112
|
+
| **HTTP request body** | `POST /v1/search` wire format | `{ "engine": "google", "query": "…", "gl": "de", "fetchSize": 10 }` |
|
|
113
|
+
|
|
114
|
+
`projection.py` and `search_engines/params.py` derive the latter two from the first. Agent and crawl use `model_derivation.derive_request_model` via per-kind projection modules to inject `query` or `urls`.
|
|
115
|
+
|
|
116
|
+
### 3.3 ExposableParam — search-engine only
|
|
117
|
+
|
|
118
|
+
Optional **search** parameters use `ExposableParam[T]`:
|
|
119
|
+
|
|
120
|
+
- **`value`** — admin default merged into every request (`null` = deactivated)
|
|
121
|
+
- **`expose`** — when `true`, the parameter appears on the LLM call schema
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
gl: ExposableParam[str | None] = ExposableParam(expose=False, value="de") # admin-fixed
|
|
125
|
+
gl: ExposableParam[str | None] = ExposableParam(expose=True, value=None) # LLM-overridable
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
No agent or crawler schema imports `ExposableParam` today.
|
|
129
|
+
|
|
130
|
+
### 3.4 merge_config_and_invocation — search-engine only
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from unique_search_proxy_core.search_engines import merge_config_and_invocation
|
|
134
|
+
|
|
135
|
+
request = merge_config_and_invocation(google_config, {"query": "EU AI Act"})
|
|
136
|
+
# → validated GoogleSearchRequest ready for POST /v1/search
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The proxy receives a flat body; it does not resolve deployment config over HTTP.
|
|
140
|
+
|
|
141
|
+
### 3.5 merge_crawler_config_and_invocation — crawler only
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from unique_search_proxy_core.crawlers import merge_crawler_config_and_invocation
|
|
145
|
+
|
|
146
|
+
request = merge_crawler_config_and_invocation(basic_config, {"urls": ["https://example.com"]})
|
|
147
|
+
# → validated BasicCrawlRequest ready for POST /v1/crawl
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 4. Architecture (modules)
|
|
153
|
+
|
|
154
|
+
```mermaid
|
|
155
|
+
flowchart TB
|
|
156
|
+
subgraph core_pkg["unique_search_proxy_core"]
|
|
157
|
+
Schema["schema.py"]
|
|
158
|
+
Errors["errors.py"]
|
|
159
|
+
MD["model_derivation/"]
|
|
160
|
+
PP["param_policy/"]
|
|
161
|
+
Proj["projection.py\n(search + LLM projection)"]
|
|
162
|
+
AProj["agent_engines/projection.py"]
|
|
163
|
+
CProj["crawlers/projection.py"]
|
|
164
|
+
Prov["providers/schema.py"]
|
|
165
|
+
SE["search_engines/"]
|
|
166
|
+
AE["agent_engines/"]
|
|
167
|
+
CR["crawlers/"]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
PP --> Proj
|
|
171
|
+
PP --> SE
|
|
172
|
+
MD --> Proj
|
|
173
|
+
MD --> AProj
|
|
174
|
+
MD --> CProj
|
|
175
|
+
Proj --> SE
|
|
176
|
+
AProj --> AE
|
|
177
|
+
CProj --> CR
|
|
178
|
+
Prov --> SE
|
|
179
|
+
Prov --> CR
|
|
180
|
+
SE --> Schema
|
|
181
|
+
AE --> Schema
|
|
182
|
+
CR --> Schema
|
|
183
|
+
Errors --> Schema
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Module | Responsibility |
|
|
187
|
+
|--------|----------------|
|
|
188
|
+
| `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
|
|
189
|
+
| `errors.py` | `ProxyError` hierarchy and stable `ProxyErrorCode` enum |
|
|
190
|
+
| `model_derivation/` | Shared `derive_request_model` + field/annotation helpers used by all three provider kinds |
|
|
191
|
+
| `param_policy/` | `ExposableParam` — used by **search engine** config models only |
|
|
192
|
+
| `projection.py` | Search-only: `build_request_model`, `build_llm_call_model`, `project_call_schema` |
|
|
193
|
+
| `agent_engines/projection.py` | Agent-only: `build_agent_request_model` (injects `query`; excludes `output_schema`) |
|
|
194
|
+
| `crawlers/projection.py` | Crawler-only: `build_crawl_request_model` (injects `urls`) |
|
|
195
|
+
| `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
|
|
196
|
+
| `search_engines/` | Config models, request union, `merge_config_and_invocation`, call-schema resolution |
|
|
197
|
+
| `agent_engines/` | Agent config/request models, output schema |
|
|
198
|
+
| `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` HTTP bodies |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 5. Provider contracts
|
|
203
|
+
|
|
204
|
+
Core registers the **discriminator ids** and config models. Runtime registration of service classes lives in the [client](../unique_search_proxy_client/README.md).
|
|
205
|
+
|
|
206
|
+
| Kind | IDs | Config model |
|
|
207
|
+
|------|-----|--------------|
|
|
208
|
+
| Search engines | `google`, `brave`, `perplexity` | `GoogleConfig`, `BraveConfig`, `PerplexityConfig` |
|
|
209
|
+
| Agent engines | `bing`, `vertexai` | `BingAgentConfig`, `VertexAIAgentConfig` |
|
|
210
|
+
| Crawlers | `Basic`, `Tavily`, `Jina`, `Firecrawl` | `BasicConfig`, `TavilyConfig`, … → `BasicCrawlRequest`, … |
|
|
211
|
+
|
|
212
|
+
Search engines share `BaseSearchEngineConfig` (`fetch_size`, `timeout`). Crawlers share `BaseCrawlerConfig` (`timeout` only — `urls` live on derived request models).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 6. Key APIs (by use case)
|
|
217
|
+
|
|
218
|
+
### Deployment UI — JSON Schema for a provider
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from unique_search_proxy_core.providers.schema import (
|
|
222
|
+
provider_config_json_schema,
|
|
223
|
+
provider_default_config,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
schema = provider_config_json_schema("search_engine", "google")
|
|
227
|
+
defaults = provider_default_config("search_engine", "google")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Tool manifest — LLM call schema
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from unique_search_proxy_core.search_engines.call_schema import resolve_search_call_schema
|
|
234
|
+
|
|
235
|
+
descriptor = resolve_search_call_schema("google", config=google_config, strict=False)
|
|
236
|
+
# descriptor.call_schema → JSON Schema for the LLM tool
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Runtime — build flat request before HTTP call
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from unique_search_proxy_core.search_engines import merge_config_and_invocation
|
|
243
|
+
|
|
244
|
+
body = merge_config_and_invocation(config, llm_invocation_dict)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Shared types and errors
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from unique_search_proxy_core import (
|
|
251
|
+
SearchResponse,
|
|
252
|
+
ProxyError,
|
|
253
|
+
EngineNotConfiguredError,
|
|
254
|
+
WebSearchResult,
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 7. Features summary
|
|
261
|
+
|
|
262
|
+
- Discriminated provider configs (`engine`, `crawler` Literal discriminators)
|
|
263
|
+
- **Search-only:** `ExposableParam` policy, three-surface projection, LLM call schema, `merge_config_and_invocation`
|
|
264
|
+
- **Agent:** config → request derivation via `model_derivation` + `build_agent_request_model`
|
|
265
|
+
- **Crawl:** `*Config` + `build_crawl_request_model` (injects `urls`); `merge_crawler_config_and_invocation`
|
|
266
|
+
- CamelCase JSON aliases on all models
|
|
267
|
+
- Zero server dependencies (import-linter enforced in the client package)
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 8. Installation & development
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
cd unique_search_proxy_core
|
|
275
|
+
uv sync
|
|
276
|
+
uv run pytest
|
|
277
|
+
uv run ruff check .
|
|
278
|
+
uv run basedpyright
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Consumers needing HTTP access should use [`unique-search-proxy-sdk`](../unique_search_proxy_sdk/README.md) rather than calling the proxy with raw httpx.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
Proprietary — Unique AG
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# unique-search-proxy-core
|
|
2
|
+
|
|
3
|
+
Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy-core`
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. What this package is
|
|
8
|
+
|
|
9
|
+
**Core is the contract layer.** It defines every shared type — deployment configs, HTTP request/response shapes, error codes, and LLM tool schemas — without importing FastAPI, httpx pools, or provider SDKs.
|
|
10
|
+
|
|
11
|
+
Install it anywhere you need to **describe** or **validate** proxy behaviour: the proxy server, the HTTP SDK, assistants-core tool manifests, deployment UIs.
|
|
12
|
+
|
|
13
|
+
| Package | Question it answers |
|
|
14
|
+
|---------|---------------------|
|
|
15
|
+
| **Core** (this) | *What* can be configured and *what* does a valid request/response look like? |
|
|
16
|
+
| [Client](../unique_search_proxy_client/README.md) | *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
|
+
Core sits at the centre of **Path A** (schema & config). It is imported by both the proxy pod and caller services; it never makes HTTP calls itself.
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
flowchart TB
|
|
27
|
+
subgraph consumers["Consumers of core"]
|
|
28
|
+
Client["unique_search_proxy_client"]
|
|
29
|
+
SDK["unique_search_proxy_sdk"]
|
|
30
|
+
AC["assistants-core / deployment UI"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
subgraph core["unique_search_proxy_core"]
|
|
34
|
+
Contracts["Response & error models"]
|
|
35
|
+
Config["*Config deployment models"]
|
|
36
|
+
Project["Schema projection & merge"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
AC --> Config
|
|
40
|
+
AC --> Project
|
|
41
|
+
Client --> Contracts
|
|
42
|
+
Client --> Config
|
|
43
|
+
SDK --> Contracts
|
|
44
|
+
Project -->|"flat request body"| SDK
|
|
45
|
+
Project -->|"flat request body"| Client
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
System overview → [../README.md](../README.md)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3. Key concepts
|
|
53
|
+
|
|
54
|
+
Core uses **three different config patterns** depending on provider kind. Only search engines use `ExposableParam` and the full projection pipeline.
|
|
55
|
+
|
|
56
|
+
### 3.1 Three provider patterns (search vs agent vs crawl)
|
|
57
|
+
|
|
58
|
+
| Kind | Config → request | `ExposableParam` | LLM call schema | Config/invocation merge |
|
|
59
|
+
|------|------------------|------------------|-----------------|-------------------------|
|
|
60
|
+
| **Search engines** | `GoogleConfig` → `GoogleSearchRequest` via `projection.build_request_model` | Yes | `build_llm_call_model` + `call_schema.resolve_*` | `merge_config_and_invocation` |
|
|
61
|
+
| **Agent engines** | `BingAgentConfig` → `BingAgentSearchRequest` via `agent_engines/projection.build_agent_request_model` | No (plain fields) | Not implemented in core | Not implemented in core |
|
|
62
|
+
| **Crawlers** | `BasicConfig` → `BasicCrawlRequest` via `crawlers/projection.build_crawl_request_model` | No | Not implemented in core | `merge_crawler_config_and_invocation` |
|
|
63
|
+
|
|
64
|
+
```mermaid
|
|
65
|
+
flowchart TB
|
|
66
|
+
subgraph search["Search engines only"]
|
|
67
|
+
GC["GoogleConfig\n(ExposableParam knobs)"]
|
|
68
|
+
Proj["projection.py\nbuild_request_model\nbuild_llm_call_model"]
|
|
69
|
+
GSR["GoogleSearchRequest"]
|
|
70
|
+
LLM["LLM call schema"]
|
|
71
|
+
GC --> Proj
|
|
72
|
+
Proj --> GSR
|
|
73
|
+
Proj --> LLM
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
subgraph agent["Agent engines"]
|
|
77
|
+
BC["BingAgentConfig\n(plain fields)"]
|
|
78
|
+
AProj["agent_engines/projection.py\nbuild_agent_request_model"]
|
|
79
|
+
BSR["BingAgentSearchRequest"]
|
|
80
|
+
BC --> AProj
|
|
81
|
+
AProj -->|"via model_derivation"| BSR
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
subgraph crawl["Crawlers"]
|
|
85
|
+
BCrawl["BasicConfig\n(no urls)"]
|
|
86
|
+
CProj["crawlers/projection.py\nbuild_crawl_request_model"]
|
|
87
|
+
BCR["BasicCrawlRequest\n(urls injected)"]
|
|
88
|
+
BCrawl --> CProj
|
|
89
|
+
CProj -->|"via model_derivation"| BCR
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3.2 Search engines — three surfaces from one config
|
|
94
|
+
|
|
95
|
+
Search is the only kind where a single `*Config` fans out into three derived surfaces:
|
|
96
|
+
|
|
97
|
+
| Surface | Audience | Example |
|
|
98
|
+
|---------|----------|---------|
|
|
99
|
+
| **Deployment config** | Admin / deployment UI | `{ "engine": "google", "gl": { "expose": true, "value": "de" }, … }` |
|
|
100
|
+
| **LLM call schema** | Tool manifest shown to the model | `{ "query", "gl" }` — only fields where `expose: true` |
|
|
101
|
+
| **HTTP request body** | `POST /v1/search` wire format | `{ "engine": "google", "query": "…", "gl": "de", "fetchSize": 10 }` |
|
|
102
|
+
|
|
103
|
+
`projection.py` and `search_engines/params.py` derive the latter two from the first. Agent and crawl use `model_derivation.derive_request_model` via per-kind projection modules to inject `query` or `urls`.
|
|
104
|
+
|
|
105
|
+
### 3.3 ExposableParam — search-engine only
|
|
106
|
+
|
|
107
|
+
Optional **search** parameters use `ExposableParam[T]`:
|
|
108
|
+
|
|
109
|
+
- **`value`** — admin default merged into every request (`null` = deactivated)
|
|
110
|
+
- **`expose`** — when `true`, the parameter appears on the LLM call schema
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
gl: ExposableParam[str | None] = ExposableParam(expose=False, value="de") # admin-fixed
|
|
114
|
+
gl: ExposableParam[str | None] = ExposableParam(expose=True, value=None) # LLM-overridable
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
No agent or crawler schema imports `ExposableParam` today.
|
|
118
|
+
|
|
119
|
+
### 3.4 merge_config_and_invocation — search-engine only
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from unique_search_proxy_core.search_engines import merge_config_and_invocation
|
|
123
|
+
|
|
124
|
+
request = merge_config_and_invocation(google_config, {"query": "EU AI Act"})
|
|
125
|
+
# → validated GoogleSearchRequest ready for POST /v1/search
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The proxy receives a flat body; it does not resolve deployment config over HTTP.
|
|
129
|
+
|
|
130
|
+
### 3.5 merge_crawler_config_and_invocation — crawler only
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from unique_search_proxy_core.crawlers import merge_crawler_config_and_invocation
|
|
134
|
+
|
|
135
|
+
request = merge_crawler_config_and_invocation(basic_config, {"urls": ["https://example.com"]})
|
|
136
|
+
# → validated BasicCrawlRequest ready for POST /v1/crawl
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 4. Architecture (modules)
|
|
142
|
+
|
|
143
|
+
```mermaid
|
|
144
|
+
flowchart TB
|
|
145
|
+
subgraph core_pkg["unique_search_proxy_core"]
|
|
146
|
+
Schema["schema.py"]
|
|
147
|
+
Errors["errors.py"]
|
|
148
|
+
MD["model_derivation/"]
|
|
149
|
+
PP["param_policy/"]
|
|
150
|
+
Proj["projection.py\n(search + LLM projection)"]
|
|
151
|
+
AProj["agent_engines/projection.py"]
|
|
152
|
+
CProj["crawlers/projection.py"]
|
|
153
|
+
Prov["providers/schema.py"]
|
|
154
|
+
SE["search_engines/"]
|
|
155
|
+
AE["agent_engines/"]
|
|
156
|
+
CR["crawlers/"]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
PP --> Proj
|
|
160
|
+
PP --> SE
|
|
161
|
+
MD --> Proj
|
|
162
|
+
MD --> AProj
|
|
163
|
+
MD --> CProj
|
|
164
|
+
Proj --> SE
|
|
165
|
+
AProj --> AE
|
|
166
|
+
CProj --> CR
|
|
167
|
+
Prov --> SE
|
|
168
|
+
Prov --> CR
|
|
169
|
+
SE --> Schema
|
|
170
|
+
AE --> Schema
|
|
171
|
+
CR --> Schema
|
|
172
|
+
Errors --> Schema
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Module | Responsibility |
|
|
176
|
+
|--------|----------------|
|
|
177
|
+
| `schema.py` | Shared API models: `SearchResponse`, `AgentSearchResponse`, `CrawlResponse`, `WebSearchResult`, `ErrorResponse`, SSE events |
|
|
178
|
+
| `errors.py` | `ProxyError` hierarchy and stable `ProxyErrorCode` enum |
|
|
179
|
+
| `model_derivation/` | Shared `derive_request_model` + field/annotation helpers used by all three provider kinds |
|
|
180
|
+
| `param_policy/` | `ExposableParam` — used by **search engine** config models only |
|
|
181
|
+
| `projection.py` | Search-only: `build_request_model`, `build_llm_call_model`, `project_call_schema` |
|
|
182
|
+
| `agent_engines/projection.py` | Agent-only: `build_agent_request_model` (injects `query`; excludes `output_schema`) |
|
|
183
|
+
| `crawlers/projection.py` | Crawler-only: `build_crawl_request_model` (injects `urls`) |
|
|
184
|
+
| `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
|
|
185
|
+
| `search_engines/` | Config models, request union, `merge_config_and_invocation`, call-schema resolution |
|
|
186
|
+
| `agent_engines/` | Agent config/request models, output schema |
|
|
187
|
+
| `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` HTTP bodies |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 5. Provider contracts
|
|
192
|
+
|
|
193
|
+
Core registers the **discriminator ids** and config models. Runtime registration of service classes lives in the [client](../unique_search_proxy_client/README.md).
|
|
194
|
+
|
|
195
|
+
| Kind | IDs | Config model |
|
|
196
|
+
|------|-----|--------------|
|
|
197
|
+
| Search engines | `google`, `brave`, `perplexity` | `GoogleConfig`, `BraveConfig`, `PerplexityConfig` |
|
|
198
|
+
| Agent engines | `bing`, `vertexai` | `BingAgentConfig`, `VertexAIAgentConfig` |
|
|
199
|
+
| Crawlers | `Basic`, `Tavily`, `Jina`, `Firecrawl` | `BasicConfig`, `TavilyConfig`, … → `BasicCrawlRequest`, … |
|
|
200
|
+
|
|
201
|
+
Search engines share `BaseSearchEngineConfig` (`fetch_size`, `timeout`). Crawlers share `BaseCrawlerConfig` (`timeout` only — `urls` live on derived request models).
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 6. Key APIs (by use case)
|
|
206
|
+
|
|
207
|
+
### Deployment UI — JSON Schema for a provider
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from unique_search_proxy_core.providers.schema import (
|
|
211
|
+
provider_config_json_schema,
|
|
212
|
+
provider_default_config,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
schema = provider_config_json_schema("search_engine", "google")
|
|
216
|
+
defaults = provider_default_config("search_engine", "google")
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Tool manifest — LLM call schema
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from unique_search_proxy_core.search_engines.call_schema import resolve_search_call_schema
|
|
223
|
+
|
|
224
|
+
descriptor = resolve_search_call_schema("google", config=google_config, strict=False)
|
|
225
|
+
# descriptor.call_schema → JSON Schema for the LLM tool
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Runtime — build flat request before HTTP call
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from unique_search_proxy_core.search_engines import merge_config_and_invocation
|
|
232
|
+
|
|
233
|
+
body = merge_config_and_invocation(config, llm_invocation_dict)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Shared types and errors
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from unique_search_proxy_core import (
|
|
240
|
+
SearchResponse,
|
|
241
|
+
ProxyError,
|
|
242
|
+
EngineNotConfiguredError,
|
|
243
|
+
WebSearchResult,
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 7. Features summary
|
|
250
|
+
|
|
251
|
+
- Discriminated provider configs (`engine`, `crawler` Literal discriminators)
|
|
252
|
+
- **Search-only:** `ExposableParam` policy, three-surface projection, LLM call schema, `merge_config_and_invocation`
|
|
253
|
+
- **Agent:** config → request derivation via `model_derivation` + `build_agent_request_model`
|
|
254
|
+
- **Crawl:** `*Config` + `build_crawl_request_model` (injects `urls`); `merge_crawler_config_and_invocation`
|
|
255
|
+
- CamelCase JSON aliases on all models
|
|
256
|
+
- Zero server dependencies (import-linter enforced in the client package)
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 8. Installation & development
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
cd unique_search_proxy_core
|
|
264
|
+
uv sync
|
|
265
|
+
uv run pytest
|
|
266
|
+
uv run ruff check .
|
|
267
|
+
uv run basedpyright
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Consumers needing HTTP access should use [`unique-search-proxy-sdk`](../unique_search_proxy_sdk/README.md) rather than calling the proxy with raw httpx.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
Proprietary — Unique AG
|
{unique_search_proxy_core-2026.26.0.dev4 → unique_search_proxy_core-2026.26.0.dev5}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy-core"
|
|
3
|
-
version = "2026.26.0.
|
|
3
|
+
version = "2026.26.0.dev5"
|
|
4
4
|
description = "Shared Pydantic types for the Unique Search Proxy API"
|
|
5
5
|
authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from unique_search_proxy_core.agent_engines.base import (
|
|
2
|
+
AgentEngineType,
|
|
3
|
+
AgentSearchEngine,
|
|
4
|
+
BaseAgentEngineConfig,
|
|
5
|
+
)
|
|
6
|
+
from unique_search_proxy_core.agent_engines.config_types import (
|
|
7
|
+
AgentSearchRequest,
|
|
8
|
+
parse_agent_search_request,
|
|
9
|
+
)
|
|
10
|
+
from unique_search_proxy_core.agent_engines.response_parsing import (
|
|
11
|
+
JsonConversionStrategy,
|
|
12
|
+
LLMParserStrategy,
|
|
13
|
+
ResponseParser,
|
|
14
|
+
convert_response_to_search_results,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AgentEngineType",
|
|
19
|
+
"AgentSearchEngine",
|
|
20
|
+
"AgentSearchRequest",
|
|
21
|
+
"BaseAgentEngineConfig",
|
|
22
|
+
"JsonConversionStrategy",
|
|
23
|
+
"LLMParserStrategy",
|
|
24
|
+
"ResponseParser",
|
|
25
|
+
"convert_response_to_search_results",
|
|
26
|
+
"parse_agent_search_request",
|
|
27
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import TYPE_CHECKING, AsyncIterator, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
9
|
+
|
|
10
|
+
from unique_search_proxy_core.agent_engines.output_schema import AgentSearchOutput
|
|
11
|
+
from unique_search_proxy_core.schema import (
|
|
12
|
+
AgentSearchResponse,
|
|
13
|
+
AgentSearchStreamEvent,
|
|
14
|
+
camelized_model_config,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from httpx import AsyncClient
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T", bound="AgentEngineType")
|
|
21
|
+
AgentRequestT = TypeVar("AgentRequestT", bound=BaseModel)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Default agent generation instructions. Kept aligned with
|
|
25
|
+
# unique_web_search.services.search_engine.utils.grounding.models.GENERATION_INSTRUCTIONS
|
|
26
|
+
DEFAULT_GENERATION_INSTRUCTIONS = """You are an Expert Web Research Agent whose goal is to extract the MAXIMUM amount of detail from every source you find.
|
|
27
|
+
|
|
28
|
+
## Core Directives
|
|
29
|
+
1. **Search broadly** — issue multiple searches with varied keywords and phrasings to cover every angle of the query.
|
|
30
|
+
2. **Read every source thoroughly** — do NOT skim. Extract every relevant fact, figure, statistic, date, name, quote, and piece of context.
|
|
31
|
+
3. **One entry per source** — each source gets its own result object. Never merge information from different sources into a single entry.
|
|
32
|
+
4. **Preserve detail** — prefer verbosity over brevity. Include specific numbers, full names, exact dates, and direct quotes whenever available. Do NOT paraphrase away precision.
|
|
33
|
+
5. **No omissions** — if a source contains relevant information, it MUST appear in your output. When in doubt, include it.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentEngineType(StrEnum):
|
|
38
|
+
"""Registered agent search engine ids (JSON discriminator values)."""
|
|
39
|
+
|
|
40
|
+
BING = "bing"
|
|
41
|
+
VERTEXAI = "vertexai"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BaseAgentEngineConfig(BaseModel, Generic[T]):
|
|
45
|
+
"""Shared agent-engine config; each engine narrows ``engine`` with a Literal."""
|
|
46
|
+
|
|
47
|
+
model_config = camelized_model_config
|
|
48
|
+
|
|
49
|
+
engine: T
|
|
50
|
+
generation_instructions: str = Field(
|
|
51
|
+
default=DEFAULT_GENERATION_INSTRUCTIONS,
|
|
52
|
+
description="Instructions injected into the grounding agent.",
|
|
53
|
+
)
|
|
54
|
+
output_schema: SkipJsonSchema[type[BaseModel]] = Field(
|
|
55
|
+
default=AgentSearchOutput,
|
|
56
|
+
exclude=True,
|
|
57
|
+
description=(
|
|
58
|
+
"Pydantic model defining the structured JSON shape the agent must return."
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
timeout: int = Field(
|
|
62
|
+
default=120,
|
|
63
|
+
ge=1,
|
|
64
|
+
le=600,
|
|
65
|
+
description="Request timeout in seconds (agent runs can be slow).",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AgentSearchEngine(ABC, Generic[AgentRequestT]):
|
|
70
|
+
"""Agent (grounded) search engine contract for v1 providers.
|
|
71
|
+
|
|
72
|
+
Implementations call the upstream agent/grounding provider and return opaque
|
|
73
|
+
``answer`` text plus ``raw`` provider payload. Content resolution is left to
|
|
74
|
+
consumers; this layer stays provider-egress only.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
engine_id: str
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
http_client: AsyncClient | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._http_client = http_client
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def mode(self) -> str:
|
|
89
|
+
"""Provider mode identifier for observability (always ``agent``)."""
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
async def search(self, request: AgentRequestT) -> AgentSearchResponse:
|
|
93
|
+
"""Run a grounded search and return the assembled response."""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def stream(
|
|
97
|
+
self,
|
|
98
|
+
request: AgentRequestT,
|
|
99
|
+
) -> AsyncIterator[AgentSearchStreamEvent]:
|
|
100
|
+
"""Yield incremental events, terminating with an ``AgentSearchDone``."""
|