docforge-sdk 0.24.0__tar.gz → 0.26.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.
Files changed (80) hide show
  1. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/PKG-INFO +1 -1
  2. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/__init__.py +14 -0
  3. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/_transport_async.py +24 -0
  4. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/_transport_sync.py +24 -0
  5. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/_version.py +1 -1
  6. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/__init__.py +14 -0
  7. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/explorer.py +106 -0
  8. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/search.py +95 -1
  9. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/documents.py +24 -8
  10. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/explorer.py +215 -14
  11. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/search.py +58 -2
  12. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/openapi_snapshot.json +596 -7
  13. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/parity_map.py +12 -0
  14. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/route_map.py +3 -0
  15. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_documents.py +12 -0
  16. docforge_sdk-0.26.0/tests/unit/test_explorer.py +194 -0
  17. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_search.py +52 -1
  18. docforge_sdk-0.24.0/tests/unit/test_explorer.py +0 -97
  19. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/.gitignore +0 -0
  20. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/.python-version +0 -0
  21. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/LICENSE +0 -0
  22. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/README.md +0 -0
  23. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/_exceptions.py +0 -0
  24. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/_requestspec.py +0 -0
  25. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/_transport_base.py +0 -0
  26. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/client.py +0 -0
  27. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/_shared.py +0 -0
  28. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/audit.py +0 -0
  29. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/auth.py +0 -0
  30. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/blobs.py +0 -0
  31. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/capabilities.py +0 -0
  32. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/collections.py +0 -0
  33. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/corpus.py +0 -0
  34. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/documents.py +0 -0
  35. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/estimate.py +0 -0
  36. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/health.py +0 -0
  37. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/ir.py +0 -0
  38. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/jobs.py +0 -0
  39. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/pipelines.py +0 -0
  40. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/preview.py +0 -0
  41. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/reingest.py +0 -0
  42. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/snippets.py +0 -0
  43. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/storage.py +0 -0
  44. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/trace.py +0 -0
  45. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/models/transfers.py +0 -0
  46. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/py.typed +0 -0
  47. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/__init__.py +0 -0
  48. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/_base.py +0 -0
  49. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/audit.py +0 -0
  50. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/auth.py +0 -0
  51. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/blobs.py +0 -0
  52. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/capabilities.py +0 -0
  53. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/collections.py +0 -0
  54. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/corpus.py +0 -0
  55. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/health.py +0 -0
  56. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/jobs.py +0 -0
  57. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/pipelines.py +0 -0
  58. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/snippets.py +0 -0
  59. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/docforge_sdk/resources/transfers.py +0 -0
  60. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/pyproject.toml +0 -0
  61. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/__init__.py +0 -0
  62. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/check_schema_drift.py +0 -0
  63. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/live/__init__.py +0 -0
  64. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/live/test_openapi_parity_live.py +0 -0
  65. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/__init__.py +0 -0
  66. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_audit.py +0 -0
  67. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_auth.py +0 -0
  68. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_blobs.py +0 -0
  69. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_collections.py +0 -0
  70. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_corpus.py +0 -0
  71. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_health.py +0 -0
  72. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_jobs.py +0 -0
  73. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_models_offline_parity.py +0 -0
  74. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_pipelines.py +0 -0
  75. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_resource_parity.py +0 -0
  76. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_route_coverage.py +0 -0
  77. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_snippets.py +0 -0
  78. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_transfers.py +0 -0
  79. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/tests/unit/test_transport.py +0 -0
  80. {docforge_sdk-0.24.0 → docforge_sdk-0.26.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: docforge-sdk
3
- Version: 0.24.0
3
+ Version: 0.26.0
4
4
  Summary: Typed async + sync Python client for the DocForge REST API (standalone, zero server deps).
5
5
  Project-URL: Homepage, https://github.com/Florian-BARRE/docforge
6
6
  Project-URL: Repository, https://github.com/Florian-BARRE/docforge
@@ -72,12 +72,17 @@ from .models.estimate import (
72
72
  from .models.explorer import (
73
73
  BulkChunkEnabledPatch,
74
74
  BulkChunkEnabledResponse,
75
+ ChunkContext,
75
76
  ChunkEnabledPatch,
76
77
  ChunkEnabledResult,
77
78
  ChunkInfo,
79
+ ChunkPage,
80
+ ContextChunk,
78
81
  DocumentDetail,
79
82
  DocumentListItem,
83
+ DocumentOutline,
80
84
  MetadataValue,
85
+ OutlineHeading,
81
86
  PageInfo,
82
87
  )
83
88
 
@@ -134,6 +139,8 @@ from .models.preview import (
134
139
  # ------------------- Search models ------------------- #
135
140
  from .models.search import (
136
141
  BlockLocation,
142
+ ChunkBrowseRequest,
143
+ ChunkBrowseResponse,
137
144
  SearchHealthSummary,
138
145
  SearchHit,
139
146
  SearchRequest,
@@ -208,6 +215,11 @@ __all__ = [
208
215
  "DocumentDetail",
209
216
  "PageInfo",
210
217
  "ChunkInfo",
218
+ "ChunkPage",
219
+ "ChunkContext",
220
+ "ContextChunk",
221
+ "DocumentOutline",
222
+ "OutlineHeading",
211
223
  "ChunkEnabledPatch",
212
224
  "BulkChunkEnabledPatch",
213
225
  "ChunkEnabledResult",
@@ -225,6 +237,8 @@ __all__ = [
225
237
  "BlockLocation",
226
238
  "SearchHit",
227
239
  "SearchResponse",
240
+ "ChunkBrowseRequest",
241
+ "ChunkBrowseResponse",
228
242
  "SearchHealthSummary",
229
243
  # Preview
230
244
  "PreviewIrSummary",
@@ -91,6 +91,30 @@ class AsyncTransport(_TransportBase):
91
91
  self._raise_for_status(response)
92
92
  return self._parse(response, model)
93
93
 
94
+ async def request_with_total(self, spec: RequestSpec, model: type[T]) -> tuple[T, int | None]:
95
+ """
96
+ Execute a request spec and also return the ``X-Total-Count`` header it carried.
97
+
98
+ Args:
99
+ spec (RequestSpec): The request description.
100
+ model (type[T]): The target type to validate the response into.
101
+
102
+ Returns:
103
+ tuple[T, int | None]: The validated body and the integer total, or None when the
104
+ header is absent or not an integer (older servers).
105
+ """
106
+ response = await self._send(
107
+ spec.method,
108
+ self._url(spec.path),
109
+ params=self._clean(spec.params),
110
+ json=spec.json,
111
+ files=spec.files,
112
+ )
113
+ self._raise_for_status(response)
114
+ raw_total = response.headers.get("x-total-count", "")
115
+ total = int(raw_total) if raw_total.isdigit() else None
116
+ return self._parse(response, model), total
117
+
94
118
  async def request_bare(self, spec: RequestSpec, model: type[T]) -> T:
95
119
  """
96
120
  Execute a request spec against the BARE origin (outside ``/api/v1``) and parse it.
@@ -91,6 +91,30 @@ class SyncTransport(_TransportBase):
91
91
  self._raise_for_status(response)
92
92
  return self._parse(response, model)
93
93
 
94
+ def request_with_total(self, spec: RequestSpec, model: type[T]) -> tuple[T, int | None]:
95
+ """
96
+ Execute a request spec and also return the ``X-Total-Count`` header it carried.
97
+
98
+ Args:
99
+ spec (RequestSpec): The request description.
100
+ model (type[T]): The target type to validate the response into.
101
+
102
+ Returns:
103
+ tuple[T, int | None]: The validated body and the integer total, or None when the
104
+ header is absent or not an integer (older servers).
105
+ """
106
+ response = self._send(
107
+ spec.method,
108
+ self._url(spec.path),
109
+ params=self._clean(spec.params),
110
+ json=spec.json,
111
+ files=spec.files,
112
+ )
113
+ self._raise_for_status(response)
114
+ raw_total = response.headers.get("x-total-count", "")
115
+ total = int(raw_total) if raw_total.isdigit() else None
116
+ return self._parse(response, model), total
117
+
94
118
  def request_bare(self, spec: RequestSpec, model: type[T]) -> T:
95
119
  """
96
120
  Execute a request spec against the BARE origin (outside ``/api/v1``) and parse it.
@@ -2,4 +2,4 @@
2
2
  # Single source of truth for the package version. Hatchling reads ``__version__`` from this file to
3
3
  # populate the distribution metadata (see ``[tool.hatch.version]`` in pyproject.toml).
4
4
 
5
- __version__ = "0.24.0"
5
+ __version__ = "0.26.0"
@@ -84,12 +84,17 @@ from .estimate import (
84
84
  from .explorer import (
85
85
  BulkChunkEnabledPatch,
86
86
  BulkChunkEnabledResponse,
87
+ ChunkContext,
87
88
  ChunkEnabledPatch,
88
89
  ChunkEnabledResult,
89
90
  ChunkInfo,
91
+ ChunkPage,
92
+ ContextChunk,
90
93
  DocumentDetail,
91
94
  DocumentListItem,
95
+ DocumentOutline,
92
96
  MetadataValue,
97
+ OutlineHeading,
93
98
  PageInfo,
94
99
  )
95
100
 
@@ -164,6 +169,8 @@ from .preview import (
164
169
  # ------------------- Search models ------------------- #
165
170
  from .search import (
166
171
  BlockLocation,
172
+ ChunkBrowseRequest,
173
+ ChunkBrowseResponse,
167
174
  SearchHealthSummary,
168
175
  SearchHint,
169
176
  SearchHit,
@@ -269,6 +276,11 @@ __all__ = [
269
276
  "DocumentDetail",
270
277
  "PageInfo",
271
278
  "ChunkInfo",
279
+ "ChunkPage",
280
+ "ChunkContext",
281
+ "ContextChunk",
282
+ "DocumentOutline",
283
+ "OutlineHeading",
272
284
  "ChunkEnabledPatch",
273
285
  "BulkChunkEnabledPatch",
274
286
  "ChunkEnabledResult",
@@ -287,6 +299,8 @@ __all__ = [
287
299
  "SearchHint",
288
300
  "SearchHit",
289
301
  "SearchResponse",
302
+ "ChunkBrowseRequest",
303
+ "ChunkBrowseResponse",
290
304
  "SearchHealthSummary",
291
305
  # Jobs
292
306
  "JobStatus",
@@ -236,6 +236,107 @@ class ChunkInfo(BaseModel):
236
236
  )
237
237
 
238
238
 
239
+ class ChunkPage(BaseModel):
240
+ """
241
+ One page of a document's chunks together with the document's total chunk count.
242
+
243
+ SDK-only wrapper (not an OpenAPI schema): the REST body is a bare chunk array and the total
244
+ travels in the ``X-Total-Count`` response header.
245
+
246
+ Attributes:
247
+ items (list[ChunkInfo]): The chunks of this page, in chunk_index order.
248
+ total (int | None): The document's total chunk count; None against a server that predates
249
+ the header.
250
+ """
251
+
252
+ items: list[ChunkInfo] = Field(description="The chunks of this page, in chunk_index order.")
253
+ total: int | None = Field(
254
+ default=None, description="The document's total chunk count (X-Total-Count header)."
255
+ )
256
+
257
+
258
+ class OutlineHeading(BaseModel):
259
+ """
260
+ One heading of a document's table of contents.
261
+
262
+ Attributes:
263
+ level (int): Heading depth (1 = top level).
264
+ text (str): The heading text.
265
+ page_number (int | None): 1-based page the heading sits on; None for page-less documents.
266
+ chunk_id (str | None): The first chunk of this section; None when no chunk carries it.
267
+ """
268
+
269
+ level: int = Field(description="Heading depth (1 = top level; 1 when the parser set none).")
270
+ text: str = Field(description="The heading text.")
271
+ page_number: int | None = Field(
272
+ default=None, description="1-based page the heading sits on; null for a page-less document."
273
+ )
274
+ chunk_id: str | None = Field(
275
+ default=None, description="The first chunk of this section; null when none carries it."
276
+ )
277
+
278
+
279
+ class DocumentOutline(BaseModel):
280
+ """
281
+ A document's heading tree - the cheap table of contents to read before fetching content.
282
+
283
+ Attributes:
284
+ document_id (str): The document's UUID.
285
+ display_title (str): The title to show ('' when none).
286
+ page_count (int | None): Pages of the document; None before parse / for page-less formats.
287
+ headings (list[OutlineHeading]): Every heading, in reading order.
288
+ """
289
+
290
+ document_id: str = Field(description="The document's UUID.")
291
+ display_title: str = Field(description="The title to show ('' when none).")
292
+ page_count: int | None = Field(default=None, description="Pages of the document.")
293
+ headings: list[OutlineHeading] = Field(
294
+ default_factory=list, description="Every heading, in reading order ([] when none)."
295
+ )
296
+
297
+
298
+ class ContextChunk(BaseModel):
299
+ """
300
+ One chunk of a context window (lean projection of a chunk).
301
+
302
+ Attributes:
303
+ chunk_id (str): The chunk UUID.
304
+ chunk_index (int): Position of the chunk within its document.
305
+ text (str): The chunk's (enriched) text.
306
+ page_number (int | None): 1-based page of the chunk's leading block; None when unlocated.
307
+ heading_path (list[str]): The chunk's section breadcrumb (outer to inner).
308
+ token_count (int): Token length of the text.
309
+ is_target (bool): True for the chunk the window was requested around.
310
+ """
311
+
312
+ chunk_id: str = Field(description="The chunk UUID.")
313
+ chunk_index: int = Field(description="Position of the chunk within its document.")
314
+ text: str = Field(description="The chunk's (enriched) text.")
315
+ page_number: int | None = Field(default=None, description="1-based page; null when unlocated.")
316
+ heading_path: list[str] = Field(
317
+ default_factory=list, description="The chunk's section breadcrumb (outer to inner)."
318
+ )
319
+ token_count: int = Field(description="Token length of the text.")
320
+ is_target: bool = Field(description="True for the chunk the window was requested around.")
321
+
322
+
323
+ class ChunkContext(BaseModel):
324
+ """
325
+ A chunk and its neighbours within the same document (a reading window around a search hit).
326
+
327
+ Attributes:
328
+ document_id (str): The owning document's UUID.
329
+ display_title (str): The owning document's display title.
330
+ chunks (list[ContextChunk]): The window in chunk_index order, the target flagged.
331
+ """
332
+
333
+ document_id: str = Field(description="The owning document's UUID.")
334
+ display_title: str = Field(description="The owning document's display title.")
335
+ chunks: list[ContextChunk] = Field(
336
+ description="The window in chunk_index order, the target flagged is_target."
337
+ )
338
+
339
+
239
340
  class ChunkEnabledPatch(BaseModel):
240
341
  """
241
342
  The desired searchability state for one chunk (its enabled_override).
@@ -321,6 +422,11 @@ __all__ = [
321
422
  "DocumentDetail",
322
423
  "PageInfo",
323
424
  "ChunkInfo",
425
+ "ChunkPage",
426
+ "OutlineHeading",
427
+ "DocumentOutline",
428
+ "ContextChunk",
429
+ "ChunkContext",
324
430
  "ChunkEnabledPatch",
325
431
  "BulkChunkEnabledPatch",
326
432
  "ChunkEnabledResult",
@@ -44,13 +44,25 @@ class SearchRequest(BaseModel):
44
44
  return_fields (list[str] | None): Hit fields to return (lean response). None → the full hit.
45
45
  group_by (Literal["document"] | None): ``"document"`` caps hits per document.
46
46
  max_per_document (int): The per-document cap under ``group_by`` (1-10).
47
+ min_score (float | None): Drop hits scoring below this (scale = the response's score_kind).
48
+ rerank (bool | None): Per-request override of the rerank stage (None = the pipeline decides).
49
+ fusion (Literal["rrf", "dbsf"] | None): Per-request override of the dense/sparse fusion.
50
+ debug (bool): Add each hit's ``fusion_score`` / ``rerank_score``.
47
51
  """
48
52
 
49
53
  query: str = Field(min_length=1, description="The natural-language query to search for.")
50
54
  limit: int = Field(default=10, ge=1, le=100, description="Number of fused results.")
51
55
  filters: dict[str, Any] | None = Field(
52
56
  default=None,
53
- description="Constraints on the FILTERABLE metadata fields (field → value or [values]).",
57
+ description="Metadata filters, ANDed across fields: {field: value}. A scalar = equality "
58
+ "(string/enum/keyword_list match case-insensitively); a list = any-of (max 100); an object "
59
+ '= ONE operator: {"eq": v}, {"in": [..]}, {"not": v}, {"not_in": [..]}, '
60
+ '{"contains": "sub"} / {"prefix": "pre"} (string-ish fields: case-insensitive '
61
+ 'match against stored values; text fields: contains = full-text), {"exists": true|false}, '
62
+ 'or range bounds {"gte","gt","lte","lt"} on integer/float/datetime fields '
63
+ "(datetime bounds = ISO-8601 strings). Only filterable fields; an operator not valid for the "
64
+ "field type -> 422 listing the valid ones. A value no document stores yields a response hint "
65
+ "with the closest stored values.",
54
66
  )
55
67
  search_in: list[SearchTarget] | None = Field(
56
68
  default=None,
@@ -79,6 +91,26 @@ class SearchRequest(BaseModel):
79
91
  description="Per-document hit cap when group_by='document' (default 1). Only valid "
80
92
  "together with group_by.",
81
93
  )
94
+ min_score: float | None = Field(
95
+ default=None,
96
+ ge=0.0,
97
+ description="Drop hits whose final score is below this threshold (on the scale named by "
98
+ "the response's score_kind). None -> no threshold.",
99
+ )
100
+ rerank: bool | None = Field(
101
+ default=None,
102
+ description="Override the collection's rerank stage for this request: false skips it, "
103
+ "true requires it (422 when the search pipeline has none). None -> the pipeline decides.",
104
+ )
105
+ fusion: Literal["rrf", "dbsf"] | None = Field(
106
+ default=None,
107
+ description="Override how the dense and sparse branches are fused for this request: 'rrf' "
108
+ "(rank-based) or 'dbsf' (distribution-based). None -> the collection's configured fusion.",
109
+ )
110
+ debug: bool = Field(
111
+ default=False,
112
+ description="True -> each hit also carries fusion_score and, when reranked, rerank_score.",
113
+ )
82
114
 
83
115
 
84
116
  class BlockLocation(BaseModel):
@@ -172,6 +204,16 @@ class SearchHit(BaseModel):
172
204
  default_factory=list,
173
205
  description="Every source block's page + NORMALISED bbox — draw one box per block.",
174
206
  )
207
+ fusion_score: float | None = Field(
208
+ default=None,
209
+ description="The retrieval fusion score before any rerank (only when the request set "
210
+ "debug=true).",
211
+ )
212
+ rerank_score: float | None = Field(
213
+ default=None,
214
+ description="The cross-encoder rerank score (only when debug=true and a rerank stage "
215
+ "re-scored the hit).",
216
+ )
175
217
 
176
218
 
177
219
  class SearchCost(BaseModel):
@@ -254,6 +296,56 @@ class SearchResponse(BaseModel):
254
296
  )
255
297
 
256
298
 
299
+ class ChunkBrowseRequest(BaseModel):
300
+ """
301
+ List a collection's chunks by filter, without a query.
302
+
303
+ Attributes:
304
+ filters (dict[str, Any] | None): The same filter map as search; None -> every chunk.
305
+ limit (int): Page size (1-200).
306
+ cursor (str | None): The previous page's ``next_cursor``; None -> the first page.
307
+ return_fields (list[str] | None): Chunk fields to return (as search; ``score`` is not one).
308
+ """
309
+
310
+ filters: dict[str, Any] | None = Field(
311
+ default=None,
312
+ description="Constraints on the FILTERABLE metadata fields - exactly the grammar of search "
313
+ "`filters`. None -> every searchable chunk of the collection.",
314
+ )
315
+ limit: int = Field(default=20, ge=1, le=200, description="Chunks per page (1-200).")
316
+ cursor: str | None = Field(
317
+ default=None,
318
+ description="Opaque cursor: the previous page's `next_cursor`, sent back unchanged with the "
319
+ "SAME filters. None -> the first page.",
320
+ )
321
+ return_fields: list[str] | None = Field(
322
+ default=None,
323
+ max_length=64,
324
+ description="The chunk fields to return - any search hit field except score, or "
325
+ "'metadata.<field>'. chunk_id and document_id are ALWAYS returned. None -> every field "
326
+ "except geometry (page_number is kept). Omitted fields are absent (not null).",
327
+ )
328
+
329
+
330
+ class ChunkBrowseResponse(BaseModel):
331
+ """
332
+ One page of browsed chunks.
333
+
334
+ Attributes:
335
+ chunks (list[SearchHit]): The page, ordered by document id then chunk_index (no score).
336
+ next_cursor (str | None): Pass back as ``cursor`` for the next page; None on the last page.
337
+ hints (list[SearchHint]): Filter hints (a value nothing stores; the culprit of an empty page).
338
+ """
339
+
340
+ chunks: list[SearchHit] = Field(
341
+ default_factory=list, description="The page's chunks in browse order."
342
+ )
343
+ next_cursor: str | None = Field(
344
+ default=None, description="Cursor of the next page; null on the last page."
345
+ )
346
+ hints: list[SearchHint] = Field(default_factory=list, description="Filter hints, as search.")
347
+
348
+
257
349
  class SearchHealthSummary(BaseModel):
258
350
  """
259
351
  A compact, tile-friendly roll-up of search-runtime health for the deployment cockpit.
@@ -297,5 +389,7 @@ __all__ = [
297
389
  "SearchCost",
298
390
  "SearchHint",
299
391
  "SearchResponse",
392
+ "ChunkBrowseRequest",
393
+ "ChunkBrowseResponse",
300
394
  "SearchHealthSummary",
301
395
  ]
@@ -212,7 +212,9 @@ class AsyncDocuments(AsyncResource, _DocumentsSpecs):
212
212
  self._purge_trace_payloads_spec(document_id), TracePurgeResult
213
213
  )
214
214
 
215
- async def get_markdown(self, document_id: str, download: bool = False) -> DocumentView:
215
+ async def get_markdown(
216
+ self, document_id: str, download: bool = False, pages: str | None = None
217
+ ) -> DocumentView:
216
218
  """
217
219
  Render a document as an on-the-fly markdown view generated from the canonical IR.
218
220
 
@@ -220,16 +222,20 @@ class AsyncDocuments(AsyncResource, _DocumentsSpecs):
220
222
  document_id (str): The document to render.
221
223
  download (bool): When true, ask the server for the attachment-style response (the caller
222
224
  still only gets the text back; the flag only affects the server-side response header).
225
+ pages (str | None): Render only these 1-based pages ('5', '5-7', '5,7-9'); the whole
226
+ document when None (the parameter is then not sent at all).
223
227
 
224
228
  Returns:
225
229
  DocumentView: The rendered markdown body and its ``text/markdown`` content type.
226
230
  """
227
231
  content, mime_type = await self._transport.get_text_typed(
228
- self._markdown_path(document_id), params={"download": download}
232
+ self._markdown_path(document_id), params={"download": download, "pages": pages}
229
233
  )
230
234
  return DocumentView(content=content, mime_type=mime_type)
231
235
 
232
- async def get_html(self, document_id: str, download: bool = False) -> DocumentView:
236
+ async def get_html(
237
+ self, document_id: str, download: bool = False, pages: str | None = None
238
+ ) -> DocumentView:
233
239
  """
234
240
  Render a document as an on-the-fly HTML view generated from the canonical IR.
235
241
 
@@ -237,12 +243,14 @@ class AsyncDocuments(AsyncResource, _DocumentsSpecs):
237
243
  document_id (str): The document to render.
238
244
  download (bool): When true, ask the server for the attachment-style response (the caller
239
245
  still only gets the text back; the flag only affects the server-side response header).
246
+ pages (str | None): Render only these 1-based pages ('5', '5-7', '5,7-9'); the whole
247
+ document when None (the parameter is then not sent at all).
240
248
 
241
249
  Returns:
242
250
  DocumentView: The rendered HTML body and its ``text/html`` content type.
243
251
  """
244
252
  content, mime_type = await self._transport.get_text_typed(
245
- self._html_path(document_id), params={"download": download}
253
+ self._html_path(document_id), params={"download": download, "pages": pages}
246
254
  )
247
255
  return DocumentView(content=content, mime_type=mime_type)
248
256
 
@@ -303,7 +311,9 @@ class SyncDocuments(SyncResource, _DocumentsSpecs):
303
311
  self._purge_trace_payloads_spec(document_id), TracePurgeResult
304
312
  )
305
313
 
306
- def get_markdown(self, document_id: str, download: bool = False) -> DocumentView:
314
+ def get_markdown(
315
+ self, document_id: str, download: bool = False, pages: str | None = None
316
+ ) -> DocumentView:
307
317
  """
308
318
  Render a document as an on-the-fly markdown view generated from the canonical IR.
309
319
 
@@ -311,16 +321,20 @@ class SyncDocuments(SyncResource, _DocumentsSpecs):
311
321
  document_id (str): The document to render.
312
322
  download (bool): When true, ask the server for the attachment-style response (the caller
313
323
  still only gets the text back; the flag only affects the server-side response header).
324
+ pages (str | None): Render only these 1-based pages ('5', '5-7', '5,7-9'); the whole
325
+ document when None (the parameter is then not sent at all).
314
326
 
315
327
  Returns:
316
328
  DocumentView: The rendered markdown body and its ``text/markdown`` content type.
317
329
  """
318
330
  content, mime_type = self._transport.get_text_typed(
319
- self._markdown_path(document_id), params={"download": download}
331
+ self._markdown_path(document_id), params={"download": download, "pages": pages}
320
332
  )
321
333
  return DocumentView(content=content, mime_type=mime_type)
322
334
 
323
- def get_html(self, document_id: str, download: bool = False) -> DocumentView:
335
+ def get_html(
336
+ self, document_id: str, download: bool = False, pages: str | None = None
337
+ ) -> DocumentView:
324
338
  """
325
339
  Render a document as an on-the-fly HTML view generated from the canonical IR.
326
340
 
@@ -328,12 +342,14 @@ class SyncDocuments(SyncResource, _DocumentsSpecs):
328
342
  document_id (str): The document to render.
329
343
  download (bool): When true, ask the server for the attachment-style response (the caller
330
344
  still only gets the text back; the flag only affects the server-side response header).
345
+ pages (str | None): Render only these 1-based pages ('5', '5-7', '5,7-9'); the whole
346
+ document when None (the parameter is then not sent at all).
331
347
 
332
348
  Returns:
333
349
  DocumentView: The rendered HTML body and its ``text/html`` content type.
334
350
  """
335
351
  content, mime_type = self._transport.get_text_typed(
336
- self._html_path(document_id), params={"download": download}
352
+ self._html_path(document_id), params={"download": download, "pages": pages}
337
353
  )
338
354
  return DocumentView(content=content, mime_type=mime_type)
339
355