unique-search-proxy-core 2026.30.0.dev2__tar.gz → 2026.30.0.dev4__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.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/PKG-INFO +5 -14
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/README.md +3 -12
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/pyproject.toml +2 -2
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/__init__.py +0 -2
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/base.py +7 -2
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/schema.py +9 -2
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/firecrawl/schema.py +31 -9
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/jina/schema.py +34 -12
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/tavily/schema.py +17 -6
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/request_base.py +3 -10
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/schema.py +0 -9
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/redirect.py +12 -1
- unique_search_proxy_core-2026.30.0.dev2/unique_search_proxy_core/crawlers/params.py +0 -49
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/base.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/bing/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/bing/schema.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/config_types.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/output_schema.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/resolve.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/response_parsing.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/vertexai/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/agent_engines/vertexai/schema.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/crawlers/config_types.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/errors.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/logging.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/annotations.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/derive.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/exposable_param.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/param_policy/exposed_params.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/providers/schema.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/base.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/brave/schema.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/config_types.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/google/schema.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/search_engines/perplexity/schema.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/__init__.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/dns.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/egress.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/models.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/netloc.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/policy.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/resolver.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/service.py +0 -0
- {unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/unique_search_proxy_core/url_safety/settings.py +0 -0
{unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-search-proxy-core
|
|
3
|
-
Version: 2026.30.0.
|
|
3
|
+
Version: 2026.30.0.dev4
|
|
4
4
|
Summary: Shared Pydantic types for the Unique Search Proxy API
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
@@ -8,7 +8,7 @@ Requires-Dist: httpx>=0.28.0,<0.29
|
|
|
8
8
|
Requires-Dist: pydantic>=2.12.5,<3.0.0
|
|
9
9
|
Requires-Dist: pydantic-settings>=2.10.1,<3
|
|
10
10
|
Requires-Dist: pyhumps>=3.8.0,<4
|
|
11
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.
|
|
11
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.30.0.dev15,<2026.30.0rc0
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
@@ -82,7 +82,7 @@ Admin JSON → GoogleConfig {expose, value}
|
|
|
82
82
|
| `merge(overrides, *, query)` (instance) | search only | deployment defaults + LLM overrides + query → validated `request_model()` instance |
|
|
83
83
|
| `provider_query_params(request)` (classmethod) | search only | request serialized for the upstream provider, minus `_provider_param_exclude_fields` (Google adds `search_engine_id`) |
|
|
84
84
|
|
|
85
|
-
Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()
|
|
85
|
+
Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`. Crawlers have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
|
|
86
86
|
|
|
87
87
|
### 3.2 ExposableParam — search-engine only
|
|
88
88
|
|
|
@@ -121,15 +121,6 @@ request = google_config.merge({"gl": "de"}, query="EU AI Act")
|
|
|
121
121
|
|
|
122
122
|
Deactivated knobs (`value=None`) are dropped, overrides win over admin defaults, `engine` always comes from the config. The proxy receives a flat body; it does not resolve deployment config over HTTP.
|
|
123
123
|
|
|
124
|
-
### 3.5 merge_crawler_config_and_invocation — crawler only
|
|
125
|
-
|
|
126
|
-
```python
|
|
127
|
-
from unique_search_proxy_core.crawlers import merge_crawler_config_and_invocation
|
|
128
|
-
|
|
129
|
-
request = merge_crawler_config_and_invocation(basic_config, {"urls": ["https://example.com"]})
|
|
130
|
-
# → validated BasicCrawlRequest ready for POST /v1/crawl
|
|
131
|
-
```
|
|
132
|
-
|
|
133
124
|
---
|
|
134
125
|
|
|
135
126
|
## 4. Architecture (modules)
|
|
@@ -169,7 +160,7 @@ flowchart TB
|
|
|
169
160
|
| `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
|
|
170
161
|
| `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
|
|
171
162
|
| `agent_engines/` | Agent config/request models (`request_model`), output schema |
|
|
172
|
-
| `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies
|
|
163
|
+
| `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies (`request_model()`) |
|
|
173
164
|
|
|
174
165
|
---
|
|
175
166
|
|
|
@@ -234,7 +225,7 @@ from unique_search_proxy_core import (
|
|
|
234
225
|
- Discriminated provider configs (`engine`, `crawler` Literal discriminators)
|
|
235
226
|
- **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
|
|
236
227
|
- **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
|
|
237
|
-
- **Crawl:** `BasicConfig.request_model()` (injects `urls`);
|
|
228
|
+
- **Crawl:** `BasicConfig.request_model()` (injects `urls`); no exposable params / no merge
|
|
238
229
|
- CamelCase JSON aliases on all models
|
|
239
230
|
- Zero server dependencies (import-linter enforced in the client package)
|
|
240
231
|
|
{unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/README.md
RENAMED
|
@@ -68,7 +68,7 @@ Admin JSON → GoogleConfig {expose, value}
|
|
|
68
68
|
| `merge(overrides, *, query)` (instance) | search only | deployment defaults + LLM overrides + query → validated `request_model()` instance |
|
|
69
69
|
| `provider_query_params(request)` (classmethod) | search only | request serialized for the upstream provider, minus `_provider_param_exclude_fields` (Google adds `search_engine_id`) |
|
|
70
70
|
|
|
71
|
-
Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()
|
|
71
|
+
Agent engines (`BingAgentConfig`, …) and crawlers (`BasicConfig`, …) only implement `request_model()`. Crawlers have no exposable params and no `merge` — a crawl request is simply the deployment config fields plus `urls`.
|
|
72
72
|
|
|
73
73
|
### 3.2 ExposableParam — search-engine only
|
|
74
74
|
|
|
@@ -107,15 +107,6 @@ request = google_config.merge({"gl": "de"}, query="EU AI Act")
|
|
|
107
107
|
|
|
108
108
|
Deactivated knobs (`value=None`) are dropped, overrides win over admin defaults, `engine` always comes from the config. The proxy receives a flat body; it does not resolve deployment config over HTTP.
|
|
109
109
|
|
|
110
|
-
### 3.5 merge_crawler_config_and_invocation — crawler only
|
|
111
|
-
|
|
112
|
-
```python
|
|
113
|
-
from unique_search_proxy_core.crawlers import merge_crawler_config_and_invocation
|
|
114
|
-
|
|
115
|
-
request = merge_crawler_config_and_invocation(basic_config, {"urls": ["https://example.com"]})
|
|
116
|
-
# → validated BasicCrawlRequest ready for POST /v1/crawl
|
|
117
|
-
```
|
|
118
|
-
|
|
119
110
|
---
|
|
120
111
|
|
|
121
112
|
## 4. Architecture (modules)
|
|
@@ -155,7 +146,7 @@ flowchart TB
|
|
|
155
146
|
| `providers/schema.py` | JSON Schema + defaults for deployment UIs (`provider_config_json_schema`, …) |
|
|
156
147
|
| `search_engines/` | Config models with the config-owned API (`request_model` / `exposed_params_model` / `merge` / `provider_query_params`), request union |
|
|
157
148
|
| `agent_engines/` | Agent config/request models (`request_model`), output schema |
|
|
158
|
-
| `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies
|
|
149
|
+
| `crawlers/` | `*Config` deployment models + derived `*CrawlRequest` bodies (`request_model()`) |
|
|
159
150
|
|
|
160
151
|
---
|
|
161
152
|
|
|
@@ -220,7 +211,7 @@ from unique_search_proxy_core import (
|
|
|
220
211
|
- Discriminated provider configs (`engine`, `crawler` Literal discriminators)
|
|
221
212
|
- **Search-only:** `ExposableParam` policy; config-owned `request_model` / `exposed_params_model` / `merge` / `provider_query_params`
|
|
222
213
|
- **Agent:** `BingAgentConfig.request_model()` (injects `query`; excludes `output_schema`)
|
|
223
|
-
- **Crawl:** `BasicConfig.request_model()` (injects `urls`);
|
|
214
|
+
- **Crawl:** `BasicConfig.request_model()` (injects `urls`); no exposable params / no merge
|
|
224
215
|
- CamelCase JSON aliases on all models
|
|
225
216
|
- Zero server dependencies (import-linter enforced in the client package)
|
|
226
217
|
|
{unique_search_proxy_core-2026.30.0.dev2 → unique_search_proxy_core-2026.30.0.dev4}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy-core"
|
|
3
|
-
version = "2026.30.0.
|
|
3
|
+
version = "2026.30.0.dev4"
|
|
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"
|
|
@@ -10,7 +10,7 @@ dependencies = [
|
|
|
10
10
|
"pydantic>=2.12.5,<3.0.0",
|
|
11
11
|
"pydantic-settings>=2.10.1,<3",
|
|
12
12
|
"pyhumps>=3.8.0,<4",
|
|
13
|
-
"unique-toolkit[monitoring]>=2026.30.0.
|
|
13
|
+
"unique-toolkit[monitoring]>=2026.30.0.dev15,<2026.30.0rc0",
|
|
14
14
|
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -17,7 +17,6 @@ from unique_search_proxy_core.crawlers.config_types import (
|
|
|
17
17
|
parse_crawl_request,
|
|
18
18
|
parse_crawler_config,
|
|
19
19
|
)
|
|
20
|
-
from unique_search_proxy_core.crawlers.params import merge_crawler_config_and_invocation
|
|
21
20
|
|
|
22
21
|
__all__ = [
|
|
23
22
|
"BaseCrawler",
|
|
@@ -31,7 +30,6 @@ __all__ = [
|
|
|
31
30
|
"CrawlerType",
|
|
32
31
|
"build_crawl_request_union",
|
|
33
32
|
"crawler_config_from_request",
|
|
34
|
-
"merge_crawler_config_and_invocation",
|
|
35
33
|
"parse_crawl_request",
|
|
36
34
|
"parse_crawler_config",
|
|
37
35
|
]
|
|
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
|
|
8
8
|
|
|
9
9
|
from unique_search_proxy_core.param_policy.derive import derive_request_model
|
|
10
10
|
from unique_search_proxy_core.param_policy.request_base import CrawlRequestBase
|
|
11
|
-
from unique_search_proxy_core.schema import CrawlUrlResult,
|
|
11
|
+
from unique_search_proxy_core.schema import CrawlUrlResult, camelized_model_config
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from httpx import AsyncClient
|
|
@@ -29,7 +29,12 @@ class CrawlerType(StrEnum):
|
|
|
29
29
|
class BaseCrawlerConfig(BaseModel, Generic[CrawlerTypeT]):
|
|
30
30
|
"""Deployment config for a crawler; each crawler narrows ``crawler`` with a Literal."""
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# Mirrors ``BaseSearchEngineConfig``: no ``extra='forbid'``. Emitting
|
|
33
|
+
# ``additionalProperties: false`` on every branch of the ``crawlerConfig``
|
|
34
|
+
# discriminated ``oneOf`` breaks the RJSF admin form, which carries sibling
|
|
35
|
+
# default fields across branches and then matches zero subschemas. Runtime
|
|
36
|
+
# ``/v1/crawl`` request bodies stay strict via ``CrawlRequestBase``.
|
|
37
|
+
model_config = camelized_model_config
|
|
33
38
|
|
|
34
39
|
#: Name of the derived request model; set by every concrete crawler config.
|
|
35
40
|
_request_model_name: ClassVar[str]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import ClassVar, Literal
|
|
3
|
+
from typing import Annotated, ClassVar, Literal
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
|
+
from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
|
|
6
7
|
|
|
7
8
|
from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
|
|
8
9
|
from unique_search_proxy_core.crawlers.basic.content_types import ContentTypeToggles
|
|
@@ -13,7 +14,13 @@ class BasicConfig(BaseCrawlerConfig[CrawlerType.BASIC]):
|
|
|
13
14
|
|
|
14
15
|
_request_model_name: ClassVar[str] = "BasicCrawlRequest"
|
|
15
16
|
|
|
16
|
-
crawler:
|
|
17
|
+
crawler: Annotated[
|
|
18
|
+
Literal[CrawlerType.BASIC], RJSFMetaTag.SpecialWidget.hidden()
|
|
19
|
+
] = Field(
|
|
20
|
+
default=CrawlerType.BASIC,
|
|
21
|
+
title="Crawler",
|
|
22
|
+
description="Provider discriminator; must be `Basic` for this config.",
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
content_types: ContentTypeToggles = Field(
|
|
19
26
|
default_factory=ContentTypeToggles,
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import ClassVar, Literal
|
|
3
|
+
from typing import Annotated, ClassVar, Literal, TypeAlias
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
|
+
from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
|
|
6
7
|
|
|
7
8
|
from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
|
|
9
|
+
from unique_search_proxy_core.schema import DeactivatedNone
|
|
8
10
|
|
|
9
11
|
FirecrawlProxyMode = Literal["basic", "enhanced", "auto"]
|
|
10
12
|
|
|
13
|
+
MaxConcurrencyOrNone: TypeAlias = (
|
|
14
|
+
Annotated[int, Field(title="Integer", ge=1)] | DeactivatedNone
|
|
15
|
+
)
|
|
16
|
+
TagListOrNone: TypeAlias = (
|
|
17
|
+
Annotated[
|
|
18
|
+
list[str],
|
|
19
|
+
Field(title="Tags"),
|
|
20
|
+
RJSFMetaTag({"ui:options": {"orderable": False}}),
|
|
21
|
+
]
|
|
22
|
+
| DeactivatedNone
|
|
23
|
+
)
|
|
24
|
+
ScrapeHeadersOrNone: TypeAlias = (
|
|
25
|
+
Annotated[dict[str, str], Field(title="Headers")] | DeactivatedNone
|
|
26
|
+
)
|
|
27
|
+
MaxAgeOrNone: TypeAlias = Annotated[int, Field(title="Integer", ge=0)] | DeactivatedNone
|
|
28
|
+
|
|
11
29
|
|
|
12
30
|
class FirecrawlConfig(BaseCrawlerConfig[CrawlerType.FIRECRAWL]):
|
|
13
31
|
"""Deployment config for Firecrawl (scrape or batch scrape)."""
|
|
14
32
|
|
|
15
33
|
_request_model_name: ClassVar[str] = "FirecrawlCrawlRequest"
|
|
16
34
|
|
|
17
|
-
crawler:
|
|
35
|
+
crawler: Annotated[
|
|
36
|
+
Literal[CrawlerType.FIRECRAWL], RJSFMetaTag.SpecialWidget.hidden()
|
|
37
|
+
] = Field(
|
|
38
|
+
default=CrawlerType.FIRECRAWL,
|
|
39
|
+
title="Crawler",
|
|
40
|
+
description="Provider discriminator; must be `Firecrawl` for this config.",
|
|
41
|
+
)
|
|
18
42
|
|
|
19
43
|
only_main_content: bool = Field(
|
|
20
44
|
default=True,
|
|
@@ -26,9 +50,8 @@ class FirecrawlConfig(BaseCrawlerConfig[CrawlerType.FIRECRAWL]):
|
|
|
26
50
|
title="Only clean content",
|
|
27
51
|
description="LLM pass to remove residual boilerplate from markdown.",
|
|
28
52
|
)
|
|
29
|
-
max_concurrency:
|
|
53
|
+
max_concurrency: MaxConcurrencyOrNone = Field(
|
|
30
54
|
default=None,
|
|
31
|
-
ge=1,
|
|
32
55
|
title="Max concurrency",
|
|
33
56
|
description="Maximum concurrent scrapes for this batch job.",
|
|
34
57
|
)
|
|
@@ -63,24 +86,23 @@ class FirecrawlConfig(BaseCrawlerConfig[CrawlerType.FIRECRAWL]):
|
|
|
63
86
|
title="Proxy mode",
|
|
64
87
|
description="Firecrawl proxy tier: `basic`, `enhanced`, or `auto`.",
|
|
65
88
|
)
|
|
66
|
-
include_tags:
|
|
89
|
+
include_tags: TagListOrNone = Field(
|
|
67
90
|
default=None,
|
|
68
91
|
title="Include tags",
|
|
69
92
|
description="HTML tags to include in scrape output.",
|
|
70
93
|
)
|
|
71
|
-
exclude_tags:
|
|
94
|
+
exclude_tags: TagListOrNone = Field(
|
|
72
95
|
default=None,
|
|
73
96
|
title="Exclude tags",
|
|
74
97
|
description="HTML tags to exclude from scrape output.",
|
|
75
98
|
)
|
|
76
|
-
scrape_headers:
|
|
99
|
+
scrape_headers: ScrapeHeadersOrNone = Field(
|
|
77
100
|
default=None,
|
|
78
101
|
title="Scrape headers",
|
|
79
102
|
description="Headers sent to the target page (cookies, user-agent, etc.).",
|
|
80
103
|
)
|
|
81
|
-
max_age:
|
|
104
|
+
max_age: MaxAgeOrNone = Field(
|
|
82
105
|
default=None,
|
|
83
|
-
ge=0,
|
|
84
106
|
title="Max age",
|
|
85
107
|
description=(
|
|
86
108
|
"Return cached page content younger than this age in milliseconds."
|
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import ClassVar, Literal
|
|
3
|
+
from typing import Annotated, ClassVar, Literal, TypeAlias
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
|
+
from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
|
|
6
7
|
|
|
7
8
|
from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
|
|
9
|
+
from unique_search_proxy_core.schema import DeactivatedNone
|
|
8
10
|
|
|
9
11
|
JinaReturnFormat = Literal["markdown", "html", "text", "screenshot", "pageshot"]
|
|
10
12
|
JinaEngine = Literal["auto", "browser", "direct", "cf-browser-rendering"]
|
|
11
13
|
JinaRetainImages = Literal["none", "all", "alt", "all_p", "alt_p"]
|
|
12
14
|
|
|
15
|
+
StrOrNone: TypeAlias = Annotated[str, Field(title="String")] | DeactivatedNone
|
|
16
|
+
PageTimeoutOrNone: TypeAlias = (
|
|
17
|
+
Annotated[int, Field(title="Integer", ge=1, le=180)] | DeactivatedNone
|
|
18
|
+
)
|
|
19
|
+
CssSelectorListOrNone: TypeAlias = (
|
|
20
|
+
Annotated[
|
|
21
|
+
list[str],
|
|
22
|
+
Field(title="CSS Selectors"),
|
|
23
|
+
RJSFMetaTag({"ui:options": {"orderable": False}}),
|
|
24
|
+
]
|
|
25
|
+
| DeactivatedNone
|
|
26
|
+
)
|
|
27
|
+
RetainImagesOrNone: TypeAlias = (
|
|
28
|
+
Annotated[JinaRetainImages, Field(title="Retain Images")] | DeactivatedNone
|
|
29
|
+
)
|
|
30
|
+
|
|
13
31
|
|
|
14
32
|
class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
|
|
15
33
|
"""Deployment config for the Jina Reader crawler."""
|
|
16
34
|
|
|
17
35
|
_request_model_name: ClassVar[str] = "JinaCrawlRequest"
|
|
18
36
|
|
|
19
|
-
crawler:
|
|
37
|
+
crawler: Annotated[
|
|
38
|
+
Literal[CrawlerType.JINA], RJSFMetaTag.SpecialWidget.hidden()
|
|
39
|
+
] = Field(
|
|
40
|
+
default=CrawlerType.JINA,
|
|
41
|
+
title="Crawler",
|
|
42
|
+
description="Provider discriminator; must be `Jina` for this config.",
|
|
43
|
+
)
|
|
20
44
|
|
|
21
45
|
return_format: JinaReturnFormat = Field(
|
|
22
46
|
default="markdown",
|
|
@@ -34,10 +58,8 @@ class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
|
|
|
34
58
|
"`cf-browser-rendering` for JS-heavy sites."
|
|
35
59
|
),
|
|
36
60
|
)
|
|
37
|
-
page_timeout:
|
|
61
|
+
page_timeout: PageTimeoutOrNone = Field(
|
|
38
62
|
default=None,
|
|
39
|
-
ge=1,
|
|
40
|
-
le=180,
|
|
41
63
|
title="Page load timeout",
|
|
42
64
|
description=(
|
|
43
65
|
"Max seconds to wait for the page to load (Jina Reader). "
|
|
@@ -56,17 +78,17 @@ class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
|
|
|
56
78
|
title="Bypass cache",
|
|
57
79
|
description="Bypass Jina cache and fetch fresh content.",
|
|
58
80
|
)
|
|
59
|
-
target_selector:
|
|
81
|
+
target_selector: CssSelectorListOrNone = Field(
|
|
60
82
|
default=None,
|
|
61
83
|
title="Target selector",
|
|
62
84
|
description="CSS selectors to focus extraction on specific page elements.",
|
|
63
85
|
)
|
|
64
|
-
wait_for_selector:
|
|
86
|
+
wait_for_selector: CssSelectorListOrNone = Field(
|
|
65
87
|
default=None,
|
|
66
88
|
title="Wait for selector",
|
|
67
89
|
description="CSS selectors to wait for before returning content.",
|
|
68
90
|
)
|
|
69
|
-
remove_selector:
|
|
91
|
+
remove_selector: CssSelectorListOrNone = Field(
|
|
70
92
|
default=None,
|
|
71
93
|
title="Remove selector",
|
|
72
94
|
description="CSS selectors for page regions to strip (headers, footers, etc.).",
|
|
@@ -91,22 +113,22 @@ class JinaConfig(BaseCrawlerConfig[CrawlerType.JINA]):
|
|
|
91
113
|
title="Include iframes",
|
|
92
114
|
description="Include iframe content in the reader response.",
|
|
93
115
|
)
|
|
94
|
-
retain_images:
|
|
116
|
+
retain_images: RetainImagesOrNone = Field(
|
|
95
117
|
default=None,
|
|
96
118
|
title="Retain images",
|
|
97
119
|
description="Control which images are retained in markdown output.",
|
|
98
120
|
)
|
|
99
|
-
locale:
|
|
121
|
+
locale: StrOrNone = Field(
|
|
100
122
|
default=None,
|
|
101
123
|
title="Locale",
|
|
102
124
|
description="Browser locale used to render the page (e.g. `en-US`).",
|
|
103
125
|
)
|
|
104
|
-
referer:
|
|
126
|
+
referer: StrOrNone = Field(
|
|
105
127
|
default=None,
|
|
106
128
|
title="Referer",
|
|
107
129
|
description="Referer header sent when fetching the target URL.",
|
|
108
130
|
)
|
|
109
|
-
proxy_url:
|
|
131
|
+
proxy_url: StrOrNone = Field(
|
|
110
132
|
default=None,
|
|
111
133
|
title="Proxy URL",
|
|
112
134
|
description="Proxy URL used by Jina Reader to access the target page.",
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import ClassVar, Literal
|
|
3
|
+
from typing import Annotated, ClassVar, Literal, TypeAlias
|
|
4
4
|
|
|
5
5
|
from pydantic import Field, model_validator
|
|
6
|
+
from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
|
|
6
7
|
|
|
7
8
|
from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig, CrawlerType
|
|
9
|
+
from unique_search_proxy_core.schema import DeactivatedNone
|
|
8
10
|
|
|
9
11
|
TavilyExtractDepth = Literal["basic", "advanced"]
|
|
10
12
|
TavilyExtractFormat = Literal["markdown", "text"]
|
|
11
13
|
|
|
14
|
+
StrOrNone: TypeAlias = Annotated[str, Field(title="String")] | DeactivatedNone
|
|
15
|
+
ChunksPerSourceOrNone: TypeAlias = (
|
|
16
|
+
Annotated[int, Field(title="Integer", ge=1, le=5)] | DeactivatedNone
|
|
17
|
+
)
|
|
18
|
+
|
|
12
19
|
|
|
13
20
|
class TavilyConfig(BaseCrawlerConfig[CrawlerType.TAVILY]):
|
|
14
21
|
"""Deployment config for the Tavily Extract crawler."""
|
|
15
22
|
|
|
16
23
|
_request_model_name: ClassVar[str] = "TavilyCrawlRequest"
|
|
17
24
|
|
|
18
|
-
crawler:
|
|
25
|
+
crawler: Annotated[
|
|
26
|
+
Literal[CrawlerType.TAVILY], RJSFMetaTag.SpecialWidget.hidden()
|
|
27
|
+
] = Field(
|
|
28
|
+
default=CrawlerType.TAVILY,
|
|
29
|
+
title="Crawler",
|
|
30
|
+
description="Provider discriminator; must be `Tavily` for this config.",
|
|
31
|
+
)
|
|
19
32
|
|
|
20
33
|
extract_depth: TavilyExtractDepth = Field(
|
|
21
34
|
default="advanced",
|
|
@@ -30,7 +43,7 @@ class TavilyConfig(BaseCrawlerConfig[CrawlerType.TAVILY]):
|
|
|
30
43
|
title="Output format",
|
|
31
44
|
description="Extracted content format: `markdown` or `text`.",
|
|
32
45
|
)
|
|
33
|
-
query:
|
|
46
|
+
query: StrOrNone = Field(
|
|
34
47
|
default=None,
|
|
35
48
|
title="Rerank query",
|
|
36
49
|
description=(
|
|
@@ -38,10 +51,8 @@ class TavilyConfig(BaseCrawlerConfig[CrawlerType.TAVILY]):
|
|
|
38
51
|
"`chunks_per_source` may be used."
|
|
39
52
|
),
|
|
40
53
|
)
|
|
41
|
-
chunks_per_source:
|
|
54
|
+
chunks_per_source: ChunksPerSourceOrNone = Field(
|
|
42
55
|
default=None,
|
|
43
|
-
ge=1,
|
|
44
|
-
le=5,
|
|
45
56
|
title="Chunks per source",
|
|
46
57
|
description=(
|
|
47
58
|
"Max relevant chunks per URL when `query` is set (1–5). "
|
|
@@ -9,10 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel, Field
|
|
11
11
|
|
|
12
|
-
from unique_search_proxy_core.schema import
|
|
13
|
-
camelized_model_config,
|
|
14
|
-
deployment_model_config,
|
|
15
|
-
)
|
|
12
|
+
from unique_search_proxy_core.schema import camelized_model_config
|
|
16
13
|
|
|
17
14
|
|
|
18
15
|
class SearchRequestBase(BaseModel):
|
|
@@ -27,11 +24,7 @@ class AgentRequestBase(SearchRequestBase):
|
|
|
27
24
|
|
|
28
25
|
|
|
29
26
|
class CrawlRequestBase(BaseModel):
|
|
30
|
-
"""Base for derived ``POST /v1/crawl`` bodies: carries the required ``urls``.
|
|
27
|
+
"""Base for derived ``POST /v1/crawl`` bodies: carries the required ``urls``."""
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
configs' schema (``additionalProperties: false``).
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
model_config = deployment_model_config
|
|
29
|
+
model_config = camelized_model_config
|
|
37
30
|
urls: list[str] = Field(..., min_length=1, description="URLs to crawl")
|
|
@@ -35,15 +35,6 @@ def get_model_config(title: str | None = None) -> ConfigDict:
|
|
|
35
35
|
camelized_model_config = get_model_config()
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
deployment_model_config = ConfigDict(
|
|
39
|
-
alias_generator=to_camel,
|
|
40
|
-
field_title_generator=field_title_generator,
|
|
41
|
-
model_title_generator=model_title_generator,
|
|
42
|
-
populate_by_name=True,
|
|
43
|
-
extra="forbid",
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
38
|
# Marks the deactivated branch of an optional config value. Locally declared so
|
|
48
39
|
# core has no runtime dependency on unique-toolkit for this one-line alias.
|
|
49
40
|
DeactivatedNone = Annotated[
|
|
@@ -17,6 +17,13 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
_REDIRECT_STATUS_CODES = frozenset({301, 302, 303, 307, 308})
|
|
19
19
|
|
|
20
|
+
# Browser-like UA avoids CDN bot stalls (e.g. Akamai) that make fail-closed
|
|
21
|
+
# redirect probing falsely block legitimate public pages.
|
|
22
|
+
_REDIRECT_PROBE_USER_AGENT = (
|
|
23
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
24
|
+
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
25
|
+
)
|
|
26
|
+
|
|
20
27
|
ValidateUrlFn = Callable[[str], Awaitable[tuple[str, str] | None]]
|
|
21
28
|
|
|
22
29
|
|
|
@@ -32,7 +39,11 @@ async def resolve_redirect_chain(
|
|
|
32
39
|
"""
|
|
33
40
|
current = url
|
|
34
41
|
timeout = url_safety_settings.redirect_timeout_seconds
|
|
35
|
-
async with httpx.AsyncClient(
|
|
42
|
+
async with httpx.AsyncClient(
|
|
43
|
+
follow_redirects=False,
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
headers={"User-Agent": _REDIRECT_PROBE_USER_AGENT},
|
|
46
|
+
) as client:
|
|
36
47
|
for _ in range(url_safety_settings.max_redirect_hops):
|
|
37
48
|
error = await validate_url(current)
|
|
38
49
|
if error is not None:
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"""Crawler request merge helpers (no HTTP)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
-
|
|
9
|
-
from unique_search_proxy_core.crawlers.base import BaseCrawlerConfig
|
|
10
|
-
from unique_search_proxy_core.crawlers.config_types import crawler_config_from_request
|
|
11
|
-
from unique_search_proxy_core.param_policy import URLS_FIELD
|
|
12
|
-
|
|
13
|
-
CRAWLER_FIELD = "crawler"
|
|
14
|
-
TIMEOUT_FIELD = "timeout"
|
|
15
|
-
|
|
16
|
-
_DEPLOYMENT_DEFAULT_EXCLUDED_FIELDS = frozenset({CRAWLER_FIELD, URLS_FIELD})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def crawler_config_defaults(config: BaseModel) -> dict[str, Any]:
|
|
20
|
-
"""Deployment defaults merged into each flat crawl request."""
|
|
21
|
-
defaults: dict[str, Any] = {}
|
|
22
|
-
for field_name in type(config).model_fields:
|
|
23
|
-
if field_name in _DEPLOYMENT_DEFAULT_EXCLUDED_FIELDS:
|
|
24
|
-
continue
|
|
25
|
-
defaults[field_name] = getattr(config, field_name)
|
|
26
|
-
return defaults
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def merge_crawler_config_and_invocation(
|
|
30
|
-
config: BaseCrawlerConfig,
|
|
31
|
-
invocation: dict[str, Any],
|
|
32
|
-
) -> BaseModel:
|
|
33
|
-
"""Merge deployment config defaults with caller/LLM args into a flat crawl request."""
|
|
34
|
-
request_model = config.request_model()
|
|
35
|
-
defaults = crawler_config_defaults(config)
|
|
36
|
-
merged: dict[str, Any] = {**defaults, **invocation}
|
|
37
|
-
merged[CRAWLER_FIELD] = getattr(config, CRAWLER_FIELD)
|
|
38
|
-
request = request_model.model_validate(merged)
|
|
39
|
-
crawler_config_from_request(request)
|
|
40
|
-
return request
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
__all__ = [
|
|
44
|
-
"CRAWLER_FIELD",
|
|
45
|
-
"TIMEOUT_FIELD",
|
|
46
|
-
"URLS_FIELD",
|
|
47
|
-
"crawler_config_defaults",
|
|
48
|
-
"merge_crawler_config_and_invocation",
|
|
49
|
-
]
|
|
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
|