Skip to content

Commit 4318eae

Browse files
committed
✨ add API for RAG operations (#464)
1 parent ddb6c0a commit 4318eae

43 files changed

Lines changed: 1166 additions & 132 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/_test-units.yml‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@ jobs:
5959
- "windows-2022"
6060
python-version:
6161
- "3.10"
62-
- "3.11"
63-
- "3.12"
64-
- "3.13"
6562
- "3.14"
6663
runs-on: ${{ matrix.os }}
6764
steps:

‎mindee/v2/client.py‎

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import warnings
22
from time import sleep
3-
from typing import TypeVar
43

54
import httpx
65

@@ -11,20 +10,25 @@
1110
from mindee.logger import logger
1211
from mindee.mindee_http.cancellation_token import CancellationToken
1312
from mindee.parsing.common.common_response import CommonStatus
13+
from mindee.v2.client_options.base_annotation_parameters import BaseAnnotationParameters
1414
from mindee.v2.client_options.base_product_parameters import BaseProductParameters
15+
from mindee.v2.client_options.base_rag_document_upload_parameters import (
16+
BaseRagDocumentUploadParameters,
17+
)
1518
from mindee.v2.client_options.base_search_parameters import (
1619
BaseSearchParameters,
1720
TypeSearchResponse,
1821
)
1922
from mindee.v2.mindee_http.mindee_api_v2 import MindeeAPIV2
20-
from mindee.v2.parsing.inference.base_inference_response import BaseInferenceResponse
23+
from mindee.v2.parsing.base_rag_annotation_response import (
24+
TypeRagAnnotationResponse,
25+
)
26+
from mindee.v2.parsing.inference.base_inference_response import (
27+
TypeBaseInferenceResponse,
28+
)
2129
from mindee.v2.parsing.job.job_response import JobResponse
2230
from mindee.v2.parsing.search.search_response import SearchResponse
2331

24-
TypeBaseInferenceResponse = TypeVar(
25-
"TypeBaseInferenceResponse", bound=BaseInferenceResponse
26-
)
27-
2832

2933
class Client:
3034
"""
@@ -169,6 +173,143 @@ def enqueue_and_get_result(
169173

170174
raise MindeeError(f"Couldn't retrieve document after {try_counter + 1} tries.")
171175

176+
def upload_rag_document(
177+
self,
178+
input_source: LocalInputSource,
179+
parameters: BaseRagDocumentUploadParameters[TypeRagAnnotationResponse],
180+
) -> TypeRagAnnotationResponse:
181+
"""
182+
Not recommended for general use, prefer ``upload_and_get_rag_document``.
183+
You will need to poll until the document is ready for use.
184+
Add a document to the RAG database.
185+
"""
186+
return self.mindee_api.req_post_rag_document(input_source, parameters)
187+
188+
def upload_and_get_rag_document(
189+
self,
190+
input_source: LocalInputSource,
191+
parameters: BaseRagDocumentUploadParameters[TypeRagAnnotationResponse],
192+
polling_options: PollingOptions | None = None,
193+
cancellation_token: CancellationToken | None = None,
194+
) -> TypeRagAnnotationResponse:
195+
"""
196+
Add a document to the RAG database and return the initial annotation.
197+
"""
198+
initial_response = self.upload_rag_document(input_source, parameters)
199+
if initial_response.status != "Processing":
200+
return initial_response
201+
if polling_options is None:
202+
polling_options = PollingOptions()
203+
return self._poll_for_rag_document(
204+
initial_response, polling_options, cancellation_token
205+
)
206+
207+
def get_rag_document(
208+
self, response_type: type[TypeRagAnnotationResponse], document_id: str
209+
) -> TypeRagAnnotationResponse:
210+
"""
211+
Not recommended for general use, prefer ``get_ready_rag_document``.
212+
You will need to poll until the document is ready for use.
213+
Get a document's info and annotations from the RAG database.
214+
"""
215+
return self.mindee_api.req_get_rag_annotation(response_type, document_id)
216+
217+
def get_ready_rag_document(
218+
self,
219+
response_type: type[TypeRagAnnotationResponse],
220+
document_id: str,
221+
polling_options: PollingOptions | None = None,
222+
cancellation_token: CancellationToken | None = None,
223+
):
224+
"""
225+
Get a document's info and annotations from the RAG database.
226+
"""
227+
initial_response = self.get_rag_document(response_type, document_id)
228+
if initial_response.status != "Processing":
229+
return initial_response
230+
if polling_options is None:
231+
polling_options = PollingOptions()
232+
return self._poll_for_rag_document(
233+
initial_response, polling_options, cancellation_token
234+
)
235+
236+
def update_rag_annotations(
237+
self, parameters: BaseAnnotationParameters[TypeRagAnnotationResponse]
238+
) -> TypeRagAnnotationResponse:
239+
"""
240+
Not recommended for general use, prefer ``update_and_get_rag_annotations``.
241+
You will need to poll until the document is ready for use.
242+
Update a document's annotations in the RAG database.
243+
"""
244+
return self.mindee_api.req_patch_rag_annotation(parameters)
245+
246+
def update_and_get_rag_annotations(
247+
self,
248+
parameters: BaseAnnotationParameters[TypeRagAnnotationResponse],
249+
polling_options: PollingOptions | None = None,
250+
cancellation_token: CancellationToken | None = None,
251+
) -> TypeRagAnnotationResponse:
252+
"""
253+
Update a document's annotations in the RAG database.
254+
"""
255+
initial_response = self.update_rag_annotations(parameters)
256+
if initial_response.status != "Processing":
257+
return initial_response
258+
if polling_options is None:
259+
polling_options = PollingOptions()
260+
return self._poll_for_rag_document(
261+
initial_response, polling_options, cancellation_token
262+
)
263+
264+
def delete_extraction_rag_document(self, document_id: str) -> bool:
265+
"""
266+
Delete a document from the RAG database.
267+
For extraction models only.
268+
"""
269+
return self.mindee_api.req_delete_extraction_rag_document(document_id)
270+
271+
def _poll_for_rag_document(
272+
self,
273+
initial_response: TypeRagAnnotationResponse,
274+
polling_options: PollingOptions,
275+
cancellation_token: CancellationToken | None = None,
276+
) -> TypeRagAnnotationResponse:
277+
"""
278+
Poll until the document is finished processing or the max number of attempts is reached.
279+
"""
280+
logger.info("Polling for RAG document ID: %s", initial_response.id)
281+
polling_options.validate_settings()
282+
max_retries = polling_options.max_retries + 1
283+
284+
logger.debug(
285+
"Waiting %s seconds before attempting to retrieve the result...",
286+
polling_options.initial_delay_sec,
287+
)
288+
289+
if cancellation_token and cancellation_token.is_canceled:
290+
raise MindeeError("Request canceled through cancellation token.")
291+
292+
sleep(polling_options.initial_delay_sec)
293+
document_id = initial_response.id
294+
retry_count = 1
295+
296+
while retry_count < max_retries:
297+
if cancellation_token and cancellation_token.is_canceled:
298+
raise MindeeError("Request canceled through cancellation token.")
299+
logger.info("Poll attempt %s of %s", retry_count, max_retries)
300+
301+
response = self.get_rag_document(type(initial_response), document_id)
302+
retry_count += 1
303+
304+
if response.status == "Processing":
305+
sleep(polling_options.delay_sec)
306+
continue
307+
if response.status == "Failed":
308+
raise MindeeError("Job failed without an error payload.")
309+
return response
310+
311+
raise MindeeError(f"RAG polling not complete after {retry_count - 1} attempts.")
312+
172313
def search(
173314
self, params: BaseSearchParameters[TypeSearchResponse]
174315
) -> TypeSearchResponse:
@@ -183,7 +324,7 @@ def search_models(
183324
self, name: str | None = None, model_type: str | None = None
184325
) -> SearchResponse:
185326
"""
186-
Deprecated. Use `search` instead.
327+
Deprecated: use `search` instead.
187328
"""
188329
warnings.warn(
189330
"search_models is deprecated, use search instead.",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from abc import ABC, abstractmethod
2+
from dataclasses import dataclass
3+
from typing import Generic
4+
5+
from mindee.parsing.common.string_dict import StringDict
6+
from mindee.v2.parsing.base_rag_annotation_response import TypeRagAnnotationResponse
7+
8+
9+
@dataclass(kw_only=True)
10+
class BaseAnnotationParameters(ABC, Generic[TypeRagAnnotationResponse]):
11+
"""Base parameters for document annotations."""
12+
13+
# Note: DocumentId is included in the request URL path, it is not a parameter.
14+
document_id: str
15+
"""UID of the annotated document."""
16+
17+
_response_class: type[TypeRagAnnotationResponse]
18+
"""Response class for the annotation."""
19+
20+
@abstractmethod
21+
def get_request_parameters(self) -> dict[str, str | StringDict]:
22+
"""Gets the request parameters for the upload request."""
23+
24+
def get_response_class(self) -> type[TypeRagAnnotationResponse]:
25+
"""Gets the response class for the search."""
26+
return self._response_class

‎mindee/v2/client_options/base_product_parameters.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class BaseProductParameters(ABC):
2929
"""Options for polling. Set only if having timeout issues."""
3030

3131
close_file: bool = True
32-
"""Whether to close the file after product."""
32+
"""Whether to close the file after uploading. Default: True."""
3333

3434
_slug: ClassVar[str]
3535
"""Slug of the product."""
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from abc import ABC
2+
from dataclasses import dataclass
3+
from typing import Generic
4+
5+
from mindee.v2.parsing.base_rag_annotation_response import TypeRagAnnotationResponse
6+
7+
8+
@dataclass(kw_only=True)
9+
class BaseRagDocumentUploadParameters(ABC, Generic[TypeRagAnnotationResponse]):
10+
"""Base parameters for document upload operations."""
11+
12+
model_id: str
13+
"""UUID of the model that the uploaded RAG document is linked to."""
14+
15+
close_file: bool = True
16+
"""Whether to close the file after uploading. Default: True."""
17+
18+
_response_class: type[TypeRagAnnotationResponse]
19+
"""Response class for the annotation."""
20+
21+
def get_request_parameters(self) -> dict[str, str]:
22+
"""Gets the request parameters for the upload request."""
23+
return {"model_id": self.model_id}
24+
25+
def get_response_class(self) -> type[TypeRagAnnotationResponse]:
26+
"""Gets the response class for the search."""
27+
return self._response_class

‎mindee/v2/client_options/base_search_parameters.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from abc import ABC
22
from dataclasses import dataclass
3-
from typing import ClassVar, Generic, TypeVar
3+
from typing import ClassVar, Generic
44

5-
from mindee.v2.parsing.search.base_search_response import BaseSearchResponse
6-
7-
TypeSearchResponse = TypeVar("TypeSearchResponse", bound=BaseSearchResponse)
5+
from mindee.v2.parsing.search.base_search_response import TypeSearchResponse
86

97

108
@dataclass(kw_only=True)

‎mindee/v2/error/mindee_http_error_v2.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22

3+
from mindee.logger import logger
34
from mindee.parsing.common.string_dict import StringDict
45
from mindee.v2.parsing.error import ErrorItem, ErrorResponse, IErrorResponse
56

@@ -48,4 +49,6 @@ def handle_error_v2(raw_response: StringDict) -> None:
4849
"""
4950
if "status" not in raw_response or "detail" not in raw_response:
5051
raise MindeeHTTPUnknownErrorV2(json.dumps(raw_response, indent=2))
51-
raise MindeeHTTPErrorV2(ErrorResponse(raw_response))
52+
error_response = ErrorResponse(raw_response)
53+
logger.error("\n%s", error_response)
54+
raise MindeeHTTPErrorV2(error_response)

0 commit comments

Comments
 (0)