vectoramp 0.1.0__py3-none-any.whl
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.
- vectoramp/__init__.py +45 -0
- vectoramp/client.py +133 -0
- vectoramp/connections.py +123 -0
- vectoramp/embeddings.py +39 -0
- vectoramp/exceptions.py +22 -0
- vectoramp/py.typed +0 -0
- vectoramp/resources.py +1531 -0
- vectoramp/sources.py +506 -0
- vectoramp/transport.py +198 -0
- vectoramp/types.py +145 -0
- vectoramp-0.1.0.dist-info/METADATA +464 -0
- vectoramp-0.1.0.dist-info/RECORD +15 -0
- vectoramp-0.1.0.dist-info/WHEEL +4 -0
- vectoramp-0.1.0.dist-info/licenses/LICENSE +201 -0
- vectoramp-0.1.0.dist-info/licenses/NOTICE +8 -0
vectoramp/resources.py
ADDED
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
"""Resource clients for the VectorAmp API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import ItemsView, KeysView, ValuesView
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Iterator, Mapping, Optional, Sequence, Union
|
|
10
|
+
|
|
11
|
+
from .embeddings import EMBEDDING_DIMENSIONS, VECTORAMP_EMBEDDING_4B
|
|
12
|
+
from .sources import (
|
|
13
|
+
ConfluenceSource,
|
|
14
|
+
FileUploadSource,
|
|
15
|
+
GCSSource,
|
|
16
|
+
GoogleDriveSource,
|
|
17
|
+
JiraSource,
|
|
18
|
+
S3Source,
|
|
19
|
+
SourceBuilder,
|
|
20
|
+
SourceInput,
|
|
21
|
+
WebSource,
|
|
22
|
+
)
|
|
23
|
+
from .transport import BaseTransport, RestTransport
|
|
24
|
+
from .types import JSON, AdvancedFilter, ConversationTurn, Filters, Metric, Vector, VectorId
|
|
25
|
+
|
|
26
|
+
PathLike = Union[str, Path]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Dataset:
|
|
30
|
+
"""Dataset resource object returned by create/get/list calls.
|
|
31
|
+
|
|
32
|
+
The object keeps the raw API payload while exposing instance methods that
|
|
33
|
+
delegate to the existing service-style clients. It also implements the
|
|
34
|
+
common mapping helpers so existing ``dataset["id"]`` style code keeps
|
|
35
|
+
working.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, service: "DatasetsResource", raw_data: Mapping[str, Any]) -> None:
|
|
39
|
+
self.service = service
|
|
40
|
+
self.client = service.client
|
|
41
|
+
self.raw_data: JSON = dict(raw_data)
|
|
42
|
+
self.id = self._extract_id(self.raw_data)
|
|
43
|
+
|
|
44
|
+
def search(
|
|
45
|
+
self,
|
|
46
|
+
query: Optional[Union[str, Sequence[float]]] = None,
|
|
47
|
+
*,
|
|
48
|
+
vector: Optional[Sequence[float]] = None,
|
|
49
|
+
text: Optional[str] = None,
|
|
50
|
+
search_text: Optional[str] = None,
|
|
51
|
+
top_k: int = 10,
|
|
52
|
+
filters: Optional[Filters] = None,
|
|
53
|
+
advanced_filters: Optional[Sequence[AdvancedFilter]] = None,
|
|
54
|
+
embedding_provider: Optional[str] = None,
|
|
55
|
+
embedding_model: Optional[str] = None,
|
|
56
|
+
nprobe_override: Optional[int] = None,
|
|
57
|
+
rerank_depth_override: Optional[int] = None,
|
|
58
|
+
hybrid: Optional[bool] = None,
|
|
59
|
+
sparse_query: Optional[str] = None,
|
|
60
|
+
alpha: Optional[float] = None,
|
|
61
|
+
include_embeddings: Optional[bool] = None,
|
|
62
|
+
include_documents: Optional[bool] = None,
|
|
63
|
+
include_metadata: Optional[bool] = None,
|
|
64
|
+
rerank: Optional[Union[bool, Mapping[str, Any]]] = None,
|
|
65
|
+
) -> JSON:
|
|
66
|
+
"""Search this dataset by text or vector.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
query: Convenience query; ``str`` maps to text search and a float
|
|
70
|
+
sequence maps to vector search.
|
|
71
|
+
vector: Explicit vector query. Mutually exclusive with ``query`` and
|
|
72
|
+
``text``.
|
|
73
|
+
text: Explicit text query. Mutually exclusive with ``query`` and
|
|
74
|
+
``vector``.
|
|
75
|
+
search_text: Alias for ``text`` for single-field hybrid/BM25 UX.
|
|
76
|
+
top_k: Maximum matches to return. Defaults to ``10``.
|
|
77
|
+
filters: Exact-match metadata filters.
|
|
78
|
+
advanced_filters: API-native advanced metadata filters.
|
|
79
|
+
embedding_provider: Optional provider override for text queries.
|
|
80
|
+
embedding_model: Optional model override for text queries.
|
|
81
|
+
nprobe_override: Optional ANN probe override.
|
|
82
|
+
rerank_depth_override: Optional rerank depth override.
|
|
83
|
+
hybrid: Optional hybrid dense/sparse search toggle.
|
|
84
|
+
sparse_query: Optional sparse query text for hybrid search.
|
|
85
|
+
alpha: Optional dense/sparse weighting for hybrid search.
|
|
86
|
+
include_embeddings: Whether result vectors include embeddings.
|
|
87
|
+
include_documents: Whether result vectors include document text.
|
|
88
|
+
include_metadata: Whether result vectors include metadata; defaults to
|
|
89
|
+
API behavior when omitted.
|
|
90
|
+
rerank: Enable semantic reranking. Use ``True`` or ``{"enabled": True}``;
|
|
91
|
+
provider defaults to ``vectoramp`` and model to ``VectorAmp-Rerank-v1``.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Search response JSON.
|
|
95
|
+
"""
|
|
96
|
+
return self.service.search(
|
|
97
|
+
self.id,
|
|
98
|
+
query,
|
|
99
|
+
vector=vector,
|
|
100
|
+
text=text,
|
|
101
|
+
search_text=search_text,
|
|
102
|
+
top_k=top_k,
|
|
103
|
+
filters=filters,
|
|
104
|
+
advanced_filters=advanced_filters,
|
|
105
|
+
embedding_provider=embedding_provider,
|
|
106
|
+
embedding_model=embedding_model,
|
|
107
|
+
nprobe_override=nprobe_override,
|
|
108
|
+
rerank_depth_override=rerank_depth_override,
|
|
109
|
+
hybrid=hybrid,
|
|
110
|
+
sparse_query=sparse_query,
|
|
111
|
+
alpha=alpha,
|
|
112
|
+
include_embeddings=include_embeddings,
|
|
113
|
+
include_documents=include_documents,
|
|
114
|
+
include_metadata=include_metadata,
|
|
115
|
+
rerank=rerank,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def insert(self, vectors: Sequence[Vector]) -> JSON:
|
|
119
|
+
"""Insert vectors into this dataset.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
vectors: Vector records containing values plus optional id/metadata.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Insert response JSON.
|
|
126
|
+
"""
|
|
127
|
+
return self.service.insert_vectors(self.id, vectors)
|
|
128
|
+
|
|
129
|
+
def insert_vectors(self, vectors: Sequence[Vector]) -> JSON:
|
|
130
|
+
"""Insert vectors into this dataset.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
vectors: Vector records containing values plus optional id/metadata.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Insert response JSON.
|
|
137
|
+
"""
|
|
138
|
+
return self.service.insert_vectors(self.id, vectors)
|
|
139
|
+
|
|
140
|
+
def add_texts(
|
|
141
|
+
self,
|
|
142
|
+
texts: Union[str, Sequence[str]],
|
|
143
|
+
*,
|
|
144
|
+
ids: Optional[Sequence[VectorId]] = None,
|
|
145
|
+
metadatas: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
146
|
+
) -> JSON:
|
|
147
|
+
"""Embed text values with the dataset model and insert them.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
texts: Single text or sequence of texts.
|
|
151
|
+
ids: Optional vector ids (string or integer). Integer ids are kept as
|
|
152
|
+
JSON numbers. Length must match ``texts`` when provided.
|
|
153
|
+
metadatas: Optional per-text metadata. Length must match ``texts``.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Insert response JSON.
|
|
157
|
+
"""
|
|
158
|
+
return self.service.add_texts(self.id, texts, ids=ids, metadatas=metadatas)
|
|
159
|
+
|
|
160
|
+
def delete(self) -> Any:
|
|
161
|
+
"""Delete this dataset and return the API response."""
|
|
162
|
+
return self.service.delete(self.id)
|
|
163
|
+
|
|
164
|
+
def list_documents(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
limit: int = 50,
|
|
168
|
+
cursor: Optional[str] = None,
|
|
169
|
+
status: Optional[str] = None,
|
|
170
|
+
) -> JSON:
|
|
171
|
+
"""List source documents retained for this dataset.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
limit: Maximum documents to return. Defaults to ``50``.
|
|
175
|
+
cursor: Cursor from a previous page's ``next_cursor``.
|
|
176
|
+
status: Optional document status filter.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Document page JSON from ``/datasets/{id}/documents``.
|
|
180
|
+
"""
|
|
181
|
+
return self.service.list_documents(self.id, limit=limit, cursor=cursor, status=status)
|
|
182
|
+
|
|
183
|
+
def download_document(self, document_id: str) -> bytes:
|
|
184
|
+
"""Download a retained source document as raw bytes.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
document_id: Source document identifier from :meth:`list_documents`.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Raw document bytes.
|
|
191
|
+
"""
|
|
192
|
+
return self.service.download_document(self.id, document_id)
|
|
193
|
+
|
|
194
|
+
def ask(
|
|
195
|
+
self,
|
|
196
|
+
query: str,
|
|
197
|
+
*,
|
|
198
|
+
top_k: int = 5,
|
|
199
|
+
conversation_history: Optional[Sequence[ConversationTurn]] = None,
|
|
200
|
+
include_sources: bool = True,
|
|
201
|
+
) -> JSON:
|
|
202
|
+
"""Ask a non-streaming RAG question scoped to this dataset.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
query: Natural-language question.
|
|
206
|
+
top_k: Number of retrieved chunks to consider. Defaults to ``5``.
|
|
207
|
+
conversation_history: Optional prior chat turns.
|
|
208
|
+
include_sources: Whether to include source chunks. Defaults to ``True``.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
JSON response from ``/intelligence/query``.
|
|
212
|
+
"""
|
|
213
|
+
if self.client is None:
|
|
214
|
+
raise TypeError("Dataset.ask requires a Dataset created by a VectorAmp client.")
|
|
215
|
+
return self.client.intelligence.query(
|
|
216
|
+
query,
|
|
217
|
+
dataset_id=self.id,
|
|
218
|
+
top_k=top_k,
|
|
219
|
+
conversation_history=conversation_history,
|
|
220
|
+
include_sources=include_sources,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def ingest_files(
|
|
224
|
+
self,
|
|
225
|
+
paths: Sequence[PathLike],
|
|
226
|
+
*,
|
|
227
|
+
source_name: Optional[str] = None,
|
|
228
|
+
description: Optional[str] = None,
|
|
229
|
+
) -> JSON:
|
|
230
|
+
"""Upload local files into this dataset.
|
|
231
|
+
|
|
232
|
+
Creates a temporary ``file_upload`` source automatically. When
|
|
233
|
+
``source_name`` is omitted, the name is generated as
|
|
234
|
+
``file-upload-{first-file-stem}-{random-suffix}``.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
paths: Local file paths to upload. Must contain at least one path.
|
|
238
|
+
source_name: Optional source name for the auto-created source.
|
|
239
|
+
description: Optional description for the source.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Upload completion response JSON, including the created ingestion job.
|
|
243
|
+
"""
|
|
244
|
+
if self.client is None:
|
|
245
|
+
raise TypeError(
|
|
246
|
+
"Dataset.ingest_files requires a Dataset created by a VectorAmp client."
|
|
247
|
+
)
|
|
248
|
+
return self.client.ingestion.ingest_files(
|
|
249
|
+
dataset_id=self.id,
|
|
250
|
+
paths=paths,
|
|
251
|
+
source_name=source_name,
|
|
252
|
+
description=description,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def ingest_source(self, source: SourceInput, *, pipeline_id: Optional[str] = None) -> JSON:
|
|
256
|
+
"""Start ingestion for an existing or newly-created source.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
source: Existing source id, or a source builder that will be created
|
|
260
|
+
before starting the job.
|
|
261
|
+
pipeline_id: Optional pipeline override.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Ingestion job response JSON.
|
|
265
|
+
"""
|
|
266
|
+
if self.client is None:
|
|
267
|
+
raise TypeError(
|
|
268
|
+
"Dataset.ingest_source requires a Dataset created by a VectorAmp client."
|
|
269
|
+
)
|
|
270
|
+
source_id = self.client.sources.resolve_source_id(source)
|
|
271
|
+
return self.client.ingestion.start_job(
|
|
272
|
+
source_id=source_id,
|
|
273
|
+
dataset_id=self.id,
|
|
274
|
+
pipeline_id=pipeline_id,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
278
|
+
"""Return a value from the raw dataset payload, or ``default``."""
|
|
279
|
+
return self.raw_data.get(key, default)
|
|
280
|
+
|
|
281
|
+
def keys(self) -> KeysView[str]:
|
|
282
|
+
"""Return keys from the raw dataset payload."""
|
|
283
|
+
return self.raw_data.keys()
|
|
284
|
+
|
|
285
|
+
def values(self) -> ValuesView[Any]:
|
|
286
|
+
"""Return values from the raw dataset payload."""
|
|
287
|
+
return self.raw_data.values()
|
|
288
|
+
|
|
289
|
+
def items(self) -> ItemsView[str, Any]:
|
|
290
|
+
"""Return items from the raw dataset payload."""
|
|
291
|
+
return self.raw_data.items()
|
|
292
|
+
|
|
293
|
+
def __getitem__(self, key: str) -> Any:
|
|
294
|
+
return self.raw_data[key]
|
|
295
|
+
|
|
296
|
+
def __contains__(self, key: object) -> bool:
|
|
297
|
+
return key in self.raw_data
|
|
298
|
+
|
|
299
|
+
def __iter__(self) -> Iterator[str]:
|
|
300
|
+
return iter(self.raw_data)
|
|
301
|
+
|
|
302
|
+
def __len__(self) -> int:
|
|
303
|
+
return len(self.raw_data)
|
|
304
|
+
|
|
305
|
+
def __repr__(self) -> str:
|
|
306
|
+
return f"Dataset(id={self.id!r})"
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def _extract_id(raw_data: Mapping[str, Any]) -> str:
|
|
310
|
+
value = raw_data.get("id") or raw_data.get("dataset_id")
|
|
311
|
+
if value is None:
|
|
312
|
+
raise ValueError("Dataset response did not include id or dataset_id.")
|
|
313
|
+
return str(value)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class DatasetsResource:
|
|
317
|
+
"""Dataset management, search, and vector insertion APIs."""
|
|
318
|
+
|
|
319
|
+
def __init__(self, transport: BaseTransport, *, client: Optional[Any] = None) -> None:
|
|
320
|
+
self._transport = transport
|
|
321
|
+
self.client = client
|
|
322
|
+
|
|
323
|
+
def list(self, *, limit: int = 50, offset: int = 0) -> JSON:
|
|
324
|
+
"""List datasets.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
limit: Maximum datasets to return. Defaults to ``50``.
|
|
328
|
+
offset: Pagination offset. Defaults to ``0``.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Page JSON whose ``datasets`` entries are ``Dataset`` objects.
|
|
332
|
+
"""
|
|
333
|
+
page = self._transport.request(
|
|
334
|
+
"GET", "/datasets", params={"limit": limit, "offset": offset}
|
|
335
|
+
)
|
|
336
|
+
datasets = page.get("datasets")
|
|
337
|
+
if isinstance(datasets, list):
|
|
338
|
+
page["datasets"] = [self._to_dataset(dataset) for dataset in datasets]
|
|
339
|
+
return page
|
|
340
|
+
|
|
341
|
+
def get(self, dataset_id: str) -> Dataset:
|
|
342
|
+
"""Return one dataset by id.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
dataset_id: Dataset identifier.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
``Dataset`` wrapper around the API payload.
|
|
349
|
+
"""
|
|
350
|
+
return self._to_dataset(self._transport.request("GET", f"/datasets/{dataset_id}"))
|
|
351
|
+
|
|
352
|
+
def create(
|
|
353
|
+
self,
|
|
354
|
+
*,
|
|
355
|
+
name: str,
|
|
356
|
+
dim: Optional[int] = None,
|
|
357
|
+
metric: Metric = "cosine",
|
|
358
|
+
embedding: Optional[Mapping[str, str]] = None,
|
|
359
|
+
embedding_provider: str = "vectoramp",
|
|
360
|
+
embedding_model: str = VECTORAMP_EMBEDDING_4B,
|
|
361
|
+
hybrid: bool = False,
|
|
362
|
+
filters: Optional[Mapping[str, Any]] = None,
|
|
363
|
+
metadata_schema: Optional[Mapping[str, Any]] = None,
|
|
364
|
+
tuning: Optional[Mapping[str, Any]] = None,
|
|
365
|
+
) -> Dataset:
|
|
366
|
+
"""Create a SABLE dataset.
|
|
367
|
+
|
|
368
|
+
``index_type`` is intentionally not exposed; the SDK always requests
|
|
369
|
+
SABLE. The only required argument is ``name``; everything else is
|
|
370
|
+
inferred or defaulted (``VectorAmp-Embedding-4B`` at ``dim=2560``,
|
|
371
|
+
``metric="cosine"``).
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
name: Dataset name.
|
|
375
|
+
dim: Vector dimension. Inferred for built-in embedding helpers when omitted.
|
|
376
|
+
metric: Distance metric. Defaults to ``"cosine"``.
|
|
377
|
+
embedding: Nested embedding config. Use ``openai("small")`` or
|
|
378
|
+
``openai("large")`` for OpenAI BYOM datasets.
|
|
379
|
+
embedding_provider: Embedding provider. Defaults to ``"vectoramp"``.
|
|
380
|
+
embedding_model: Embedding model. Defaults to
|
|
381
|
+
``"VectorAmp-Embedding-4B"``.
|
|
382
|
+
hybrid: Enable hybrid dense + sparse indexing. Sends ``hybrid: true``
|
|
383
|
+
in the create body when set. Defaults to ``False``.
|
|
384
|
+
filters: Optional filter schema/configuration.
|
|
385
|
+
metadata_schema: Optional metadata schema.
|
|
386
|
+
tuning: Optional SABLE tuning parameters.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Created ``Dataset`` object.
|
|
390
|
+
"""
|
|
391
|
+
embedding_config = {"provider": embedding_provider, "model": embedding_model}
|
|
392
|
+
if embedding is not None:
|
|
393
|
+
embedding_config.update(dict(embedding))
|
|
394
|
+
resolved_dim = dim or EMBEDDING_DIMENSIONS.get(str(embedding_config.get("model")))
|
|
395
|
+
if resolved_dim is None:
|
|
396
|
+
raise ValueError("dim is required for custom embedding models")
|
|
397
|
+
|
|
398
|
+
body: JSON = {
|
|
399
|
+
"name": name,
|
|
400
|
+
"dim": resolved_dim,
|
|
401
|
+
"metric": metric,
|
|
402
|
+
"embedding": embedding_config,
|
|
403
|
+
"index_type": "sable",
|
|
404
|
+
}
|
|
405
|
+
if hybrid:
|
|
406
|
+
body["hybrid"] = True
|
|
407
|
+
if filters is not None:
|
|
408
|
+
body["filters"] = dict(filters)
|
|
409
|
+
if metadata_schema is not None:
|
|
410
|
+
body["metadata_schema"] = dict(metadata_schema)
|
|
411
|
+
if tuning is not None:
|
|
412
|
+
body["tuning"] = dict(tuning)
|
|
413
|
+
return self._to_dataset(self._transport.request("POST", "/datasets", json_body=body))
|
|
414
|
+
|
|
415
|
+
def delete(self, dataset_id: str) -> Any:
|
|
416
|
+
"""Delete a dataset and return the API response.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
dataset_id: Dataset identifier.
|
|
420
|
+
"""
|
|
421
|
+
return self._transport.request("DELETE", f"/datasets/{dataset_id}")
|
|
422
|
+
|
|
423
|
+
def stats(self, dataset_id: str) -> JSON:
|
|
424
|
+
"""Return dataset statistics.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
dataset_id: Dataset identifier.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Stats response JSON.
|
|
431
|
+
"""
|
|
432
|
+
return self._transport.request("GET", f"/datasets/{dataset_id}/stats")
|
|
433
|
+
|
|
434
|
+
def list_documents(
|
|
435
|
+
self,
|
|
436
|
+
dataset_id: str,
|
|
437
|
+
*,
|
|
438
|
+
limit: int = 50,
|
|
439
|
+
cursor: Optional[str] = None,
|
|
440
|
+
status: Optional[str] = None,
|
|
441
|
+
) -> JSON:
|
|
442
|
+
"""List source documents retained for a dataset.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
dataset_id: Dataset identifier.
|
|
446
|
+
limit: Maximum documents to return. Defaults to ``50``.
|
|
447
|
+
cursor: Cursor from a previous page's ``next_cursor``.
|
|
448
|
+
status: Optional document status filter.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Document page JSON from ``/datasets/{dataset_id}/documents``.
|
|
452
|
+
"""
|
|
453
|
+
return self._transport.request(
|
|
454
|
+
"GET",
|
|
455
|
+
f"/datasets/{dataset_id}/documents",
|
|
456
|
+
params={"limit": limit, "cursor": cursor, "status": status},
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def download_document(self, dataset_id: str, document_id: str) -> bytes:
|
|
460
|
+
"""Download a retained source document as raw bytes.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
dataset_id: Dataset identifier.
|
|
464
|
+
document_id: Source document identifier from :meth:`list_documents`.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Raw document bytes.
|
|
468
|
+
"""
|
|
469
|
+
return self._transport.download(
|
|
470
|
+
"GET", f"/datasets/{dataset_id}/documents/{document_id}/download"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def search(
|
|
474
|
+
self,
|
|
475
|
+
dataset_id: str,
|
|
476
|
+
query: Optional[Union[str, Sequence[float]]] = None,
|
|
477
|
+
*,
|
|
478
|
+
vector: Optional[Sequence[float]] = None,
|
|
479
|
+
text: Optional[str] = None,
|
|
480
|
+
search_text: Optional[str] = None,
|
|
481
|
+
top_k: int = 10,
|
|
482
|
+
filters: Optional[Filters] = None,
|
|
483
|
+
advanced_filters: Optional[Sequence[AdvancedFilter]] = None,
|
|
484
|
+
embedding_provider: Optional[str] = None,
|
|
485
|
+
embedding_model: Optional[str] = None,
|
|
486
|
+
nprobe_override: Optional[int] = None,
|
|
487
|
+
rerank_depth_override: Optional[int] = None,
|
|
488
|
+
hybrid: Optional[bool] = None,
|
|
489
|
+
sparse_query: Optional[str] = None,
|
|
490
|
+
alpha: Optional[float] = None,
|
|
491
|
+
include_embeddings: Optional[bool] = None,
|
|
492
|
+
include_documents: Optional[bool] = None,
|
|
493
|
+
include_metadata: Optional[bool] = None,
|
|
494
|
+
rerank: Optional[Union[bool, Mapping[str, Any]]] = None,
|
|
495
|
+
) -> JSON:
|
|
496
|
+
"""Search a dataset by text or vector.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
dataset_id: Dataset identifier.
|
|
500
|
+
query: Convenience query; ``str`` maps to text search and a float
|
|
501
|
+
sequence maps to vector search.
|
|
502
|
+
vector: Explicit vector query. Mutually exclusive with ``query`` and
|
|
503
|
+
``text``.
|
|
504
|
+
text: Explicit text query. Mutually exclusive with ``query`` and
|
|
505
|
+
``vector``.
|
|
506
|
+
search_text: Alias for ``text`` for single-field hybrid/BM25 UX.
|
|
507
|
+
top_k: Maximum matches to return. Defaults to ``10``.
|
|
508
|
+
filters: Exact-match metadata filters.
|
|
509
|
+
advanced_filters: API-native advanced metadata filters.
|
|
510
|
+
embedding_provider: Optional provider override for text queries.
|
|
511
|
+
embedding_model: Optional model override for text queries.
|
|
512
|
+
nprobe_override: Optional ANN probe override.
|
|
513
|
+
rerank_depth_override: Optional rerank depth override.
|
|
514
|
+
hybrid: Optional hybrid dense/sparse search toggle.
|
|
515
|
+
sparse_query: Optional sparse query text for hybrid search.
|
|
516
|
+
alpha: Optional dense/sparse weighting for hybrid search.
|
|
517
|
+
include_embeddings: Whether result vectors include embeddings.
|
|
518
|
+
include_documents: Whether result vectors include document text.
|
|
519
|
+
include_metadata: Whether result vectors include metadata; defaults to
|
|
520
|
+
API behavior when omitted.
|
|
521
|
+
rerank: Enable semantic reranking. Use ``True`` or ``{"enabled": True}``;
|
|
522
|
+
provider defaults to ``vectoramp`` and model to ``VectorAmp-Rerank-v1``.
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
Search response JSON.
|
|
526
|
+
"""
|
|
527
|
+
if search_text is not None:
|
|
528
|
+
if text is not None:
|
|
529
|
+
raise ValueError("Provide text or search_text, not both.")
|
|
530
|
+
text = search_text
|
|
531
|
+
if query is not None:
|
|
532
|
+
if vector is not None or text is not None:
|
|
533
|
+
raise ValueError("Provide query or vector/text/search_text, not both.")
|
|
534
|
+
if isinstance(query, str):
|
|
535
|
+
text = query
|
|
536
|
+
else:
|
|
537
|
+
vector = query
|
|
538
|
+
if (vector is None) == (text is None):
|
|
539
|
+
raise ValueError("Provide exactly one of vector or text/search_text.")
|
|
540
|
+
body: JSON = {"top_k": top_k}
|
|
541
|
+
if vector is not None:
|
|
542
|
+
body["query"] = list(vector)
|
|
543
|
+
if text is not None:
|
|
544
|
+
body["query_text"] = text
|
|
545
|
+
optional = {
|
|
546
|
+
"filters": dict(filters) if filters is not None else None,
|
|
547
|
+
"advanced_filters": list(advanced_filters) if advanced_filters is not None else None,
|
|
548
|
+
"embedding_provider": embedding_provider,
|
|
549
|
+
"embedding_model": embedding_model,
|
|
550
|
+
"nprobe_override": nprobe_override,
|
|
551
|
+
"rerank_depth_override": rerank_depth_override,
|
|
552
|
+
"hybrid": hybrid,
|
|
553
|
+
"sparse_query": sparse_query,
|
|
554
|
+
"alpha": alpha,
|
|
555
|
+
"include_embeddings": include_embeddings,
|
|
556
|
+
"include_documents": include_documents,
|
|
557
|
+
"include_metadata": include_metadata,
|
|
558
|
+
"rerank": dict(rerank) if isinstance(rerank, Mapping) else rerank,
|
|
559
|
+
}
|
|
560
|
+
body.update({key: value for key, value in optional.items() if value is not None})
|
|
561
|
+
return self._transport.request("POST", f"/datasets/{dataset_id}/search", json_body=body)
|
|
562
|
+
|
|
563
|
+
def insert_vectors(self, dataset_id: str, vectors: Sequence[Vector]) -> JSON:
|
|
564
|
+
"""Insert vectors into a dataset.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
dataset_id: Dataset identifier.
|
|
568
|
+
vectors: Vector records containing values plus optional id/metadata.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Insert response JSON.
|
|
572
|
+
"""
|
|
573
|
+
return self._transport.request(
|
|
574
|
+
"POST", f"/datasets/{dataset_id}/insert", json_body={"vectors": list(vectors)}
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
def insert(self, dataset_id: str, vectors: Sequence[Vector]) -> JSON:
|
|
578
|
+
"""Alias for ``insert_vectors``."""
|
|
579
|
+
return self.insert_vectors(dataset_id, vectors)
|
|
580
|
+
|
|
581
|
+
def embed(
|
|
582
|
+
self,
|
|
583
|
+
dataset_id: str,
|
|
584
|
+
*,
|
|
585
|
+
text: Optional[str] = None,
|
|
586
|
+
texts: Optional[Sequence[str]] = None,
|
|
587
|
+
) -> JSON:
|
|
588
|
+
"""Embed one or more texts with the dataset embedding model.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
dataset_id: Dataset identifier.
|
|
592
|
+
text: Single text to embed.
|
|
593
|
+
texts: Multiple texts to embed. Mutually exclusive with ``text``.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Embedding response JSON.
|
|
597
|
+
"""
|
|
598
|
+
if (text is None) == (texts is None):
|
|
599
|
+
raise ValueError("Provide exactly one of text or texts.")
|
|
600
|
+
body: JSON = {"text": text} if text is not None else {"texts": list(texts or [])}
|
|
601
|
+
return self._transport.request("POST", f"/datasets/{dataset_id}/embed", json_body=body)
|
|
602
|
+
|
|
603
|
+
def add_texts(
|
|
604
|
+
self,
|
|
605
|
+
dataset_id: str,
|
|
606
|
+
texts: Union[str, Sequence[str]],
|
|
607
|
+
*,
|
|
608
|
+
ids: Optional[Sequence[VectorId]] = None,
|
|
609
|
+
metadatas: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
610
|
+
) -> JSON:
|
|
611
|
+
"""Embed text values with the dataset model and insert them.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
dataset_id: Dataset identifier.
|
|
615
|
+
texts: Single text or sequence of texts.
|
|
616
|
+
ids: Optional vector ids (string or integer). Integer ids are kept as
|
|
617
|
+
JSON numbers rather than coerced to strings. Length must match
|
|
618
|
+
``texts`` when provided.
|
|
619
|
+
metadatas: Optional per-text metadata. Length must match ``texts``.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Insert response JSON.
|
|
623
|
+
"""
|
|
624
|
+
text_list = [texts] if isinstance(texts, str) else list(texts)
|
|
625
|
+
if ids is not None and len(ids) != len(text_list):
|
|
626
|
+
raise ValueError("ids length must match texts length.")
|
|
627
|
+
if metadatas is not None and len(metadatas) != len(text_list):
|
|
628
|
+
raise ValueError("metadatas length must match texts length.")
|
|
629
|
+
embeddings = self.embed(dataset_id, texts=text_list).get("embeddings", [])
|
|
630
|
+
if len(embeddings) != len(text_list):
|
|
631
|
+
raise ValueError("Embedding response length did not match texts length.")
|
|
632
|
+
vectors: list[Vector] = []
|
|
633
|
+
for index, (text, values) in enumerate(zip(text_list, embeddings)):
|
|
634
|
+
metadata = dict(metadatas[index]) if metadatas is not None else {}
|
|
635
|
+
metadata.setdefault("text", text)
|
|
636
|
+
# Provided ids pass through verbatim so integer ids stay JSON numbers;
|
|
637
|
+
# only auto-generated ids are strings (UUIDs).
|
|
638
|
+
vector_id: VectorId = ids[index] if ids is not None else str(uuid.uuid4())
|
|
639
|
+
vectors.append(
|
|
640
|
+
{
|
|
641
|
+
"id": vector_id,
|
|
642
|
+
"values": values,
|
|
643
|
+
"metadata": metadata,
|
|
644
|
+
}
|
|
645
|
+
)
|
|
646
|
+
return self.insert_vectors(dataset_id, vectors)
|
|
647
|
+
|
|
648
|
+
def ensure_engine(self, dataset_id: str) -> JSON:
|
|
649
|
+
"""Ensure a dataset engine is loaded and return the API response.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
dataset_id: Dataset identifier.
|
|
653
|
+
"""
|
|
654
|
+
return self._transport.request("POST", f"/datasets/{dataset_id}/ensure-engine")
|
|
655
|
+
|
|
656
|
+
def _to_dataset(self, raw_data: Mapping[str, Any]) -> Dataset:
|
|
657
|
+
return Dataset(self, raw_data)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class IngestionResource:
|
|
661
|
+
"""Sources, upload sessions, and ingestion jobs."""
|
|
662
|
+
|
|
663
|
+
def __init__(self, transport: BaseTransport) -> None:
|
|
664
|
+
self._transport = transport
|
|
665
|
+
|
|
666
|
+
def list_sources(self, *, limit: int = 50, offset: int = 0) -> JSON:
|
|
667
|
+
"""List ingestion sources.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
limit: Maximum sources to return. Defaults to ``50``.
|
|
671
|
+
offset: Pagination offset. Defaults to ``0``.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Source page JSON.
|
|
675
|
+
"""
|
|
676
|
+
return self._transport.request(
|
|
677
|
+
"GET", "/ingestion/sources", params={"limit": limit, "offset": offset}
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def get_source(self, source_id: str) -> JSON:
|
|
681
|
+
"""Return one ingestion source by id."""
|
|
682
|
+
return self._transport.request("GET", f"/ingestion/sources/{source_id}")
|
|
683
|
+
|
|
684
|
+
def delete_source(self, source_id: str, *, force: bool = False) -> Any:
|
|
685
|
+
"""Delete an ingestion source.
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
source_id: Source identifier.
|
|
689
|
+
force: When ``True``, delete even if the source is still referenced by
|
|
690
|
+
schedules or jobs (sends ``?force=true``). Defaults to ``False``.
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
API response, or ``None`` for a ``204`` response.
|
|
694
|
+
"""
|
|
695
|
+
params = {"force": "true"} if force else None
|
|
696
|
+
return self._transport.request(
|
|
697
|
+
"DELETE", f"/ingestion/sources/{source_id}", params=params
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def list_unused_sources(self, *, limit: int = 50, offset: int = 0) -> JSON:
|
|
701
|
+
"""List sources not referenced by any schedule or job.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
limit: Maximum sources to return. Defaults to ``50``.
|
|
705
|
+
offset: Pagination offset. Defaults to ``0``.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
Source page JSON from ``/ingestion/sources/unused``.
|
|
709
|
+
"""
|
|
710
|
+
return self._transport.request(
|
|
711
|
+
"GET", "/ingestion/sources/unused", params={"limit": limit, "offset": offset}
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
def cleanup_unused_sources(self) -> JSON:
|
|
715
|
+
"""Delete every source not referenced by a schedule or job.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
``{"deleted": [...], "count": int}`` describing the removed sources.
|
|
719
|
+
"""
|
|
720
|
+
return self._transport.request("POST", "/ingestion/sources/cleanup")
|
|
721
|
+
|
|
722
|
+
def get_source_references(self, source_id: str) -> JSON:
|
|
723
|
+
"""Return the schedules and jobs that reference a source.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
source_id: Source identifier.
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
Reference JSON from ``/ingestion/sources/{id}/references``.
|
|
730
|
+
"""
|
|
731
|
+
return self._transport.request("GET", f"/ingestion/sources/{source_id}/references")
|
|
732
|
+
|
|
733
|
+
def validate_source(
|
|
734
|
+
self,
|
|
735
|
+
source: Optional[SourceBuilder] = None,
|
|
736
|
+
*,
|
|
737
|
+
source_type: Optional[str] = None,
|
|
738
|
+
config: Optional[Mapping[str, Any]] = None,
|
|
739
|
+
) -> JSON:
|
|
740
|
+
"""Validate a source configuration without creating it.
|
|
741
|
+
|
|
742
|
+
Accepts either a source builder (its ``source_type`` and ``config`` are
|
|
743
|
+
derived from ``to_create_request``) or explicit ``source_type`` and
|
|
744
|
+
``config`` keyword arguments.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
source: Optional source builder such as ``WebSource`` or ``S3Source``.
|
|
748
|
+
source_type: Source type when not passing ``source``.
|
|
749
|
+
config: Source config when not passing ``source``.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
Validation response JSON from ``/ingestion/sources/validate``.
|
|
753
|
+
"""
|
|
754
|
+
if source is not None:
|
|
755
|
+
request = source.to_create_request()
|
|
756
|
+
body = {"source_type": request["source_type"], "config": request["config"]}
|
|
757
|
+
else:
|
|
758
|
+
if source_type is None or config is None:
|
|
759
|
+
raise TypeError(
|
|
760
|
+
"validate_source requires source or both source_type and config."
|
|
761
|
+
)
|
|
762
|
+
body = {"source_type": source_type, "config": dict(config)}
|
|
763
|
+
return self._transport.request(
|
|
764
|
+
"POST", "/ingestion/sources/validate", json_body=body
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
def create_source(
|
|
768
|
+
self,
|
|
769
|
+
source: Optional[SourceBuilder] = None,
|
|
770
|
+
*,
|
|
771
|
+
name: Optional[str] = None,
|
|
772
|
+
source_type: Optional[str] = None,
|
|
773
|
+
config: Optional[Mapping[str, Any]] = None,
|
|
774
|
+
description: Optional[str] = None,
|
|
775
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
776
|
+
) -> JSON:
|
|
777
|
+
"""Create an ingestion source.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
source: Optional source builder such as ``WebSource`` or ``S3Source``.
|
|
781
|
+
name: Source name when not passing ``source``.
|
|
782
|
+
source_type: Source type when not passing ``source``.
|
|
783
|
+
config: Source config when not passing ``source``.
|
|
784
|
+
description: Optional source description.
|
|
785
|
+
metadata: Optional source metadata.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
Created source JSON.
|
|
789
|
+
"""
|
|
790
|
+
if source is not None:
|
|
791
|
+
body = source.to_create_request()
|
|
792
|
+
else:
|
|
793
|
+
if name is None or source_type is None or config is None:
|
|
794
|
+
raise TypeError("create_source requires source or name, source_type, and config.")
|
|
795
|
+
body = {
|
|
796
|
+
"name": name,
|
|
797
|
+
"source_type": source_type,
|
|
798
|
+
"config": dict(config),
|
|
799
|
+
}
|
|
800
|
+
if description is not None:
|
|
801
|
+
body["description"] = description
|
|
802
|
+
if metadata is not None:
|
|
803
|
+
body["metadata"] = dict(metadata)
|
|
804
|
+
return self._transport.request("POST", "/ingestion/sources", json_body=body)
|
|
805
|
+
|
|
806
|
+
def create(self, source: SourceBuilder) -> JSON:
|
|
807
|
+
"""Create an ingestion source from a source builder."""
|
|
808
|
+
return self.create_source(source)
|
|
809
|
+
|
|
810
|
+
def create_web(
|
|
811
|
+
self,
|
|
812
|
+
*,
|
|
813
|
+
start_urls: Sequence[str],
|
|
814
|
+
name: Optional[str] = None,
|
|
815
|
+
max_depth: Optional[int] = None,
|
|
816
|
+
max_pages: Optional[int] = None,
|
|
817
|
+
allowed_domains: Optional[Sequence[str]] = None,
|
|
818
|
+
include_patterns: Optional[Sequence[str]] = None,
|
|
819
|
+
exclude_patterns: Optional[Sequence[str]] = None,
|
|
820
|
+
crawl_delay_seconds: Optional[float] = None,
|
|
821
|
+
include_assets: Optional[bool] = None,
|
|
822
|
+
max_assets_per_page: Optional[int] = None,
|
|
823
|
+
description: Optional[str] = None,
|
|
824
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
825
|
+
config_extra: Optional[Mapping[str, Any]] = None,
|
|
826
|
+
) -> JSON:
|
|
827
|
+
"""Create a web crawler source.
|
|
828
|
+
|
|
829
|
+
``name`` defaults to ``web-{host-or-path}`` from the first start URL.
|
|
830
|
+
Optional crawl settings are omitted from the request when ``None``.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
Created source JSON.
|
|
834
|
+
"""
|
|
835
|
+
return self.create_source(
|
|
836
|
+
WebSource(
|
|
837
|
+
name=name,
|
|
838
|
+
start_urls=start_urls,
|
|
839
|
+
max_depth=max_depth,
|
|
840
|
+
max_pages=max_pages,
|
|
841
|
+
allowed_domains=allowed_domains,
|
|
842
|
+
include_patterns=include_patterns,
|
|
843
|
+
exclude_patterns=exclude_patterns,
|
|
844
|
+
crawl_delay_seconds=crawl_delay_seconds,
|
|
845
|
+
include_assets=include_assets,
|
|
846
|
+
max_assets_per_page=max_assets_per_page,
|
|
847
|
+
description=description,
|
|
848
|
+
metadata=metadata,
|
|
849
|
+
config_extra=config_extra,
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
def create_s3(
|
|
854
|
+
self,
|
|
855
|
+
*,
|
|
856
|
+
bucket: str,
|
|
857
|
+
name: Optional[str] = None,
|
|
858
|
+
region: str = "us-east-1",
|
|
859
|
+
prefix: Optional[str] = None,
|
|
860
|
+
sync_mode: Optional[str] = None,
|
|
861
|
+
access_key_id: Optional[str] = None,
|
|
862
|
+
secret_access_key: Optional[str] = None,
|
|
863
|
+
role_arn: Optional[str] = None,
|
|
864
|
+
endpoint_url: Optional[str] = None,
|
|
865
|
+
file_patterns: Optional[Sequence[str]] = None,
|
|
866
|
+
max_file_size_mb: Optional[int] = None,
|
|
867
|
+
description: Optional[str] = None,
|
|
868
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
869
|
+
config_extra: Optional[Mapping[str, Any]] = None,
|
|
870
|
+
) -> JSON:
|
|
871
|
+
"""Create an Amazon S3 source.
|
|
872
|
+
|
|
873
|
+
``name`` defaults to ``s3-{bucket}``; ``region`` defaults to
|
|
874
|
+
``"us-east-1"``. ``sync_mode`` is omitted when ``None`` so the server
|
|
875
|
+
applies its default (``"incremental"``). Optional credentials and file
|
|
876
|
+
settings are omitted when ``None``.
|
|
877
|
+
|
|
878
|
+
Returns:
|
|
879
|
+
Created source JSON.
|
|
880
|
+
"""
|
|
881
|
+
return self.create_source(
|
|
882
|
+
S3Source(
|
|
883
|
+
name=name,
|
|
884
|
+
bucket=bucket,
|
|
885
|
+
region=region,
|
|
886
|
+
prefix=prefix,
|
|
887
|
+
sync_mode=sync_mode,
|
|
888
|
+
access_key_id=access_key_id,
|
|
889
|
+
secret_access_key=secret_access_key,
|
|
890
|
+
role_arn=role_arn,
|
|
891
|
+
endpoint_url=endpoint_url,
|
|
892
|
+
file_patterns=file_patterns,
|
|
893
|
+
max_file_size_mb=max_file_size_mb,
|
|
894
|
+
description=description,
|
|
895
|
+
metadata=metadata,
|
|
896
|
+
config_extra=config_extra,
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
def create_gcs(
|
|
901
|
+
self,
|
|
902
|
+
*,
|
|
903
|
+
bucket: str,
|
|
904
|
+
name: Optional[str] = None,
|
|
905
|
+
prefix: Optional[str] = None,
|
|
906
|
+
project_id: Optional[str] = None,
|
|
907
|
+
credentials_json: Optional[Mapping[str, Any]] = None,
|
|
908
|
+
sync_mode: Optional[str] = None,
|
|
909
|
+
file_patterns: Optional[Sequence[str]] = None,
|
|
910
|
+
max_file_size_mb: Optional[int] = None,
|
|
911
|
+
description: Optional[str] = None,
|
|
912
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
913
|
+
config_extra: Optional[Mapping[str, Any]] = None,
|
|
914
|
+
) -> JSON:
|
|
915
|
+
"""Create a Google Cloud Storage source.
|
|
916
|
+
|
|
917
|
+
``sync_mode`` is omitted when ``None`` so the server applies its default
|
|
918
|
+
(``"incremental"``).
|
|
919
|
+
"""
|
|
920
|
+
return self.create_source(
|
|
921
|
+
GCSSource(
|
|
922
|
+
name=name,
|
|
923
|
+
bucket=bucket,
|
|
924
|
+
prefix=prefix,
|
|
925
|
+
project_id=project_id,
|
|
926
|
+
credentials_json=credentials_json,
|
|
927
|
+
sync_mode=sync_mode,
|
|
928
|
+
file_patterns=file_patterns,
|
|
929
|
+
max_file_size_mb=max_file_size_mb,
|
|
930
|
+
description=description,
|
|
931
|
+
metadata=metadata,
|
|
932
|
+
config_extra=config_extra,
|
|
933
|
+
)
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
def create_jira(
|
|
937
|
+
self,
|
|
938
|
+
*,
|
|
939
|
+
cloud_id: str,
|
|
940
|
+
name: Optional[str] = None,
|
|
941
|
+
access_token: Optional[str] = None,
|
|
942
|
+
project_keys: Optional[Sequence[str]] = None,
|
|
943
|
+
jql: Optional[str] = None,
|
|
944
|
+
include_comments: bool = True,
|
|
945
|
+
sync_mode: Optional[str] = None,
|
|
946
|
+
description: Optional[str] = None,
|
|
947
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
948
|
+
config_extra: Optional[Mapping[str, Any]] = None,
|
|
949
|
+
) -> JSON:
|
|
950
|
+
"""Create a Jira source.
|
|
951
|
+
|
|
952
|
+
``include_comments`` defaults to true. ``sync_mode`` is omitted when
|
|
953
|
+
``None`` so the server applies its default (``"incremental"``).
|
|
954
|
+
"""
|
|
955
|
+
return self.create_source(
|
|
956
|
+
JiraSource(
|
|
957
|
+
name=name,
|
|
958
|
+
cloud_id=cloud_id,
|
|
959
|
+
access_token=access_token,
|
|
960
|
+
project_keys=project_keys,
|
|
961
|
+
jql=jql,
|
|
962
|
+
include_comments=include_comments,
|
|
963
|
+
sync_mode=sync_mode,
|
|
964
|
+
description=description,
|
|
965
|
+
metadata=metadata,
|
|
966
|
+
config_extra=config_extra,
|
|
967
|
+
)
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
def create_confluence(
|
|
971
|
+
self,
|
|
972
|
+
*,
|
|
973
|
+
cloud_id: Optional[str] = None,
|
|
974
|
+
base_url: Optional[str] = None,
|
|
975
|
+
name: Optional[str] = None,
|
|
976
|
+
auth_mode: str = "basic",
|
|
977
|
+
username: Optional[str] = None,
|
|
978
|
+
api_token: Optional[str] = None,
|
|
979
|
+
oauth_credentials: Optional[Mapping[str, Any]] = None,
|
|
980
|
+
spaces: Optional[Sequence[str]] = None,
|
|
981
|
+
include_attachments: bool = False,
|
|
982
|
+
sync_mode: Optional[str] = None,
|
|
983
|
+
description: Optional[str] = None,
|
|
984
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
985
|
+
config_extra: Optional[Mapping[str, Any]] = None,
|
|
986
|
+
) -> JSON:
|
|
987
|
+
"""Create a Confluence source.
|
|
988
|
+
|
|
989
|
+
Provide either ``cloud_id`` or ``base_url``. ``auth_mode`` defaults to
|
|
990
|
+
``"basic"`` (set ``username``/``api_token``); use ``oauth_credentials``
|
|
991
|
+
for OAuth. ``include_attachments`` defaults to ``False``. ``sync_mode``
|
|
992
|
+
is omitted when ``None`` so the server applies its default
|
|
993
|
+
(``"incremental"``).
|
|
994
|
+
|
|
995
|
+
Returns:
|
|
996
|
+
Created source JSON.
|
|
997
|
+
"""
|
|
998
|
+
return self.create_source(
|
|
999
|
+
ConfluenceSource(
|
|
1000
|
+
name=name,
|
|
1001
|
+
cloud_id=cloud_id,
|
|
1002
|
+
base_url=base_url,
|
|
1003
|
+
auth_mode=auth_mode,
|
|
1004
|
+
username=username,
|
|
1005
|
+
api_token=api_token,
|
|
1006
|
+
oauth_credentials=oauth_credentials,
|
|
1007
|
+
spaces=spaces,
|
|
1008
|
+
include_attachments=include_attachments,
|
|
1009
|
+
sync_mode=sync_mode,
|
|
1010
|
+
description=description,
|
|
1011
|
+
metadata=metadata,
|
|
1012
|
+
config_extra=config_extra,
|
|
1013
|
+
)
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
def create_google_drive(
|
|
1017
|
+
self,
|
|
1018
|
+
*,
|
|
1019
|
+
name: Optional[str] = None,
|
|
1020
|
+
folder_ids: Optional[Sequence[str]] = None,
|
|
1021
|
+
file_ids: Optional[Sequence[str]] = None,
|
|
1022
|
+
auth_mode: str = "oauth",
|
|
1023
|
+
oauth_credentials: Optional[Mapping[str, Any]] = None,
|
|
1024
|
+
include_shared_drives: Optional[bool] = None,
|
|
1025
|
+
sync_mode: Optional[str] = None,
|
|
1026
|
+
service_account_json: Optional[Mapping[str, Any]] = None,
|
|
1027
|
+
credentials_json: Optional[Mapping[str, Any]] = None,
|
|
1028
|
+
description: Optional[str] = None,
|
|
1029
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
1030
|
+
config_extra: Optional[Mapping[str, Any]] = None,
|
|
1031
|
+
) -> JSON:
|
|
1032
|
+
"""Create a Google Drive source.
|
|
1033
|
+
|
|
1034
|
+
``name`` defaults to ``gdrive-{first-folder-or-file-id}`` or ``gdrive``.
|
|
1035
|
+
``auth_mode`` defaults to ``"oauth"``. ``sync_mode`` is omitted when
|
|
1036
|
+
``None`` so the server applies its default (``"incremental"``). Optional
|
|
1037
|
+
auth/config values are omitted when ``None``.
|
|
1038
|
+
|
|
1039
|
+
Returns:
|
|
1040
|
+
Created source JSON.
|
|
1041
|
+
"""
|
|
1042
|
+
return self.create_source(
|
|
1043
|
+
GoogleDriveSource(
|
|
1044
|
+
name=name,
|
|
1045
|
+
folder_ids=folder_ids,
|
|
1046
|
+
file_ids=file_ids,
|
|
1047
|
+
auth_mode=auth_mode,
|
|
1048
|
+
oauth_credentials=oauth_credentials,
|
|
1049
|
+
include_shared_drives=include_shared_drives,
|
|
1050
|
+
sync_mode=sync_mode,
|
|
1051
|
+
service_account_json=service_account_json,
|
|
1052
|
+
credentials_json=credentials_json,
|
|
1053
|
+
description=description,
|
|
1054
|
+
metadata=metadata,
|
|
1055
|
+
config_extra=config_extra,
|
|
1056
|
+
)
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
def create_file_upload(
|
|
1060
|
+
self,
|
|
1061
|
+
*,
|
|
1062
|
+
name: str = "vectoramp-python-upload",
|
|
1063
|
+
storage_provider: str = "s3",
|
|
1064
|
+
sync_mode: str = "full",
|
|
1065
|
+
description: Optional[str] = None,
|
|
1066
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
1067
|
+
config_extra: Optional[Mapping[str, Any]] = None,
|
|
1068
|
+
) -> JSON:
|
|
1069
|
+
"""Create a file-upload source record.
|
|
1070
|
+
|
|
1071
|
+
Args:
|
|
1072
|
+
name: Source name. Defaults to ``"vectoramp-python-upload"``.
|
|
1073
|
+
storage_provider: Upload storage provider. Defaults to ``"s3"``.
|
|
1074
|
+
sync_mode: Source sync mode. Defaults to ``"full"``.
|
|
1075
|
+
description: Optional source description.
|
|
1076
|
+
metadata: Optional source metadata.
|
|
1077
|
+
config_extra: Optional extra config fields merged into the request.
|
|
1078
|
+
|
|
1079
|
+
Returns:
|
|
1080
|
+
Created source JSON. For local file upload, prefer ``ingest_files``,
|
|
1081
|
+
which creates this source automatically and uploads the files.
|
|
1082
|
+
"""
|
|
1083
|
+
return self.create_source(
|
|
1084
|
+
FileUploadSource(
|
|
1085
|
+
name=name,
|
|
1086
|
+
storage_provider=storage_provider,
|
|
1087
|
+
sync_mode=sync_mode,
|
|
1088
|
+
description=description,
|
|
1089
|
+
metadata=metadata,
|
|
1090
|
+
config_extra=config_extra,
|
|
1091
|
+
)
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
def resolve_source_id(self, source: SourceInput) -> str:
|
|
1095
|
+
"""Return a source id, creating builder inputs when needed.
|
|
1096
|
+
|
|
1097
|
+
Args:
|
|
1098
|
+
source: Existing source id or source builder.
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
Source identifier string.
|
|
1102
|
+
"""
|
|
1103
|
+
if isinstance(source, str):
|
|
1104
|
+
return source
|
|
1105
|
+
created = self.create_source(source)
|
|
1106
|
+
source_id = created.get("id") or created.get("source_id") or created.get("uuid")
|
|
1107
|
+
if source_id is None:
|
|
1108
|
+
raise ValueError("Source creation response did not include id or source_id.")
|
|
1109
|
+
return str(source_id)
|
|
1110
|
+
|
|
1111
|
+
def start_job(
|
|
1112
|
+
self, *, source_id: str, dataset_id: str, pipeline_id: Optional[str] = None
|
|
1113
|
+
) -> JSON:
|
|
1114
|
+
"""Start an ingestion job.
|
|
1115
|
+
|
|
1116
|
+
Args:
|
|
1117
|
+
source_id: Source identifier.
|
|
1118
|
+
dataset_id: Target dataset identifier.
|
|
1119
|
+
pipeline_id: Optional pipeline override.
|
|
1120
|
+
|
|
1121
|
+
Returns:
|
|
1122
|
+
Ingestion job response JSON.
|
|
1123
|
+
"""
|
|
1124
|
+
body: JSON = {"source_id": source_id, "dataset_id": dataset_id}
|
|
1125
|
+
if pipeline_id is not None:
|
|
1126
|
+
body["pipeline_id"] = pipeline_id
|
|
1127
|
+
return self._transport.request("POST", "/ingestion/jobs", json_body=body)
|
|
1128
|
+
|
|
1129
|
+
def list_jobs(
|
|
1130
|
+
self, *, dataset_id: Optional[str] = None, limit: int = 50, offset: int = 0
|
|
1131
|
+
) -> JSON:
|
|
1132
|
+
"""List ingestion jobs.
|
|
1133
|
+
|
|
1134
|
+
Args:
|
|
1135
|
+
dataset_id: Optional dataset filter.
|
|
1136
|
+
limit: Maximum jobs to return. Defaults to ``50``.
|
|
1137
|
+
offset: Pagination offset. Defaults to ``0``.
|
|
1138
|
+
|
|
1139
|
+
Returns:
|
|
1140
|
+
Job page JSON.
|
|
1141
|
+
"""
|
|
1142
|
+
return self._transport.request(
|
|
1143
|
+
"GET",
|
|
1144
|
+
"/ingestion/jobs",
|
|
1145
|
+
params={"dataset_id": dataset_id, "limit": limit, "offset": offset},
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
def get_job(self, job_id: str) -> JSON:
|
|
1149
|
+
"""Return one ingestion job by id."""
|
|
1150
|
+
return self._transport.request("GET", f"/ingestion/jobs/{job_id}")
|
|
1151
|
+
|
|
1152
|
+
def retry_job(self, job_id: str) -> JSON:
|
|
1153
|
+
"""Queue a fresh full-rerun job from an eligible failed or cancelled job."""
|
|
1154
|
+
return self._transport.request("POST", f"/ingestion/jobs/{job_id}/retry")
|
|
1155
|
+
|
|
1156
|
+
def init_upload(self, source_id: str, files: Sequence[Mapping[str, Any]]) -> JSON:
|
|
1157
|
+
"""Initialize presigned uploads for a file-upload source.
|
|
1158
|
+
|
|
1159
|
+
Args:
|
|
1160
|
+
source_id: File-upload source identifier.
|
|
1161
|
+
files: File descriptors with name, size, and content type.
|
|
1162
|
+
|
|
1163
|
+
Returns:
|
|
1164
|
+
Upload session JSON, including upload URLs and job id.
|
|
1165
|
+
"""
|
|
1166
|
+
return self._transport.request(
|
|
1167
|
+
"POST", f"/ingestion/sources/{source_id}/upload/init", json_body={"files": list(files)}
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
def complete_upload(self, source_id: str, *, job_id: str, file_ids: Sequence[str]) -> JSON:
|
|
1171
|
+
"""Complete a file-upload session.
|
|
1172
|
+
|
|
1173
|
+
Args:
|
|
1174
|
+
source_id: File-upload source identifier.
|
|
1175
|
+
job_id: Upload job identifier returned by ``init_upload``.
|
|
1176
|
+
file_ids: Uploaded file identifiers returned by ``init_upload``.
|
|
1177
|
+
|
|
1178
|
+
Returns:
|
|
1179
|
+
Upload completion response JSON.
|
|
1180
|
+
"""
|
|
1181
|
+
return self._transport.request(
|
|
1182
|
+
"POST",
|
|
1183
|
+
f"/ingestion/sources/{source_id}/upload/complete",
|
|
1184
|
+
json_body={"job_id": job_id, "file_ids": list(file_ids)},
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
def ingest_files(
|
|
1188
|
+
self,
|
|
1189
|
+
*,
|
|
1190
|
+
dataset_id: str,
|
|
1191
|
+
paths: Sequence[PathLike],
|
|
1192
|
+
source_name: Optional[str] = None,
|
|
1193
|
+
description: Optional[str] = None,
|
|
1194
|
+
) -> JSON:
|
|
1195
|
+
"""Create a file-upload source, upload files, and complete the flow.
|
|
1196
|
+
|
|
1197
|
+
Creates a ``file_upload`` source automatically with config
|
|
1198
|
+
``{"storage_provider": "s3", "sync_mode": "full"}`` and metadata
|
|
1199
|
+
containing ``dataset_id``. When ``source_name`` is omitted, the name is
|
|
1200
|
+
generated as ``file-upload-{first-file-stem}-{random-suffix}``.
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
dataset_id: Target dataset identifier.
|
|
1204
|
+
paths: Local file paths to upload. Must contain at least one path.
|
|
1205
|
+
source_name: Optional source name for the auto-created source.
|
|
1206
|
+
description: Optional description for the source.
|
|
1207
|
+
|
|
1208
|
+
Returns:
|
|
1209
|
+
Upload completion response JSON, including the created ingestion job.
|
|
1210
|
+
"""
|
|
1211
|
+
file_paths = [Path(path) for path in paths]
|
|
1212
|
+
if not file_paths:
|
|
1213
|
+
raise ValueError("ingest_files requires at least one path.")
|
|
1214
|
+
source = self.create_source(
|
|
1215
|
+
name=source_name or self._default_upload_source_name(file_paths),
|
|
1216
|
+
source_type="file_upload",
|
|
1217
|
+
description=description,
|
|
1218
|
+
config={"storage_provider": "s3", "sync_mode": "full"},
|
|
1219
|
+
metadata={"dataset_id": dataset_id},
|
|
1220
|
+
)
|
|
1221
|
+
source_id = str(source.get("id") or source.get("source_id"))
|
|
1222
|
+
files = [self._file_descriptor(path) for path in file_paths]
|
|
1223
|
+
upload = self.init_upload(source_id, files)
|
|
1224
|
+
uploads = upload.get("uploads", [])
|
|
1225
|
+
if len(uploads) != len(file_paths):
|
|
1226
|
+
raise ValueError("Upload init response did not match requested files.")
|
|
1227
|
+
if not isinstance(self._transport, RestTransport):
|
|
1228
|
+
raise TypeError(
|
|
1229
|
+
"ingest_files requires a REST transport with presigned URL upload support."
|
|
1230
|
+
)
|
|
1231
|
+
file_ids: list[str] = []
|
|
1232
|
+
for path, upload_info in zip(file_paths, uploads):
|
|
1233
|
+
file_ids.append(str(upload_info["file_id"]))
|
|
1234
|
+
self._transport.put_url(
|
|
1235
|
+
str(upload_info["upload_url"]),
|
|
1236
|
+
content=path.read_bytes(),
|
|
1237
|
+
content_type=self._guess_content_type(path),
|
|
1238
|
+
)
|
|
1239
|
+
job_id = str(upload["job_id"])
|
|
1240
|
+
complete = self.complete_upload(source_id, job_id=job_id, file_ids=file_ids)
|
|
1241
|
+
if isinstance(complete, dict):
|
|
1242
|
+
complete.setdefault("job_id", job_id)
|
|
1243
|
+
return complete
|
|
1244
|
+
|
|
1245
|
+
@staticmethod
|
|
1246
|
+
def _default_upload_source_name(paths: Sequence[Path]) -> str:
|
|
1247
|
+
first_stem = paths[0].stem or "files"
|
|
1248
|
+
slug = "".join(char.lower() if char.isalnum() else "-" for char in first_stem)
|
|
1249
|
+
slug = "-".join(part for part in slug.split("-") if part)[:32] or "files"
|
|
1250
|
+
return f"file-upload-{slug}-{uuid.uuid4().hex[:8]}"
|
|
1251
|
+
|
|
1252
|
+
@staticmethod
|
|
1253
|
+
def _file_descriptor(path: Path) -> JSON:
|
|
1254
|
+
return {
|
|
1255
|
+
"name": path.name,
|
|
1256
|
+
"size_bytes": path.stat().st_size,
|
|
1257
|
+
"content_type": IngestionResource._guess_content_type(path),
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
@staticmethod
|
|
1261
|
+
def _guess_content_type(path: Path) -> str:
|
|
1262
|
+
return mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
class SchedulesResource:
|
|
1266
|
+
"""Ingestion schedule management.
|
|
1267
|
+
|
|
1268
|
+
Recurring ingestion runs are defined as schedules. Each schedule pairs a
|
|
1269
|
+
source with a target dataset and a cron expression; the ingestion scheduler
|
|
1270
|
+
daemon polls for due schedules and creates jobs as they fire.
|
|
1271
|
+
"""
|
|
1272
|
+
|
|
1273
|
+
def __init__(self, transport: BaseTransport) -> None:
|
|
1274
|
+
self._transport = transport
|
|
1275
|
+
|
|
1276
|
+
def list(self, *, limit: int = 50, offset: int = 0) -> JSON:
|
|
1277
|
+
"""List schedules for the calling organization.
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
limit: Maximum schedules to return. Defaults to ``50``.
|
|
1281
|
+
offset: Pagination offset. Defaults to ``0``.
|
|
1282
|
+
|
|
1283
|
+
Returns:
|
|
1284
|
+
``{"schedules": [...], "total": int, "limit": int, "offset": int}``.
|
|
1285
|
+
"""
|
|
1286
|
+
return self._transport.request(
|
|
1287
|
+
"GET", "/ingestion/schedules", params={"limit": limit, "offset": offset}
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
def get(self, schedule_id: str) -> JSON:
|
|
1291
|
+
"""Return one schedule by id."""
|
|
1292
|
+
return self._transport.request("GET", f"/ingestion/schedules/{schedule_id}")
|
|
1293
|
+
|
|
1294
|
+
def create(
|
|
1295
|
+
self,
|
|
1296
|
+
*,
|
|
1297
|
+
source_id: str,
|
|
1298
|
+
dataset_id: str,
|
|
1299
|
+
cron: str,
|
|
1300
|
+
timezone: Optional[str] = None,
|
|
1301
|
+
pipeline_id: Optional[str] = None,
|
|
1302
|
+
enabled: Optional[bool] = None,
|
|
1303
|
+
name: Optional[str] = None,
|
|
1304
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
1305
|
+
) -> JSON:
|
|
1306
|
+
"""Create a recurring ingestion schedule.
|
|
1307
|
+
|
|
1308
|
+
Args:
|
|
1309
|
+
source_id: Ingestion source id to pull from on each run.
|
|
1310
|
+
dataset_id: Dataset id to ingest into.
|
|
1311
|
+
cron: 5-field cron expression (e.g. ``"0 * * * *"`` for hourly).
|
|
1312
|
+
timezone: Optional IANA timezone. Defaults to ``UTC`` server-side.
|
|
1313
|
+
pipeline_id: Optional pipeline id. Omit to use the default ingestion pipeline.
|
|
1314
|
+
enabled: Optional flag, defaults to ``True`` server-side.
|
|
1315
|
+
name: Optional human-readable name.
|
|
1316
|
+
metadata: Optional metadata blob attached to the schedule.
|
|
1317
|
+
|
|
1318
|
+
Returns:
|
|
1319
|
+
Created schedule JSON.
|
|
1320
|
+
"""
|
|
1321
|
+
body: dict[str, Any] = {
|
|
1322
|
+
"source_id": source_id,
|
|
1323
|
+
"dataset_id": dataset_id,
|
|
1324
|
+
"cron": cron,
|
|
1325
|
+
}
|
|
1326
|
+
if timezone is not None:
|
|
1327
|
+
body["timezone"] = timezone
|
|
1328
|
+
if pipeline_id is not None:
|
|
1329
|
+
body["pipeline_id"] = pipeline_id
|
|
1330
|
+
if enabled is not None:
|
|
1331
|
+
body["enabled"] = enabled
|
|
1332
|
+
if name is not None:
|
|
1333
|
+
body["name"] = name
|
|
1334
|
+
if metadata is not None:
|
|
1335
|
+
body["metadata"] = dict(metadata)
|
|
1336
|
+
return self._transport.request("POST", "/ingestion/schedules", json_body=body)
|
|
1337
|
+
|
|
1338
|
+
def update(
|
|
1339
|
+
self,
|
|
1340
|
+
schedule_id: str,
|
|
1341
|
+
*,
|
|
1342
|
+
cron: Optional[str] = None,
|
|
1343
|
+
timezone: Optional[str] = None,
|
|
1344
|
+
pipeline_id: Optional[str] = None,
|
|
1345
|
+
enabled: Optional[bool] = None,
|
|
1346
|
+
name: Optional[str] = None,
|
|
1347
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
1348
|
+
) -> JSON:
|
|
1349
|
+
"""Update a schedule. Pass only the fields you want to change.
|
|
1350
|
+
|
|
1351
|
+
Returns:
|
|
1352
|
+
Updated schedule JSON.
|
|
1353
|
+
"""
|
|
1354
|
+
body: dict[str, Any] = {}
|
|
1355
|
+
if cron is not None:
|
|
1356
|
+
body["cron"] = cron
|
|
1357
|
+
if timezone is not None:
|
|
1358
|
+
body["timezone"] = timezone
|
|
1359
|
+
if pipeline_id is not None:
|
|
1360
|
+
body["pipeline_id"] = pipeline_id
|
|
1361
|
+
if enabled is not None:
|
|
1362
|
+
body["enabled"] = enabled
|
|
1363
|
+
if name is not None:
|
|
1364
|
+
body["name"] = name
|
|
1365
|
+
if metadata is not None:
|
|
1366
|
+
body["metadata"] = dict(metadata)
|
|
1367
|
+
return self._transport.request(
|
|
1368
|
+
"PATCH", f"/ingestion/schedules/{schedule_id}", json_body=body
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
def delete(self, schedule_id: str) -> JSON:
|
|
1372
|
+
"""Delete a schedule."""
|
|
1373
|
+
return self._transport.request("DELETE", f"/ingestion/schedules/{schedule_id}")
|
|
1374
|
+
|
|
1375
|
+
def trigger(self, schedule_id: str) -> JSON:
|
|
1376
|
+
"""Trigger an immediate run for a schedule, outside its cron cadence.
|
|
1377
|
+
|
|
1378
|
+
Returns:
|
|
1379
|
+
``{"job_id": "..."}`` for the newly created ingestion job.
|
|
1380
|
+
"""
|
|
1381
|
+
return self._transport.request(
|
|
1382
|
+
"POST", f"/ingestion/schedules/{schedule_id}/trigger"
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
class IntelligenceResource:
|
|
1387
|
+
"""RAG query APIs."""
|
|
1388
|
+
|
|
1389
|
+
def __init__(self, transport: BaseTransport) -> None:
|
|
1390
|
+
self._transport = transport
|
|
1391
|
+
|
|
1392
|
+
def query(
|
|
1393
|
+
self,
|
|
1394
|
+
query: str,
|
|
1395
|
+
*,
|
|
1396
|
+
dataset_id: Optional[str] = None,
|
|
1397
|
+
top_k: int = 5,
|
|
1398
|
+
conversation_history: Optional[Sequence[ConversationTurn]] = None,
|
|
1399
|
+
include_sources: bool = True,
|
|
1400
|
+
) -> JSON:
|
|
1401
|
+
"""Ask a non-streaming RAG question.
|
|
1402
|
+
|
|
1403
|
+
Args:
|
|
1404
|
+
query: Natural-language question.
|
|
1405
|
+
dataset_id: Optional dataset to ground the answer in. When omitted,
|
|
1406
|
+
the API chooses its configured/default scope.
|
|
1407
|
+
top_k: Number of retrieved chunks to consider. Defaults to ``5``.
|
|
1408
|
+
conversation_history: Optional prior chat turns.
|
|
1409
|
+
include_sources: Whether to include source chunks. Defaults to ``True``.
|
|
1410
|
+
|
|
1411
|
+
Returns:
|
|
1412
|
+
JSON response from ``/intelligence/query``.
|
|
1413
|
+
"""
|
|
1414
|
+
body = self._body(
|
|
1415
|
+
query,
|
|
1416
|
+
dataset_id=dataset_id,
|
|
1417
|
+
top_k=top_k,
|
|
1418
|
+
conversation_history=conversation_history,
|
|
1419
|
+
include_sources=include_sources,
|
|
1420
|
+
stream=False,
|
|
1421
|
+
)
|
|
1422
|
+
return self._transport.request("POST", "/intelligence/query", json_body=body)
|
|
1423
|
+
|
|
1424
|
+
def stream(
|
|
1425
|
+
self,
|
|
1426
|
+
query: str,
|
|
1427
|
+
*,
|
|
1428
|
+
dataset_id: Optional[str] = None,
|
|
1429
|
+
top_k: int = 5,
|
|
1430
|
+
conversation_history: Optional[Sequence[ConversationTurn]] = None,
|
|
1431
|
+
include_sources: bool = True,
|
|
1432
|
+
) -> Iterator[JSON]:
|
|
1433
|
+
"""Yield Server-Sent Event chunks for a streaming RAG answer.
|
|
1434
|
+
|
|
1435
|
+
Args:
|
|
1436
|
+
query: Natural-language question.
|
|
1437
|
+
dataset_id: Optional dataset to ground the answer in. When omitted,
|
|
1438
|
+
the API chooses its configured/default scope.
|
|
1439
|
+
top_k: Number of retrieved chunks to consider. Defaults to ``5``.
|
|
1440
|
+
conversation_history: Optional prior chat turns.
|
|
1441
|
+
include_sources: Whether to include source chunks. Defaults to ``True``.
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
Iterator of JSON Server-Sent Event payloads.
|
|
1445
|
+
"""
|
|
1446
|
+
body = self._body(
|
|
1447
|
+
query,
|
|
1448
|
+
dataset_id=dataset_id,
|
|
1449
|
+
top_k=top_k,
|
|
1450
|
+
conversation_history=conversation_history,
|
|
1451
|
+
include_sources=include_sources,
|
|
1452
|
+
stream=True,
|
|
1453
|
+
)
|
|
1454
|
+
yield from self._transport.stream("POST", "/intelligence/query", json_body=body)
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def create_session(
|
|
1458
|
+
self,
|
|
1459
|
+
*,
|
|
1460
|
+
title: Optional[str] = None,
|
|
1461
|
+
workspace_id: Optional[str] = None,
|
|
1462
|
+
dataset_id: Optional[str] = None,
|
|
1463
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
1464
|
+
) -> JSON:
|
|
1465
|
+
"""Create a persistent Intelligence session."""
|
|
1466
|
+
body: JSON = {}
|
|
1467
|
+
if title is not None:
|
|
1468
|
+
body["title"] = title
|
|
1469
|
+
if workspace_id is not None:
|
|
1470
|
+
body["workspace_id"] = workspace_id
|
|
1471
|
+
if dataset_id is not None:
|
|
1472
|
+
body["dataset_id"] = dataset_id
|
|
1473
|
+
if metadata is not None:
|
|
1474
|
+
body["metadata"] = dict(metadata)
|
|
1475
|
+
return self._transport.request("POST", "/intelligence/sessions", json_body=body)
|
|
1476
|
+
|
|
1477
|
+
def list_sessions(self, *, limit: int = 50) -> JSON:
|
|
1478
|
+
"""List persistent Intelligence sessions."""
|
|
1479
|
+
return self._transport.request("GET", "/intelligence/sessions", params={"limit": limit})
|
|
1480
|
+
|
|
1481
|
+
def get_session(self, session_id: str) -> JSON:
|
|
1482
|
+
"""Fetch one persistent Intelligence session."""
|
|
1483
|
+
return self._transport.request("GET", f"/intelligence/sessions/{session_id}")
|
|
1484
|
+
|
|
1485
|
+
def delete_session(self, session_id: str) -> JSON:
|
|
1486
|
+
"""Delete a persistent Intelligence session."""
|
|
1487
|
+
return self._transport.request("DELETE", f"/intelligence/sessions/{session_id}")
|
|
1488
|
+
|
|
1489
|
+
def append_message(
|
|
1490
|
+
self,
|
|
1491
|
+
session_id: str,
|
|
1492
|
+
*,
|
|
1493
|
+
role: str,
|
|
1494
|
+
content: str,
|
|
1495
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
1496
|
+
) -> JSON:
|
|
1497
|
+
"""Append a message to a persistent Intelligence session."""
|
|
1498
|
+
body: JSON = {"role": role, "content": content}
|
|
1499
|
+
if metadata is not None:
|
|
1500
|
+
body["metadata"] = dict(metadata)
|
|
1501
|
+
return self._transport.request(
|
|
1502
|
+
"POST", f"/intelligence/sessions/{session_id}/messages", json_body=body
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
def list_messages(self, session_id: str, *, limit: int = 100) -> JSON:
|
|
1506
|
+
"""List messages for a persistent Intelligence session."""
|
|
1507
|
+
return self._transport.request(
|
|
1508
|
+
"GET", f"/intelligence/sessions/{session_id}/messages", params={"limit": limit}
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
@staticmethod
|
|
1512
|
+
def _body(
|
|
1513
|
+
query: str,
|
|
1514
|
+
*,
|
|
1515
|
+
dataset_id: Optional[str],
|
|
1516
|
+
top_k: int,
|
|
1517
|
+
conversation_history: Optional[Sequence[ConversationTurn]],
|
|
1518
|
+
include_sources: bool,
|
|
1519
|
+
stream: bool,
|
|
1520
|
+
) -> JSON:
|
|
1521
|
+
body: JSON = {
|
|
1522
|
+
"query": query,
|
|
1523
|
+
"top_k": top_k,
|
|
1524
|
+
"stream": stream,
|
|
1525
|
+
"include_sources": include_sources,
|
|
1526
|
+
}
|
|
1527
|
+
if dataset_id is not None:
|
|
1528
|
+
body["dataset_id"] = dataset_id
|
|
1529
|
+
if conversation_history is not None:
|
|
1530
|
+
body["conversation_history"] = list(conversation_history)
|
|
1531
|
+
return body
|