sse-lib 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.
sse_lib-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: sse-lib
3
+ Version: 0.1.0
4
+ Summary: Thin Python client for the Semantic Search Engine (SSE) REST API
5
+ Author: RadLab
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/radlab-dev-group/semantic-search-engine
8
+ Project-URL: Repository, https://github.com/radlab-dev-group/semantic-search-engine
9
+ Project-URL: Issues, https://github.com/radlab-dev-group/semantic-search-engine/issues
10
+ Keywords: semantic-search,rag,rest,client,sse,search-engine
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
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.14
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: requests<3.0,>=2.31
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest<10.0,>=8.0; extra == "dev"
27
+ Requires-Dist: black<26.0,>=24.1; extra == "dev"
28
+ Requires-Dist: flake8<8.0,>=7.0; extra == "dev"
29
+
30
+ # sse_lib
31
+
32
+ Thin Python client for the **Semantic Search Engine** (SSE) REST API
33
+ (`src/sse_api/` in this repository).
34
+
35
+ `sse_lib` is a client-side layer only: it maps Python calls onto the HTTP
36
+ endpoints of a running SSE backend and turns the JSON answers into small
37
+ dataclasses. It does **not** import Django, `torch`, `pymilvus` or any other
38
+ server dependency — the single runtime requirement is `requests`.
39
+
40
+ ```python
41
+ from sse_lib import SSEClient, SearchOptions, GenerativeOptions
42
+
43
+ client = SSEClient("http://localhost:8271", token="...")
44
+
45
+ client.create_collection(
46
+ "my_docs",
47
+ "My Documents",
48
+ "Test collection",
49
+ embedder="radlab/polish-bi-encoder-mean",
50
+ reranker="radlab/polish-cross-encoder",
51
+ index_type="HNSW",
52
+ chunking={"max_tokens_in_chunk": 128, "number_of_overlap_tokens": 0},
53
+ )
54
+ client.upload_files("my_docs", ["invoice.pdf", "regulations.zip"])
55
+ client.index_documents("my_docs", ["Some text to index."], category="Notes")
56
+
57
+ found = client.search("my_docs", "policy on data retention",
58
+ SearchOptions(max_results=20, rerank_results=True))
59
+ answer = client.generate_answer(
60
+ found.query_response_id,
61
+ GenerativeOptions(
62
+ generative_model="radlab/pLLama-3-8B-DPO-L",
63
+ percentage_rank_mass=40,
64
+ ),
65
+ )
66
+ print(answer.answer)
67
+ ```
68
+
69
+ Chunking belongs to the **collection**, not to a single call: declare it with
70
+ `create_collection(..., chunking={...})` (or `strict_chunking=True` to forbid any
71
+ deviation) and the indexing calls above may leave both keys out — the server takes
72
+ the settings of the collection. `client.embedders_info()` returns
73
+ `max_model_length` of every embedder, the largest chunk its model can take in
74
+ (`client.rerankers_info()` the pair budget of the rerankers). Passing
75
+ `max_tokens_in_chunk` / `number_of_overlap_tokens` per call, or once per client
76
+ with `SSEClient(..., default_indexing_options={...})`, still works; a split which
77
+ cannot exist (`overlap >= chunk`) raises `SSEValueError` before the request goes
78
+ out. `percentage_rank_mass` of `generate_answer` is required by the backend and
79
+ has no library-wide default on purpose.
80
+
81
+ ## Installation
82
+
83
+ From a checkout:
84
+
85
+ ```bash
86
+ pip install ./sse_lib # or: pip install -e ./sse_lib (development)
87
+ ```
88
+
89
+ Once published to a registry, `pip install sse-lib` is all that is needed — the
90
+ package is standalone and versioned independently (`sse_lib.__version__`).
91
+
92
+ Python 3.9+, `requests` as the only runtime dependency.
93
+
94
+ ## Authentication
95
+
96
+ | Mode | How |
97
+ |------|-----|
98
+ | Pre-existing token (default DRF token auth) | `SSEClient(host, token="...")` — sent as `Authorization: Token <value>` |
99
+ | Login with user and password | `SSEClient(host, username="user", password="...")` — `POST login` runs lazily, before the first request |
100
+ | Keycloak / OAuth (backend started with `ENV_USE_KC_AUTH=1`) | `client.login_url()` → redirect the user → `client.login_with_code(code, state)`; later `client.refresh_access_token()` and `client.logout()` |
101
+ | Custom scheme | `SSEClient(host, token="...", token_type="Bearer")` |
102
+
103
+ ## Configuration
104
+
105
+ | Constructor argument | Environment variable | Default | Meaning |
106
+ |----------------------|----------------------|---------|---------|
107
+ | `api_host` | `SSE_API_HOST` | – | `http://localhost:8271`; the scheme may be omitted (`localhost` → `http`, anything else → `https`) |
108
+ | `api_prefix` | `SSE_API_PREFIX` | `api` | Path between the host and the endpoints; set `api/v1` for versioned deployments (`main.api` in `django-config.json`) |
109
+ | `token` | `SSE_API_TOKEN` | – | Value of the `Authorization` header |
110
+ | `language` | `SSE_API_LANGUAGE` | `pl` | Sent as `lang` on every call (`pl` / `en`) |
111
+ | `timeout` | `SSE_API_TIMEOUT` | `60.0` | Per-request timeout in seconds |
112
+ | `verify`, `session`, `headers`, `max_retries` | – | `True`, new session, `{}`, `2` | TLS verification, shared `requests.Session`, extra headers, GET retries |
113
+ | `default_search_options`, `default_indexing_options` | – | – | Client-wide option defaults merged into every search / indexing call |
114
+
115
+ `SSEClient` is a context manager; leaving the block closes the HTTP session.
116
+
117
+ ## API coverage
118
+
119
+ | Client method | REST endpoint |
120
+ |---------------|---------------|
121
+ | `create_collection(...)` | `POST new_collection` |
122
+ | `list_collections()`, `get_collection(name)`, `collection_exists(name)` | `GET collections` |
123
+ | `list_categories(collection)` | `GET categories` |
124
+ | `list_documents(collection, with_disabled=False)`, `list_document_details(collection)` | `GET documents` |
125
+ | `upload_files(collection, files, ...)` | `POST upload_and_index_files` |
126
+ | `document_content(collection, document, from_page=None, max_pages=None, chunk_type=None)` | `GET document_content` |
127
+ | `index_documents(collection, documents, ...)` | `POST add_and_index_texts` |
128
+ | `indexing_status(collection=None, job_id=None)` | `GET indexing_status` |
129
+ | `delete_texts(collection, chunk_ids)` | `POST delete_texts` |
130
+ | `delete_documents(collection, documents=None, document_ids=None)` | `POST delete_documents` |
131
+ | `delete_collection(collection)` | `POST delete_collection` |
132
+ | `set_documents_use_in_search(collection, documents=None, document_ids=None, use_in_search=False)`, `deactivate_documents(...)`, `activate_documents(...)` | `POST set_documents_use_in_search` |
133
+ | `list_question_templates()` | `GET question_templates` |
134
+ | `list_filter_options()` | `GET filter_options` |
135
+ | `search(collection, query, options, ...)` | `POST search_with_options` |
136
+ | `generate_answer(query_response_id, options, ...)` | `POST generative_answer` |
137
+ | `rate_answer(answer_response_id, value, value_max, comment=None)` | `POST rate_generative_answer` |
138
+ | `list_search_history(collection=None, query_response_id=None)`, `get_search_history(query_response_id)`, `delete_search_history(query_ids=None, query_response_ids=None)` | `GET search_history`, `GET get_search_history`, `POST delete_search_history` |
139
+ | `list_generative_models()`, `list_embedders()`, `list_rerankers()` | `GET generative_models`, `GET embedders`, `GET rerankers` |
140
+ | `embedders_info()`, `rerankers_info()` | `GET embedders`, `GET rerankers` — `models_info` with `vector_size`, `device` and `max_model_length` |
141
+ | `new_chat(...)`, `send_chat_message(...)`, `save_chat(...)`, `get_chat_by_hash(...)`, `list_chats()` | `POST new_chat`, `POST add_user_message`, `POST save_chat`, `GET get_chat_by_hash`, `GET chats` |
142
+ | `login()`, `login_with_code()`, `refresh_access_token()`, `logout()`, `login_url()` | `POST login`, `POST refresh_token`, `POST logout`, `POST login_url` |
143
+ | `health()`, `is_healthy()` | `GET healthz` (public — no token, no login) |
144
+ | `request(method, endpoint, ...)`, `get(...)`, `post(...)` | any endpoint, including ones this release does not wrap |
145
+
146
+ Collections may be passed as a name, a `Collection` returned by
147
+ `create_collection()` / `list_collections()`, or any mapping with a `name` key.
148
+ A `Collection` also carries the settings of the collection: `metadata_json`,
149
+ `chunking` (`{}` when none was declared) and `strict_chunking`.
150
+
151
+ `GET documents` has two views. The default one lists the names of the
152
+ documents which take part in the search (`list_documents()`); with
153
+ `with_disabled=True` — and always in `list_document_details()` — the server
154
+ adds the deactivated documents together with their `id` and `use_in_search`
155
+ flag. Those identifiers are what `document_ids[]` of `delete_documents` and
156
+ `set_documents_use_in_search` expects.
157
+
158
+ `send_chat_message(..., collection="my_docs")` requires an explicit collection,
159
+ even if the chat was created with one: the current server resolves the collection
160
+ from each message request. Omitting it raises `SSEValueError` before sending.
161
+
162
+ Set `api_prefix=""` for deployments without an API path prefix; an explicitly
163
+ provided empty prefix overrides `SSE_API_PREFIX`.
164
+
165
+ ## Options objects
166
+
167
+ | Class | Used by | Notes |
168
+ |-------|---------|-------|
169
+ | `SearchOptions` | `search()` | Only the fields that are set are sent; `extra` merges unknown keys |
170
+ | `IndexingOptions` | `upload_files()`, `index_documents()` | Serialises the five cleaning keys the server reads with `options["..."]`; the two chunk keys appear only when set, so the collection decides |
171
+ | `GenerativeOptions` | `generate_answer()`, `send_chat_message()` | `None` fields are dropped; carries `use_doc_names_in_response`, the flag the generator reads from the same `query_options` |
172
+ | `ChatOptions` | `new_chat()`, `send_chat_message()` (`chat_options=`) | The four pipeline switches (`use_rag_supervisor`, `use_content_supervisor`, `use_intelligent_query_rewrite`, `query_instruction`) merged into the single `options` object the chat endpoints read |
173
+
174
+ Single options may also be passed as keyword arguments — the two calls below are
175
+ equivalent:
176
+
177
+ ```python
178
+ client.search("my_docs", "query", SearchOptions(max_results=20, hybrid_search=True))
179
+ client.search("my_docs", "query", max_results=20, hybrid_search=True)
180
+ ```
181
+
182
+ Unknown keys in search and generation dictionaries are preserved in `extra`;
183
+ unknown indexing keys raise `SSEValueError`. Per-call dictionaries override only
184
+ the supplied client defaults, while `IndexingOptions` instances supply a complete
185
+ configuration (including their dataclass defaults). Keyword arguments win over
186
+ both.
187
+
188
+ ### Wire-format quirks handled by the client
189
+
190
+ The backend is not uniform, and these traps are the reason the wrapper exists:
191
+
192
+ * `options` of `search_with_options`, `query_options` of `generative_answer` and
193
+ `indexing_options` of `upload_and_index_files` are read with `json.loads()`,
194
+ so they are sent as **JSON strings**;
195
+ * `indexing_options` of `add_and_index_texts` and `options`/`search_options` of
196
+ the chat endpoints are read as objects, so they are sent as **JSON objects**;
197
+ * uploaded files use the field name `files[]` and input texts `texts[]`;
198
+ * endpoint paths have **no trailing slash** (that is how Django registers them);
199
+ * business errors come back as HTTP 200 with `{"status": false, "errors": [...]}`.
200
+
201
+ ## Return values
202
+
203
+ Methods return dataclasses (`sse_lib.models`): `Collection`, `SearchResponse`
204
+ (with `.hits` of `SearchHit`), `Answer`, `UploadResult`, `IndexingResult`,
205
+ `DocumentRef`, `Chat`, `ChatReply`, `ChatMessage`, `ChatHistory`. Every one of them keeps the
206
+ untouched server payload in `.raw`, and
207
+ `client.request("GET", "some_endpoint")` returns the unwrapped `body` of any
208
+ endpoint for fields that are not modelled yet.
209
+
210
+ ## Errors
211
+
212
+ | Exception | Raised when |
213
+ |-----------|-------------|
214
+ | `SSEValueError` | An argument is rejected before the request is sent (empty query, unknown index type, ...) |
215
+ | `SSEConfigError` | Invalid configuration (missing host, non-positive timeout, login without credentials) |
216
+ | `SSEAPIError` | The API answered `{"status": false, "errors": [...]}` — inspect `.error_names`, `.not_given_params` |
217
+ | `SSEAuthenticationError` | HTTP 401/403 |
218
+ | `SSEHTTPError` | Any other non-2xx status |
219
+ | `SSETransportError` | No response at all (DNS, connection refused, timeout) |
220
+
221
+ All of them derive from `SSEError`, so a single `except SSEError` is enough for
222
+ a coarse handler. Idempotent `GET` calls are retried on connection errors and
223
+ on 429/502/503/504; `POST` is never replayed, because indexing twice is a real
224
+ side effect.
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ cd sse_lib
230
+ PYTHONPATH=src python -m unittest discover -s tests -v # no installation needed
231
+ python -m pytest . # pytest, if available
232
+ black --check . && flake8 .
233
+ ```
234
+
235
+ The tests never open a socket: `tests/support.py` replaces the HTTP session
236
+ with a recording double, which pins the exact payloads sent to the API.
@@ -0,0 +1,207 @@
1
+ # sse_lib
2
+
3
+ Thin Python client for the **Semantic Search Engine** (SSE) REST API
4
+ (`src/sse_api/` in this repository).
5
+
6
+ `sse_lib` is a client-side layer only: it maps Python calls onto the HTTP
7
+ endpoints of a running SSE backend and turns the JSON answers into small
8
+ dataclasses. It does **not** import Django, `torch`, `pymilvus` or any other
9
+ server dependency — the single runtime requirement is `requests`.
10
+
11
+ ```python
12
+ from sse_lib import SSEClient, SearchOptions, GenerativeOptions
13
+
14
+ client = SSEClient("http://localhost:8271", token="...")
15
+
16
+ client.create_collection(
17
+ "my_docs",
18
+ "My Documents",
19
+ "Test collection",
20
+ embedder="radlab/polish-bi-encoder-mean",
21
+ reranker="radlab/polish-cross-encoder",
22
+ index_type="HNSW",
23
+ chunking={"max_tokens_in_chunk": 128, "number_of_overlap_tokens": 0},
24
+ )
25
+ client.upload_files("my_docs", ["invoice.pdf", "regulations.zip"])
26
+ client.index_documents("my_docs", ["Some text to index."], category="Notes")
27
+
28
+ found = client.search("my_docs", "policy on data retention",
29
+ SearchOptions(max_results=20, rerank_results=True))
30
+ answer = client.generate_answer(
31
+ found.query_response_id,
32
+ GenerativeOptions(
33
+ generative_model="radlab/pLLama-3-8B-DPO-L",
34
+ percentage_rank_mass=40,
35
+ ),
36
+ )
37
+ print(answer.answer)
38
+ ```
39
+
40
+ Chunking belongs to the **collection**, not to a single call: declare it with
41
+ `create_collection(..., chunking={...})` (or `strict_chunking=True` to forbid any
42
+ deviation) and the indexing calls above may leave both keys out — the server takes
43
+ the settings of the collection. `client.embedders_info()` returns
44
+ `max_model_length` of every embedder, the largest chunk its model can take in
45
+ (`client.rerankers_info()` the pair budget of the rerankers). Passing
46
+ `max_tokens_in_chunk` / `number_of_overlap_tokens` per call, or once per client
47
+ with `SSEClient(..., default_indexing_options={...})`, still works; a split which
48
+ cannot exist (`overlap >= chunk`) raises `SSEValueError` before the request goes
49
+ out. `percentage_rank_mass` of `generate_answer` is required by the backend and
50
+ has no library-wide default on purpose.
51
+
52
+ ## Installation
53
+
54
+ From a checkout:
55
+
56
+ ```bash
57
+ pip install ./sse_lib # or: pip install -e ./sse_lib (development)
58
+ ```
59
+
60
+ Once published to a registry, `pip install sse-lib` is all that is needed — the
61
+ package is standalone and versioned independently (`sse_lib.__version__`).
62
+
63
+ Python 3.9+, `requests` as the only runtime dependency.
64
+
65
+ ## Authentication
66
+
67
+ | Mode | How |
68
+ |------|-----|
69
+ | Pre-existing token (default DRF token auth) | `SSEClient(host, token="...")` — sent as `Authorization: Token <value>` |
70
+ | Login with user and password | `SSEClient(host, username="user", password="...")` — `POST login` runs lazily, before the first request |
71
+ | Keycloak / OAuth (backend started with `ENV_USE_KC_AUTH=1`) | `client.login_url()` → redirect the user → `client.login_with_code(code, state)`; later `client.refresh_access_token()` and `client.logout()` |
72
+ | Custom scheme | `SSEClient(host, token="...", token_type="Bearer")` |
73
+
74
+ ## Configuration
75
+
76
+ | Constructor argument | Environment variable | Default | Meaning |
77
+ |----------------------|----------------------|---------|---------|
78
+ | `api_host` | `SSE_API_HOST` | – | `http://localhost:8271`; the scheme may be omitted (`localhost` → `http`, anything else → `https`) |
79
+ | `api_prefix` | `SSE_API_PREFIX` | `api` | Path between the host and the endpoints; set `api/v1` for versioned deployments (`main.api` in `django-config.json`) |
80
+ | `token` | `SSE_API_TOKEN` | – | Value of the `Authorization` header |
81
+ | `language` | `SSE_API_LANGUAGE` | `pl` | Sent as `lang` on every call (`pl` / `en`) |
82
+ | `timeout` | `SSE_API_TIMEOUT` | `60.0` | Per-request timeout in seconds |
83
+ | `verify`, `session`, `headers`, `max_retries` | – | `True`, new session, `{}`, `2` | TLS verification, shared `requests.Session`, extra headers, GET retries |
84
+ | `default_search_options`, `default_indexing_options` | – | – | Client-wide option defaults merged into every search / indexing call |
85
+
86
+ `SSEClient` is a context manager; leaving the block closes the HTTP session.
87
+
88
+ ## API coverage
89
+
90
+ | Client method | REST endpoint |
91
+ |---------------|---------------|
92
+ | `create_collection(...)` | `POST new_collection` |
93
+ | `list_collections()`, `get_collection(name)`, `collection_exists(name)` | `GET collections` |
94
+ | `list_categories(collection)` | `GET categories` |
95
+ | `list_documents(collection, with_disabled=False)`, `list_document_details(collection)` | `GET documents` |
96
+ | `upload_files(collection, files, ...)` | `POST upload_and_index_files` |
97
+ | `document_content(collection, document, from_page=None, max_pages=None, chunk_type=None)` | `GET document_content` |
98
+ | `index_documents(collection, documents, ...)` | `POST add_and_index_texts` |
99
+ | `indexing_status(collection=None, job_id=None)` | `GET indexing_status` |
100
+ | `delete_texts(collection, chunk_ids)` | `POST delete_texts` |
101
+ | `delete_documents(collection, documents=None, document_ids=None)` | `POST delete_documents` |
102
+ | `delete_collection(collection)` | `POST delete_collection` |
103
+ | `set_documents_use_in_search(collection, documents=None, document_ids=None, use_in_search=False)`, `deactivate_documents(...)`, `activate_documents(...)` | `POST set_documents_use_in_search` |
104
+ | `list_question_templates()` | `GET question_templates` |
105
+ | `list_filter_options()` | `GET filter_options` |
106
+ | `search(collection, query, options, ...)` | `POST search_with_options` |
107
+ | `generate_answer(query_response_id, options, ...)` | `POST generative_answer` |
108
+ | `rate_answer(answer_response_id, value, value_max, comment=None)` | `POST rate_generative_answer` |
109
+ | `list_search_history(collection=None, query_response_id=None)`, `get_search_history(query_response_id)`, `delete_search_history(query_ids=None, query_response_ids=None)` | `GET search_history`, `GET get_search_history`, `POST delete_search_history` |
110
+ | `list_generative_models()`, `list_embedders()`, `list_rerankers()` | `GET generative_models`, `GET embedders`, `GET rerankers` |
111
+ | `embedders_info()`, `rerankers_info()` | `GET embedders`, `GET rerankers` — `models_info` with `vector_size`, `device` and `max_model_length` |
112
+ | `new_chat(...)`, `send_chat_message(...)`, `save_chat(...)`, `get_chat_by_hash(...)`, `list_chats()` | `POST new_chat`, `POST add_user_message`, `POST save_chat`, `GET get_chat_by_hash`, `GET chats` |
113
+ | `login()`, `login_with_code()`, `refresh_access_token()`, `logout()`, `login_url()` | `POST login`, `POST refresh_token`, `POST logout`, `POST login_url` |
114
+ | `health()`, `is_healthy()` | `GET healthz` (public — no token, no login) |
115
+ | `request(method, endpoint, ...)`, `get(...)`, `post(...)` | any endpoint, including ones this release does not wrap |
116
+
117
+ Collections may be passed as a name, a `Collection` returned by
118
+ `create_collection()` / `list_collections()`, or any mapping with a `name` key.
119
+ A `Collection` also carries the settings of the collection: `metadata_json`,
120
+ `chunking` (`{}` when none was declared) and `strict_chunking`.
121
+
122
+ `GET documents` has two views. The default one lists the names of the
123
+ documents which take part in the search (`list_documents()`); with
124
+ `with_disabled=True` — and always in `list_document_details()` — the server
125
+ adds the deactivated documents together with their `id` and `use_in_search`
126
+ flag. Those identifiers are what `document_ids[]` of `delete_documents` and
127
+ `set_documents_use_in_search` expects.
128
+
129
+ `send_chat_message(..., collection="my_docs")` requires an explicit collection,
130
+ even if the chat was created with one: the current server resolves the collection
131
+ from each message request. Omitting it raises `SSEValueError` before sending.
132
+
133
+ Set `api_prefix=""` for deployments without an API path prefix; an explicitly
134
+ provided empty prefix overrides `SSE_API_PREFIX`.
135
+
136
+ ## Options objects
137
+
138
+ | Class | Used by | Notes |
139
+ |-------|---------|-------|
140
+ | `SearchOptions` | `search()` | Only the fields that are set are sent; `extra` merges unknown keys |
141
+ | `IndexingOptions` | `upload_files()`, `index_documents()` | Serialises the five cleaning keys the server reads with `options["..."]`; the two chunk keys appear only when set, so the collection decides |
142
+ | `GenerativeOptions` | `generate_answer()`, `send_chat_message()` | `None` fields are dropped; carries `use_doc_names_in_response`, the flag the generator reads from the same `query_options` |
143
+ | `ChatOptions` | `new_chat()`, `send_chat_message()` (`chat_options=`) | The four pipeline switches (`use_rag_supervisor`, `use_content_supervisor`, `use_intelligent_query_rewrite`, `query_instruction`) merged into the single `options` object the chat endpoints read |
144
+
145
+ Single options may also be passed as keyword arguments — the two calls below are
146
+ equivalent:
147
+
148
+ ```python
149
+ client.search("my_docs", "query", SearchOptions(max_results=20, hybrid_search=True))
150
+ client.search("my_docs", "query", max_results=20, hybrid_search=True)
151
+ ```
152
+
153
+ Unknown keys in search and generation dictionaries are preserved in `extra`;
154
+ unknown indexing keys raise `SSEValueError`. Per-call dictionaries override only
155
+ the supplied client defaults, while `IndexingOptions` instances supply a complete
156
+ configuration (including their dataclass defaults). Keyword arguments win over
157
+ both.
158
+
159
+ ### Wire-format quirks handled by the client
160
+
161
+ The backend is not uniform, and these traps are the reason the wrapper exists:
162
+
163
+ * `options` of `search_with_options`, `query_options` of `generative_answer` and
164
+ `indexing_options` of `upload_and_index_files` are read with `json.loads()`,
165
+ so they are sent as **JSON strings**;
166
+ * `indexing_options` of `add_and_index_texts` and `options`/`search_options` of
167
+ the chat endpoints are read as objects, so they are sent as **JSON objects**;
168
+ * uploaded files use the field name `files[]` and input texts `texts[]`;
169
+ * endpoint paths have **no trailing slash** (that is how Django registers them);
170
+ * business errors come back as HTTP 200 with `{"status": false, "errors": [...]}`.
171
+
172
+ ## Return values
173
+
174
+ Methods return dataclasses (`sse_lib.models`): `Collection`, `SearchResponse`
175
+ (with `.hits` of `SearchHit`), `Answer`, `UploadResult`, `IndexingResult`,
176
+ `DocumentRef`, `Chat`, `ChatReply`, `ChatMessage`, `ChatHistory`. Every one of them keeps the
177
+ untouched server payload in `.raw`, and
178
+ `client.request("GET", "some_endpoint")` returns the unwrapped `body` of any
179
+ endpoint for fields that are not modelled yet.
180
+
181
+ ## Errors
182
+
183
+ | Exception | Raised when |
184
+ |-----------|-------------|
185
+ | `SSEValueError` | An argument is rejected before the request is sent (empty query, unknown index type, ...) |
186
+ | `SSEConfigError` | Invalid configuration (missing host, non-positive timeout, login without credentials) |
187
+ | `SSEAPIError` | The API answered `{"status": false, "errors": [...]}` — inspect `.error_names`, `.not_given_params` |
188
+ | `SSEAuthenticationError` | HTTP 401/403 |
189
+ | `SSEHTTPError` | Any other non-2xx status |
190
+ | `SSETransportError` | No response at all (DNS, connection refused, timeout) |
191
+
192
+ All of them derive from `SSEError`, so a single `except SSEError` is enough for
193
+ a coarse handler. Idempotent `GET` calls are retried on connection errors and
194
+ on 429/502/503/504; `POST` is never replayed, because indexing twice is a real
195
+ side effect.
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ cd sse_lib
201
+ PYTHONPATH=src python -m unittest discover -s tests -v # no installation needed
202
+ python -m pytest . # pytest, if available
203
+ black --check . && flake8 .
204
+ ```
205
+
206
+ The tests never open a socket: `tests/support.py` replaces the HTTP session
207
+ with a recording double, which pins the exact payloads sent to the API.
@@ -0,0 +1,66 @@
1
+ # Standalone, pip-installable client of the SSE REST API.
2
+ #
3
+ # pip install ./sse_lib # from a checkout
4
+ # pip install -e ./sse_lib # development install
5
+ #
6
+ # It deliberately depends on `requests` only: the library is meant to be added
7
+ # to any project that has to talk to a Semantic Search Engine deployment,
8
+ # without pulling in Django, torch or pymilvus.
9
+ [build-system]
10
+ requires = ["setuptools>=61"]
11
+ build-backend = "setuptools.build_meta"
12
+
13
+ [project]
14
+ name = "sse-lib"
15
+ description = "Thin Python client for the Semantic Search Engine (SSE) REST API"
16
+ readme = "README.md"
17
+ requires-python = ">=3.9"
18
+ license = { text = "Apache-2.0" }
19
+ authors = [{ name = "RadLab" }]
20
+ keywords = ["semantic-search", "rag", "rest", "client", "sse", "search-engine"]
21
+ classifiers = [
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: Apache Software License",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Typing :: Typed",
33
+ ]
34
+ dependencies = ["requests>=2.31,<3.0"]
35
+ dynamic = ["version"]
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=8.0,<10.0",
40
+ "black>=24.1,<26.0",
41
+ "flake8>=7.0,<8.0",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/radlab-dev-group/semantic-search-engine"
46
+ Repository = "https://github.com/radlab-dev-group/semantic-search-engine"
47
+ Issues = "https://github.com/radlab-dev-group/semantic-search-engine/issues"
48
+
49
+ [tool.setuptools]
50
+ package-dir = { "" = "src" }
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.setuptools.package-data]
56
+ sse_lib = ["py.typed"]
57
+
58
+ [tool.setuptools.dynamic]
59
+ version = { attr = "sse_lib.__version__" }
60
+
61
+ [tool.black]
62
+ line-length = 85
63
+ target-version = ['py39', 'py310', 'py311', 'py312']
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,119 @@
1
+ """
2
+ sse_lib
3
+ -------
4
+
5
+ Thin client library for the **Semantic Search Engine** (SSE) REST API.
6
+
7
+ It wraps the HTTP endpoints of ``sse_api`` (collections, indexing, hybrid
8
+ search, RAG answers, chats and model listings) behind a small Python facade;
9
+ the server side stays untouched::
10
+
11
+ from sse_lib import SSEClient, SearchOptions, GenerativeOptions
12
+
13
+ with SSEClient("http://localhost:8271", token="...") as client:
14
+ collection = client.create_collection(
15
+ "my_docs",
16
+ "My Documents",
17
+ "Test collection",
18
+ embedder="radlab/polish-bi-encoder-mean",
19
+ reranker="radlab/polish-cross-encoder",
20
+ index_type="HNSW",
21
+ )
22
+ client.index_documents(collection.name, ["Some text to index."])
23
+ found = client.search(collection.name, "text", max_results=10)
24
+ answer = client.generate_answer(
25
+ found.query_response_id,
26
+ GenerativeOptions(
27
+ generative_model="radlab/pLLama-3-8B-DPO-L",
28
+ percentage_rank_mass=40,
29
+ ),
30
+ )
31
+ print(answer.answer)
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from sse_lib import endpoints
37
+ from sse_lib.client import SSEClient
38
+ from sse_lib.exceptions import (
39
+ SSEAPIError,
40
+ SSEAuthenticationError,
41
+ SSEConfigError,
42
+ SSEError,
43
+ SSEHTTPError,
44
+ SSETransportError,
45
+ SSEValueError,
46
+ )
47
+ from sse_lib.models import (
48
+ Answer,
49
+ Chat,
50
+ ChatHistory,
51
+ ChatMessage,
52
+ ChatReply,
53
+ Collection,
54
+ DocumentPreview,
55
+ DocumentPreviewPage,
56
+ DocumentRef,
57
+ IndexingJobStatus,
58
+ IndexingResult,
59
+ IndexingStatus,
60
+ SearchHistory,
61
+ SearchHistoryAnswer,
62
+ SearchHit,
63
+ SearchResponse,
64
+ UploadResult,
65
+ )
66
+ from sse_lib.options import (
67
+ DEFAULT_INDEX_TYPE,
68
+ INDEX_TYPES,
69
+ ChatOptions,
70
+ GenerativeOptions,
71
+ IndexingOptions,
72
+ SearchOptions,
73
+ build_options,
74
+ text_document,
75
+ )
76
+ from sse_lib.transport import DEFAULT_TIMEOUT, DEFAULT_TOKEN_TYPE, Transport
77
+
78
+ __version__ = "0.1.0"
79
+
80
+ __all__ = [
81
+ "DEFAULT_INDEX_TYPE",
82
+ "DEFAULT_TIMEOUT",
83
+ "DEFAULT_TOKEN_TYPE",
84
+ "INDEX_TYPES",
85
+ "Answer",
86
+ "Chat",
87
+ "ChatHistory",
88
+ "ChatMessage",
89
+ "ChatOptions",
90
+ "ChatReply",
91
+ "Collection",
92
+ "DocumentPreview",
93
+ "DocumentPreviewPage",
94
+ "DocumentRef",
95
+ "GenerativeOptions",
96
+ "IndexingJobStatus",
97
+ "IndexingOptions",
98
+ "IndexingResult",
99
+ "IndexingStatus",
100
+ "SSEAPIError",
101
+ "SSEAuthenticationError",
102
+ "SSEClient",
103
+ "SSEConfigError",
104
+ "SSEError",
105
+ "SSEHTTPError",
106
+ "SSETransportError",
107
+ "SSEValueError",
108
+ "SearchHistory",
109
+ "SearchHistoryAnswer",
110
+ "SearchHit",
111
+ "SearchOptions",
112
+ "SearchResponse",
113
+ "Transport",
114
+ "UploadResult",
115
+ "build_options",
116
+ "endpoints",
117
+ "text_document",
118
+ "__version__",
119
+ ]