coresens 0.1.0__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.
- coresens-0.1.0/.gitignore +8 -0
- coresens-0.1.0/PKG-INFO +37 -0
- coresens-0.1.0/README.md +10 -0
- coresens-0.1.0/example_test.py +43 -0
- coresens-0.1.0/project.md +10 -0
- coresens-0.1.0/pyproject.toml +43 -0
- coresens-0.1.0/src/coresens/__init__.py +46 -0
- coresens-0.1.0/src/coresens/async_client.py +154 -0
- coresens-0.1.0/src/coresens/client.py +156 -0
- coresens-0.1.0/src/coresens/exceptions.py +48 -0
- coresens-0.1.0/src/coresens/models.py +115 -0
coresens-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: coresens
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for CoreSens Neural Search & Web Retrieval API
|
|
5
|
+
Project-URL: Homepage, https://coresens.ru
|
|
6
|
+
Project-URL: Documentation, https://docs.coresens.ru
|
|
7
|
+
Project-URL: Repository, https://github.com/coresens/coresens-python
|
|
8
|
+
Author-email: CoreSens Team <dev@coresens.ru>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ai,crawler,llm,neural-search,rag,retrieval,search
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# CoreSens Python SDK
|
|
29
|
+
|
|
30
|
+
Официальная Python-библиотека для взаимодействия с **CoreSens API** — отечественным движком Neural Search & Web Retrieval для AI-агентов и RAG-систем.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🚀 Установка
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install coresens
|
coresens-0.1.0/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from coresens import CoreSens, AsyncCoreSens
|
|
3
|
+
|
|
4
|
+
BASE_URL = "**************************"
|
|
5
|
+
API_KEY = "vw_live_******************************************"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_sync():
|
|
9
|
+
print("--- 1. Тест синхронного поиска ---")
|
|
10
|
+
client = CoreSens(api_key=API_KEY, base_url=BASE_URL)
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
response = client.search(
|
|
14
|
+
query="Обратная инженерия",
|
|
15
|
+
num_results=2,
|
|
16
|
+
contents={"highlights": True, "text": True}
|
|
17
|
+
)
|
|
18
|
+
print(f"Запрос: {response.query} (Задержка: {response.latency_ms} мс)")
|
|
19
|
+
for item in response.results:
|
|
20
|
+
print(f"- [{item.score}] {item.title}: {item.url}")
|
|
21
|
+
print(f" Питч: {item.pitch}")
|
|
22
|
+
print(f" Хайлайты: {item.highlights}")
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"Ошибка: {e}")
|
|
25
|
+
finally:
|
|
26
|
+
client.close()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def test_async():
|
|
30
|
+
print("\n--- 2. Тест асинхронного извлечения (Extract) ---")
|
|
31
|
+
async with AsyncCoreSens(api_key=API_KEY, base_url=BASE_URL) as client:
|
|
32
|
+
try:
|
|
33
|
+
res = await client.extract(urls=["https://habr.com/ru/news/715642/"])
|
|
34
|
+
for doc in res.documents:
|
|
35
|
+
print(f"Извлечено: {doc.title} (Символов: {doc.char_count})")
|
|
36
|
+
print(f"Markdown превью: {str(doc.markdown_content)[:150]}...")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(f"Ошибка async: {e}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
test_sync()
|
|
43
|
+
asyncio.run(test_async())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "coresens"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for CoreSens Neural Search & Web Retrieval API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "CoreSens Team", email = "dev@coresens.ru" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["search", "neural-search", "rag", "ai", "crawler", "retrieval", "llm"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# [Dev-Assistant]: Минимальный набор зависимостей. Только проверенный HTTP-клиент и валидатор типов.
|
|
32
|
+
dependencies = [
|
|
33
|
+
"httpx>=0.27.0",
|
|
34
|
+
"pydantic>=2.0.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://coresens.ru"
|
|
39
|
+
Documentation = "https://docs.coresens.ru"
|
|
40
|
+
Repository = "https://github.com/coresens/coresens-python"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/coresens"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# [Dev-Assistant]: Главный экспорт библиотеки CoreSens.
|
|
2
|
+
# Позволяет импортировать всё необходимое напрямую: from coresens import CoreSens, AsyncCoreSens
|
|
3
|
+
|
|
4
|
+
from coresens.client import CoreSens
|
|
5
|
+
from coresens.async_client import AsyncCoreSens
|
|
6
|
+
from coresens.models import (
|
|
7
|
+
SearchRequest,
|
|
8
|
+
SearchResponse,
|
|
9
|
+
SearchResultItem,
|
|
10
|
+
ContentOptions,
|
|
11
|
+
ExtractRequest,
|
|
12
|
+
ExtractResponse,
|
|
13
|
+
ExtractedDocument,
|
|
14
|
+
)
|
|
15
|
+
from coresens.exceptions import (
|
|
16
|
+
CoreSensError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
PermissionDeniedError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
InvalidRequestError,
|
|
22
|
+
InternalServerError,
|
|
23
|
+
APIConnectionError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CoreSens",
|
|
30
|
+
"AsyncCoreSens",
|
|
31
|
+
"SearchRequest",
|
|
32
|
+
"SearchResponse",
|
|
33
|
+
"SearchResultItem",
|
|
34
|
+
"ContentOptions",
|
|
35
|
+
"ExtractRequest",
|
|
36
|
+
"ExtractResponse",
|
|
37
|
+
"ExtractedDocument",
|
|
38
|
+
"CoreSensError",
|
|
39
|
+
"AuthenticationError",
|
|
40
|
+
"PermissionDeniedError",
|
|
41
|
+
"NotFoundError",
|
|
42
|
+
"RateLimitError",
|
|
43
|
+
"InvalidRequestError",
|
|
44
|
+
"InternalServerError",
|
|
45
|
+
"APIConnectionError",
|
|
46
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# [Dev-Assistant]: Асинхронный HTTP-клиент для CoreSens API на базе httpx.AsyncClient.
|
|
2
|
+
# Безопасен для использования в асинхронных фреймворках (FastAPI, LangGraph, aiogram).
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Optional, Union, Dict, Any
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from coresens.models import (
|
|
9
|
+
SearchRequest,
|
|
10
|
+
SearchResponse,
|
|
11
|
+
ContentOptions,
|
|
12
|
+
TimeRangeType,
|
|
13
|
+
ExtractRequest,
|
|
14
|
+
ExtractResponse
|
|
15
|
+
)
|
|
16
|
+
from coresens.exceptions import (
|
|
17
|
+
CoreSensError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
PermissionDeniedError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
InvalidRequestError,
|
|
23
|
+
InternalServerError,
|
|
24
|
+
APIConnectionError
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncCoreSens:
|
|
29
|
+
"""
|
|
30
|
+
Асинхронный клиент для взаимодействия с CoreSens API.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: Optional[str] = None,
|
|
36
|
+
base_url: str = "https://api.coresens.ru",
|
|
37
|
+
timeout: float = 30.0,
|
|
38
|
+
max_retries: int = 2
|
|
39
|
+
):
|
|
40
|
+
self.api_key = api_key or os.getenv("CORESENS_API_KEY")
|
|
41
|
+
if not self.api_key:
|
|
42
|
+
raise AuthenticationError(
|
|
43
|
+
"API-ключ не найден. Передайте его явно: AsyncCoreSens(api_key='...') "
|
|
44
|
+
"или установите переменную окружения CORESENS_API_KEY."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.base_url = base_url.rstrip("/")
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
self.max_retries = max_retries
|
|
50
|
+
|
|
51
|
+
self._client = httpx.AsyncClient(
|
|
52
|
+
base_url=self.base_url,
|
|
53
|
+
headers={
|
|
54
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
55
|
+
"X-API-Key": self.api_key,
|
|
56
|
+
"User-Agent": "CoreSens-Async-Python-SDK/0.1.0",
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
},
|
|
59
|
+
timeout=self.timeout
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
63
|
+
"""Транслирует HTTP-статусы в типизированные исключения SDK."""
|
|
64
|
+
try:
|
|
65
|
+
error_data = response.json()
|
|
66
|
+
message = error_data.get("detail", response.text)
|
|
67
|
+
except Exception:
|
|
68
|
+
error_data = None
|
|
69
|
+
message = response.text or f"HTTP {response.status_code}"
|
|
70
|
+
|
|
71
|
+
status_code = response.status_code
|
|
72
|
+
|
|
73
|
+
if status_code == 401:
|
|
74
|
+
raise AuthenticationError(f"Неверный или просроченный API-ключ: {message}", error_data)
|
|
75
|
+
elif status_code == 402:
|
|
76
|
+
raise RateLimitError(f"Недостаточно средств на балансе: {message}", error_data)
|
|
77
|
+
elif status_code == 403:
|
|
78
|
+
raise PermissionDeniedError(f"Доступ запрещен: {message}", error_data)
|
|
79
|
+
elif status_code == 404:
|
|
80
|
+
raise NotFoundError(f"Ресурс не найден: {message}", error_data)
|
|
81
|
+
elif status_code in (400, 422):
|
|
82
|
+
raise InvalidRequestError(f"Ошибка валидации запроса: {message}", error_data)
|
|
83
|
+
elif status_code == 429:
|
|
84
|
+
raise RateLimitError(f"Превышен лимит запросов: {message}", error_data)
|
|
85
|
+
elif status_code >= 500:
|
|
86
|
+
raise InternalServerError(f"Ошибка сервера CoreSens (HTTP {status_code}): {message}", error_data)
|
|
87
|
+
else:
|
|
88
|
+
raise CoreSensError(f"Неизвестная ошибка API (HTTP {status_code}): {message}", error_data)
|
|
89
|
+
|
|
90
|
+
async def search(
|
|
91
|
+
self,
|
|
92
|
+
query: str,
|
|
93
|
+
num_results: int = 3,
|
|
94
|
+
time_range: Optional[TimeRangeType] = "auto",
|
|
95
|
+
languages: Optional[List[str]] = None,
|
|
96
|
+
start_published_date: Optional[str] = None,
|
|
97
|
+
end_published_date: Optional[str] = None,
|
|
98
|
+
contents: Optional[Union[ContentOptions, Dict[str, Any]]] = None,
|
|
99
|
+
) -> SearchResponse:
|
|
100
|
+
"""
|
|
101
|
+
Асинхронный семантический поиск по векторизованному Рунету.
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(contents, dict):
|
|
104
|
+
contents_obj = ContentOptions(**contents)
|
|
105
|
+
elif contents is None:
|
|
106
|
+
contents_obj = ContentOptions()
|
|
107
|
+
else:
|
|
108
|
+
contents_obj = contents
|
|
109
|
+
|
|
110
|
+
req_payload = SearchRequest(
|
|
111
|
+
query=query,
|
|
112
|
+
num_results=num_results,
|
|
113
|
+
time_range=time_range,
|
|
114
|
+
languages=languages,
|
|
115
|
+
start_published_date=start_published_date,
|
|
116
|
+
end_published_date=end_published_date,
|
|
117
|
+
contents=contents_obj
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
response = await self._client.post("/v1/search", json=req_payload.model_dump(exclude_none=True))
|
|
122
|
+
except httpx.RequestError as e:
|
|
123
|
+
raise APIConnectionError(f"Сетевая ошибка при запросе к CoreSens: {str(e)}")
|
|
124
|
+
|
|
125
|
+
if not response.is_success:
|
|
126
|
+
self._handle_error_response(response)
|
|
127
|
+
|
|
128
|
+
return SearchResponse.model_validate(response.json())
|
|
129
|
+
|
|
130
|
+
async def extract(self, urls: List[str]) -> ExtractResponse:
|
|
131
|
+
"""
|
|
132
|
+
Асинхронное мгновенное извлечение страниц в чистый Markdown и метаданные.
|
|
133
|
+
"""
|
|
134
|
+
req_payload = ExtractRequest(urls=urls)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
response = await self._client.post("/v1/extract", json=req_payload.model_dump())
|
|
138
|
+
except httpx.RequestError as e:
|
|
139
|
+
raise APIConnectionError(f"Сетевая ошибка при запросе к CoreSens: {str(e)}")
|
|
140
|
+
|
|
141
|
+
if not response.is_success:
|
|
142
|
+
self._handle_error_response(response)
|
|
143
|
+
|
|
144
|
+
return ExtractResponse.model_validate(response.json())
|
|
145
|
+
|
|
146
|
+
async def aclose(self) -> None:
|
|
147
|
+
"""Закрывает асинхронный пул соединений."""
|
|
148
|
+
await self._client.aclose()
|
|
149
|
+
|
|
150
|
+
async def __aenter__(self):
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
154
|
+
await self.aclose()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# [Dev-Assistant]: Синхронный HTTP-клиент для CoreSens API на базе httpx.Client.
|
|
2
|
+
# Автоматически управляет токеном, пулом соединений и маппингом ошибок в кастомные исключения.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Optional, Union, Dict, Any
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from coresens.models import (
|
|
9
|
+
SearchRequest,
|
|
10
|
+
SearchResponse,
|
|
11
|
+
ContentOptions,
|
|
12
|
+
TimeRangeType,
|
|
13
|
+
ExtractRequest,
|
|
14
|
+
ExtractResponse
|
|
15
|
+
)
|
|
16
|
+
from coresens.exceptions import (
|
|
17
|
+
CoreSensError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
PermissionDeniedError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
InvalidRequestError,
|
|
23
|
+
InternalServerError,
|
|
24
|
+
APIConnectionError
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoreSens:
|
|
29
|
+
"""
|
|
30
|
+
Синхронный клиент для взаимодействия с CoreSens Neural Search & Web Retrieval API.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: Optional[str] = None,
|
|
36
|
+
base_url: str = "https://api.coresens.ru",
|
|
37
|
+
timeout: float = 30.0,
|
|
38
|
+
max_retries: int = 2
|
|
39
|
+
):
|
|
40
|
+
# 1. Извлекаем API-ключ (аргумент -> переменная окружения)
|
|
41
|
+
self.api_key = api_key or os.getenv("CORESENS_API_KEY")
|
|
42
|
+
if not self.api_key:
|
|
43
|
+
raise AuthenticationError(
|
|
44
|
+
"API-ключ не найден. Передайте его явно: CoreSens(api_key='...') "
|
|
45
|
+
"или установите переменную окружения CORESENS_API_KEY."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.timeout = timeout
|
|
50
|
+
self.max_retries = max_retries
|
|
51
|
+
|
|
52
|
+
# 2. Инициализируем HTTP-сессию с авторизацией
|
|
53
|
+
self._client = httpx.Client(
|
|
54
|
+
base_url=self.base_url,
|
|
55
|
+
headers={
|
|
56
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
57
|
+
"X-API-Key": self.api_key,
|
|
58
|
+
"User-Agent": "CoreSens-Python-SDK/0.1.0",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
timeout=self.timeout
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
65
|
+
"""Транслирует HTTP-статусы в типизированные исключения SDK."""
|
|
66
|
+
try:
|
|
67
|
+
error_data = response.json()
|
|
68
|
+
message = error_data.get("detail", response.text)
|
|
69
|
+
except Exception:
|
|
70
|
+
error_data = None
|
|
71
|
+
message = response.text or f"HTTP {response.status_code}"
|
|
72
|
+
|
|
73
|
+
status_code = response.status_code
|
|
74
|
+
|
|
75
|
+
if status_code == 401:
|
|
76
|
+
raise AuthenticationError(f"Неверный или просроченный API-ключ: {message}", error_data)
|
|
77
|
+
elif status_code == 402:
|
|
78
|
+
raise RateLimitError(f"Недостаточно средств на балансе: {message}", error_data)
|
|
79
|
+
elif status_code == 403:
|
|
80
|
+
raise PermissionDeniedError(f"Доступ запрещен: {message}", error_data)
|
|
81
|
+
elif status_code == 404:
|
|
82
|
+
raise NotFoundError(f"Ресурс не найден: {message}", error_data)
|
|
83
|
+
elif status_code in (400, 422):
|
|
84
|
+
raise InvalidRequestError(f"Ошибка валидации запроса: {message}", error_data)
|
|
85
|
+
elif status_code == 429:
|
|
86
|
+
raise RateLimitError(f"Превышен лимит запросов: {message}", error_data)
|
|
87
|
+
elif status_code >= 500:
|
|
88
|
+
raise InternalServerError(f"Ошибка сервера CoreSens (HTTP {status_code}): {message}", error_data)
|
|
89
|
+
else:
|
|
90
|
+
raise CoreSensError(f"Неизвестная ошибка API (HTTP {status_code}): {message}", error_data)
|
|
91
|
+
|
|
92
|
+
def search(
|
|
93
|
+
self,
|
|
94
|
+
query: str,
|
|
95
|
+
num_results: int = 3,
|
|
96
|
+
time_range: Optional[TimeRangeType] = "auto",
|
|
97
|
+
languages: Optional[List[str]] = None,
|
|
98
|
+
start_published_date: Optional[str] = None,
|
|
99
|
+
end_published_date: Optional[str] = None,
|
|
100
|
+
contents: Optional[Union[ContentOptions, Dict[str, Any]]] = None,
|
|
101
|
+
) -> SearchResponse:
|
|
102
|
+
"""
|
|
103
|
+
Семантический нейропоиск по векторизованному Рунету.
|
|
104
|
+
"""
|
|
105
|
+
if isinstance(contents, dict):
|
|
106
|
+
contents_obj = ContentOptions(**contents)
|
|
107
|
+
elif contents is None:
|
|
108
|
+
contents_obj = ContentOptions()
|
|
109
|
+
else:
|
|
110
|
+
contents_obj = contents
|
|
111
|
+
|
|
112
|
+
req_payload = SearchRequest(
|
|
113
|
+
query=query,
|
|
114
|
+
num_results=num_results,
|
|
115
|
+
time_range=time_range,
|
|
116
|
+
languages=languages,
|
|
117
|
+
start_published_date=start_published_date,
|
|
118
|
+
end_published_date=end_published_date,
|
|
119
|
+
contents=contents_obj
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
response = self._client.post("/v1/search", json=req_payload.model_dump(exclude_none=True))
|
|
124
|
+
except httpx.RequestError as e:
|
|
125
|
+
raise APIConnectionError(f"Сетевая ошибка при запросе к CoreSens: {str(e)}")
|
|
126
|
+
|
|
127
|
+
if not response.is_success:
|
|
128
|
+
self._handle_error_response(response)
|
|
129
|
+
|
|
130
|
+
return SearchResponse.model_validate(response.json())
|
|
131
|
+
|
|
132
|
+
def extract(self, urls: List[str]) -> ExtractResponse:
|
|
133
|
+
"""
|
|
134
|
+
Мгновенное извлечение страниц в чистый Markdown и метаданные.
|
|
135
|
+
"""
|
|
136
|
+
req_payload = ExtractRequest(urls=urls)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
response = self._client.post("/v1/extract", json=req_payload.model_dump())
|
|
140
|
+
except httpx.RequestError as e:
|
|
141
|
+
raise APIConnectionError(f"Сетевая ошибка при запросе к CoreSens: {str(e)}")
|
|
142
|
+
|
|
143
|
+
if not response.is_success:
|
|
144
|
+
self._handle_error_response(response)
|
|
145
|
+
|
|
146
|
+
return ExtractResponse.model_validate(response.json())
|
|
147
|
+
|
|
148
|
+
def close(self) -> None:
|
|
149
|
+
"""Закрывает пул соединений."""
|
|
150
|
+
self._client.close()
|
|
151
|
+
|
|
152
|
+
def __enter__(self):
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
156
|
+
self.close()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# [Dev-Assistant]: Иерархия исключений для библиотеки CoreSens.
|
|
2
|
+
# Позволяет пользователям SDK элегантно перехватывать сетевые и API ошибки.
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CoreSensError(Exception):
|
|
8
|
+
"""Базовое исключение для всех ошибок SDK CoreSens."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, raw_response: Optional[Dict[str, Any]] = None):
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.message = message
|
|
13
|
+
self.raw_response = raw_response
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticationError(CoreSensError):
|
|
17
|
+
"""Ошибка аутентификации (HTTP 401). Неверный или отсутствующий API-ключ."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PermissionDeniedError(CoreSensError):
|
|
22
|
+
"""Ошибка прав доступа (HTTP 403). Заблокирован аккаунт или нет доступа к методу."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NotFoundError(CoreSensError):
|
|
27
|
+
"""Ресурс не найден (HTTP 404)."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RateLimitError(CoreSensError):
|
|
32
|
+
"""Превышен лимит запросов или закончились токены тарифа (HTTP 429)."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InvalidRequestError(CoreSensError):
|
|
37
|
+
"""Некорректные параметры запроса (HTTP 400 / 422)."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InternalServerError(CoreSensError):
|
|
42
|
+
"""Внутренняя ошибка на стороне серверов CoreSens (HTTP 5xx)."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class APIConnectionError(CoreSensError):
|
|
47
|
+
"""Сетевая ошибка: невозможно соединиться с хостом или сработал таймаут."""
|
|
48
|
+
pass
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# [Dev-Assistant]: Строго синхронизированные Pydantic v2 модели с app/domain/search.py и app/api/v1/endpoints/extract.py.
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Literal, Dict, Any
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseCoreSensModel(BaseModel):
|
|
8
|
+
"""Базовая модель с защитой от изменения схемы бэкенда."""
|
|
9
|
+
model_config = ConfigDict(
|
|
10
|
+
extra="ignore",
|
|
11
|
+
populate_by_name=True,
|
|
12
|
+
arbitrary_types_allowed=True
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ==========================================================
|
|
17
|
+
# 1. СХЕМЫ ДЛЯ SEARCH API (/v1/search)
|
|
18
|
+
# ==========================================================
|
|
19
|
+
|
|
20
|
+
class ContentOptions(BaseCoreSensModel):
|
|
21
|
+
"""Настройки извлечения контента в результатах поиска."""
|
|
22
|
+
text: bool = Field(
|
|
23
|
+
default=False,
|
|
24
|
+
description="Возвращать ли полный очищенный Markdown контент страницы (экономия токенов)"
|
|
25
|
+
)
|
|
26
|
+
highlights: bool = Field(
|
|
27
|
+
default=True,
|
|
28
|
+
description="Возвращать ли семантические цитаты"
|
|
29
|
+
)
|
|
30
|
+
highlights_per_url: int = Field(
|
|
31
|
+
default=2,
|
|
32
|
+
description="Количество ключевых цитат на каждый найденный URL"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TimeRangeType = Literal["auto", "day", "week", "month", "year", "any"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SearchRequest(BaseCoreSensModel):
|
|
40
|
+
"""Схема POST-запроса на поиск."""
|
|
41
|
+
query: str = Field(..., min_length=2, description="Поисковый запрос от LLM-агента или пользователя")
|
|
42
|
+
num_results: int = Field(default=3, ge=1, le=10, description="Количество результатов в выдаче")
|
|
43
|
+
time_range: Optional[TimeRangeType] = Field(
|
|
44
|
+
default="auto",
|
|
45
|
+
description="Окно свежести: 'auto', 'day', 'week', 'month', 'year', 'any'"
|
|
46
|
+
)
|
|
47
|
+
languages: Optional[List[str]] = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
description="Фильтр по ISO-639-1 языкам (например, ['ru', 'en'])"
|
|
50
|
+
)
|
|
51
|
+
start_published_date: Optional[str] = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
description="Нижний порог даты публикации в ISO (например, '2026-09-01')"
|
|
54
|
+
)
|
|
55
|
+
end_published_date: Optional[str] = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="Верхний порог даты публикации в ISO"
|
|
58
|
+
)
|
|
59
|
+
contents: ContentOptions = Field(default_factory=ContentOptions)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SearchResultItem(BaseCoreSensModel):
|
|
63
|
+
"""Отдельный результат в выдаче поиска."""
|
|
64
|
+
title: str
|
|
65
|
+
url: str
|
|
66
|
+
canonical_url: str
|
|
67
|
+
pitch: str = Field(description="Краткая выжимка / питч страницы для сниппета")
|
|
68
|
+
score: float = Field(description="Скор релевантности Cross-Encoder от 0 до 1")
|
|
69
|
+
published_date: Optional[str] = None
|
|
70
|
+
highlights: List[str] = Field(default_factory=list, description="Точечные семантические цитаты")
|
|
71
|
+
text: Optional[str] = Field(default=None, description="Полный очищенный Markdown контент страницы")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SearchResponse(BaseCoreSensModel):
|
|
75
|
+
"""Ответ API на поисковый запрос."""
|
|
76
|
+
query: str
|
|
77
|
+
results: List[SearchResultItem] = Field(default_factory=list)
|
|
78
|
+
total_results: int
|
|
79
|
+
latency_ms: int
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ==========================================================
|
|
83
|
+
# 2. СХЕМЫ ДЛЯ EXTRACT API (/v1/extract)
|
|
84
|
+
# ==========================================================
|
|
85
|
+
|
|
86
|
+
class ExtractRequest(BaseCoreSensModel):
|
|
87
|
+
"""Схема POST-запроса на извлечение в Markdown."""
|
|
88
|
+
urls: List[str] = Field(..., min_length=1, description="Список URL для мгновенного извлечения")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ExtractedDocument(BaseCoreSensModel):
|
|
92
|
+
"""Финальный структурированный документ страницы."""
|
|
93
|
+
url: str
|
|
94
|
+
canonical_url: str
|
|
95
|
+
doc_id: str = Field(description="SHA-256 от канонического URL")
|
|
96
|
+
title: Optional[str] = None
|
|
97
|
+
markdown_content: Optional[str] = None
|
|
98
|
+
content_hash: Optional[str] = None
|
|
99
|
+
author: Optional[str] = None
|
|
100
|
+
published_date: Optional[str] = None
|
|
101
|
+
published_at: Optional[int] = Field(default=None, description="UTC UNIX timestamp публикации")
|
|
102
|
+
language: Optional[str] = None
|
|
103
|
+
status_code: int = 200
|
|
104
|
+
is_modified: bool = True
|
|
105
|
+
etag: Optional[str] = None
|
|
106
|
+
last_modified_header: Optional[str] = None
|
|
107
|
+
char_count: int = 0
|
|
108
|
+
is_spa: bool = False
|
|
109
|
+
needs_headless_render: bool = False
|
|
110
|
+
error: Optional[str] = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ExtractResponse(BaseCoreSensModel):
|
|
114
|
+
"""Ответ API на запрос краулинга/извлечения."""
|
|
115
|
+
documents: List[ExtractedDocument] = Field(default_factory=list)
|