vectoramp 0.1.0__tar.gz → 0.2.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.
- {vectoramp-0.1.0 → vectoramp-0.2.0}/CHANGELOG.md +8 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/PKG-INFO +31 -8
- {vectoramp-0.1.0 → vectoramp-0.2.0}/README.md +30 -7
- {vectoramp-0.1.0 → vectoramp-0.2.0}/pyproject.toml +1 -1
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/__init__.py +2 -1
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/client.py +9 -1
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/resources.py +147 -2
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/transport.py +1 -1
- {vectoramp-0.1.0 → vectoramp-0.2.0}/tests/test_client.py +90 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/images/logo-full-dark.svg +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/images/logo-full-light.svg +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/workflows/ci.yml +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/workflows/publish.yml +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/.gitignore +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/CONTRIBUTING.md +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/LICENSE +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/NOTICE +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/SECURITY.md +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/connections.py +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/embeddings.py +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/exceptions.py +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/py.typed +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/sources.py +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/types.py +0 -0
- {vectoramp-0.1.0 → vectoramp-0.2.0}/tests/test_connections_and_sources.py +0 -0
|
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows semantic versioning.
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-07-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Add dataset/vector deletion helpers for deleting vectors by id.
|
|
12
|
+
- Add organization secret helpers and OpenAI-backed dataset creation convenience flow.
|
|
13
|
+
- Document OpenAI secret setup and vector deletion examples.
|
|
14
|
+
|
|
7
15
|
## [0.1.0] - 2026-07-02
|
|
8
16
|
|
|
9
17
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vectoramp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python SDK for the VectorAmp API
|
|
5
5
|
Project-URL: Homepage, https://vectoramp.com
|
|
6
6
|
Project-URL: Documentation, https://docs.vectoramp.com
|
|
@@ -31,9 +31,9 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
<div align="center">
|
|
32
32
|
<a href="https://vectoramp.com/">
|
|
33
33
|
<picture>
|
|
34
|
-
<source media="(prefers-color-scheme: light)" srcset=".
|
|
35
|
-
<source media="(prefers-color-scheme: dark)" srcset=".
|
|
36
|
-
<img alt="VectorAmp Logo" src=".
|
|
34
|
+
<source media="(prefers-color-scheme: light)" srcset="https://vectoramp.com/logo-full-light.svg">
|
|
35
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://vectoramp.com/logo-full-dark.svg">
|
|
36
|
+
<img alt="VectorAmp Logo" src="https://vectoramp.com/logo-full-dark.svg" width="50%">
|
|
37
37
|
</picture>
|
|
38
38
|
</a>
|
|
39
39
|
</div>
|
|
@@ -87,7 +87,7 @@ client = VectorAmp(api_key="va_...", base_url="http://localhost:8080")
|
|
|
87
87
|
|
|
88
88
|
## Datasets
|
|
89
89
|
|
|
90
|
-
Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. Built-in helpers infer dimensions for
|
|
90
|
+
Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. For normal use, omit embedding config and VectorAmp uses the managed `VectorAmp-Embedding-4B` model with inferred dim `2560`. Built-in helpers also infer dimensions for optional OpenAI BYOM models.
|
|
91
91
|
|
|
92
92
|
The only required argument is `name`. The default embedding is `VectorAmp-Embedding-4B` (`dim=2560`, `metric="cosine"`):
|
|
93
93
|
|
|
@@ -103,17 +103,32 @@ Enable hybrid (dense + sparse) indexing with `hybrid=True`:
|
|
|
103
103
|
dataset = client.datasets.create("product-docs", hybrid=True)
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Optional BYOM: use the `openai` helper only when you intentionally want an OpenAI embedding model (dimension is inferred):
|
|
107
107
|
|
|
108
108
|
```python
|
|
109
109
|
from vectoramp import openai
|
|
110
110
|
|
|
111
111
|
dataset = client.datasets.create(
|
|
112
|
-
"
|
|
112
|
+
"openai-docs",
|
|
113
113
|
embedding=openai("small"), # or openai("large")
|
|
114
114
|
)
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
To save or rotate the OpenAI API key as an organization secret first, then
|
|
118
|
+
create the dataset with `embedding.secret_ref`, use the helper:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
dataset = client.datasets.create_openai(
|
|
122
|
+
name="product-docs",
|
|
123
|
+
api_key="sk-...",
|
|
124
|
+
secret_ref="emb:openai:api_key", # default
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Or upsert the secret directly.
|
|
128
|
+
client.secrets.put_openai_api_key("sk-...")
|
|
129
|
+
client.org_secrets.update("emb:openai:api_key", "sk-rotated")
|
|
130
|
+
```
|
|
131
|
+
|
|
117
132
|
For a custom/unknown model you must pass `dim` explicitly:
|
|
118
133
|
|
|
119
134
|
```python
|
|
@@ -184,6 +199,13 @@ dataset.add_texts(
|
|
|
184
199
|
)
|
|
185
200
|
```
|
|
186
201
|
|
|
202
|
+
Delete vectors by id:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
dataset.delete_vectors(["doc-001", 42])
|
|
206
|
+
client.datasets.delete_vectors(dataset_id, ["doc-001", 42])
|
|
207
|
+
```
|
|
208
|
+
|
|
187
209
|
## Search
|
|
188
210
|
|
|
189
211
|
Search by text:
|
|
@@ -412,13 +434,14 @@ listed first; optional arguments show their default.
|
|
|
412
434
|
|
|
413
435
|
### Datasets (`client.datasets` / `Dataset`)
|
|
414
436
|
|
|
415
|
-
- `create(name, *, dim=None, metric="cosine", embedding=None, embedding_provider="vectoramp", embedding_model="VectorAmp-Embedding-4B", hybrid=False, filters=None, metadata_schema=None, tuning=None)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
|
|
437
|
+
- `create(name, *, dim=None, metric="cosine", embedding=None, embedding_provider="vectoramp", embedding_model="VectorAmp-Embedding-4B", hybrid=False, filters=None, metadata_schema=None, tuning=None, openai_api_key=None, openai_secret_ref="emb:openai:api_key", validate_openai_key=False)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
|
|
416
438
|
- `list(*, limit=50, offset=0)` → page with `Dataset` objects.
|
|
417
439
|
- `get(dataset_id)` → `Dataset`.
|
|
418
440
|
- `delete(dataset_id)` / `dataset.delete()`.
|
|
419
441
|
- `stats(dataset_id)` / `dataset.stats()`.
|
|
420
442
|
- `search(dataset_id, query=None, *, vector=None, text=None, search_text=None, top_k=10, filters=None, advanced_filters=None, embedding_provider=None, embedding_model=None, nprobe_override=None, rerank_depth_override=None, hybrid=None, sparse_query=None, alpha=None, include_embeddings=None, include_documents=None, include_metadata=None, rerank=None)` / `dataset.search(…)`. `query` accepts a string (text) or float sequence (vector); `top_k` defaults to 10.
|
|
421
443
|
- `insert(dataset_id, vectors)` and `insert_vectors(dataset_id, vectors)` / `dataset.insert(vectors)`. Record `id` may be `str` or `int` (integers stay JSON numbers).
|
|
444
|
+
- `delete_vectors(dataset_id, ids, *, write_concern=None)` / `dataset.delete_vectors(ids)`.
|
|
422
445
|
- `embed(dataset_id, *, text=None, texts=None)` / `dataset.embed(…)`.
|
|
423
446
|
- `add_texts(dataset_id, texts, *, ids=None, metadatas=None)` / `dataset.add_texts(texts, …)`. Single string or list; ids auto-generated when omitted; copies the text into `metadata.text`.
|
|
424
447
|
- `list_documents(dataset_id, *, limit=50, cursor=None, status=None)` / `dataset.list_documents(…)`.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<a href="https://vectoramp.com/">
|
|
3
3
|
<picture>
|
|
4
|
-
<source media="(prefers-color-scheme: light)" srcset=".
|
|
5
|
-
<source media="(prefers-color-scheme: dark)" srcset=".
|
|
6
|
-
<img alt="VectorAmp Logo" src=".
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://vectoramp.com/logo-full-light.svg">
|
|
5
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://vectoramp.com/logo-full-dark.svg">
|
|
6
|
+
<img alt="VectorAmp Logo" src="https://vectoramp.com/logo-full-dark.svg" width="50%">
|
|
7
7
|
</picture>
|
|
8
8
|
</a>
|
|
9
9
|
</div>
|
|
@@ -57,7 +57,7 @@ client = VectorAmp(api_key="va_...", base_url="http://localhost:8080")
|
|
|
57
57
|
|
|
58
58
|
## Datasets
|
|
59
59
|
|
|
60
|
-
Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. Built-in helpers infer dimensions for
|
|
60
|
+
Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. For normal use, omit embedding config and VectorAmp uses the managed `VectorAmp-Embedding-4B` model with inferred dim `2560`. Built-in helpers also infer dimensions for optional OpenAI BYOM models.
|
|
61
61
|
|
|
62
62
|
The only required argument is `name`. The default embedding is `VectorAmp-Embedding-4B` (`dim=2560`, `metric="cosine"`):
|
|
63
63
|
|
|
@@ -73,17 +73,32 @@ Enable hybrid (dense + sparse) indexing with `hybrid=True`:
|
|
|
73
73
|
dataset = client.datasets.create("product-docs", hybrid=True)
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
Optional BYOM: use the `openai` helper only when you intentionally want an OpenAI embedding model (dimension is inferred):
|
|
77
77
|
|
|
78
78
|
```python
|
|
79
79
|
from vectoramp import openai
|
|
80
80
|
|
|
81
81
|
dataset = client.datasets.create(
|
|
82
|
-
"
|
|
82
|
+
"openai-docs",
|
|
83
83
|
embedding=openai("small"), # or openai("large")
|
|
84
84
|
)
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
To save or rotate the OpenAI API key as an organization secret first, then
|
|
88
|
+
create the dataset with `embedding.secret_ref`, use the helper:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
dataset = client.datasets.create_openai(
|
|
92
|
+
name="product-docs",
|
|
93
|
+
api_key="sk-...",
|
|
94
|
+
secret_ref="emb:openai:api_key", # default
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Or upsert the secret directly.
|
|
98
|
+
client.secrets.put_openai_api_key("sk-...")
|
|
99
|
+
client.org_secrets.update("emb:openai:api_key", "sk-rotated")
|
|
100
|
+
```
|
|
101
|
+
|
|
87
102
|
For a custom/unknown model you must pass `dim` explicitly:
|
|
88
103
|
|
|
89
104
|
```python
|
|
@@ -154,6 +169,13 @@ dataset.add_texts(
|
|
|
154
169
|
)
|
|
155
170
|
```
|
|
156
171
|
|
|
172
|
+
Delete vectors by id:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
dataset.delete_vectors(["doc-001", 42])
|
|
176
|
+
client.datasets.delete_vectors(dataset_id, ["doc-001", 42])
|
|
177
|
+
```
|
|
178
|
+
|
|
157
179
|
## Search
|
|
158
180
|
|
|
159
181
|
Search by text:
|
|
@@ -382,13 +404,14 @@ listed first; optional arguments show their default.
|
|
|
382
404
|
|
|
383
405
|
### Datasets (`client.datasets` / `Dataset`)
|
|
384
406
|
|
|
385
|
-
- `create(name, *, dim=None, metric="cosine", embedding=None, embedding_provider="vectoramp", embedding_model="VectorAmp-Embedding-4B", hybrid=False, filters=None, metadata_schema=None, tuning=None)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
|
|
407
|
+
- `create(name, *, dim=None, metric="cosine", embedding=None, embedding_provider="vectoramp", embedding_model="VectorAmp-Embedding-4B", hybrid=False, filters=None, metadata_schema=None, tuning=None, openai_api_key=None, openai_secret_ref="emb:openai:api_key", validate_openai_key=False)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
|
|
386
408
|
- `list(*, limit=50, offset=0)` → page with `Dataset` objects.
|
|
387
409
|
- `get(dataset_id)` → `Dataset`.
|
|
388
410
|
- `delete(dataset_id)` / `dataset.delete()`.
|
|
389
411
|
- `stats(dataset_id)` / `dataset.stats()`.
|
|
390
412
|
- `search(dataset_id, query=None, *, vector=None, text=None, search_text=None, top_k=10, filters=None, advanced_filters=None, embedding_provider=None, embedding_model=None, nprobe_override=None, rerank_depth_override=None, hybrid=None, sparse_query=None, alpha=None, include_embeddings=None, include_documents=None, include_metadata=None, rerank=None)` / `dataset.search(…)`. `query` accepts a string (text) or float sequence (vector); `top_k` defaults to 10.
|
|
391
413
|
- `insert(dataset_id, vectors)` and `insert_vectors(dataset_id, vectors)` / `dataset.insert(vectors)`. Record `id` may be `str` or `int` (integers stay JSON numbers).
|
|
414
|
+
- `delete_vectors(dataset_id, ids, *, write_concern=None)` / `dataset.delete_vectors(ids)`.
|
|
392
415
|
- `embed(dataset_id, *, text=None, texts=None)` / `dataset.embed(…)`.
|
|
393
416
|
- `add_texts(dataset_id, texts, *, ids=None, metadatas=None)` / `dataset.add_texts(texts, …)`. Single string or list; ids auto-generated when omitted; copies the text into `metadata.text`.
|
|
394
417
|
- `list_documents(dataset_id, *, limit=50, cursor=None, status=None)` / `dataset.list_documents(…)`.
|
|
@@ -10,7 +10,7 @@ from .embeddings import (
|
|
|
10
10
|
openai,
|
|
11
11
|
)
|
|
12
12
|
from .exceptions import APIError, AuthenticationError, VectorAmpError
|
|
13
|
-
from .resources import Dataset
|
|
13
|
+
from .resources import Dataset, OrgSecretsResource
|
|
14
14
|
from .sources import (
|
|
15
15
|
ConfluenceSource,
|
|
16
16
|
FileUploadSource,
|
|
@@ -36,6 +36,7 @@ __all__ = [
|
|
|
36
36
|
"JiraSource",
|
|
37
37
|
"OPENAI_TEXT_EMBEDDING_3_LARGE",
|
|
38
38
|
"OPENAI_TEXT_EMBEDDING_3_SMALL",
|
|
39
|
+
"OrgSecretsResource",
|
|
39
40
|
"S3Source",
|
|
40
41
|
"VectorAmp",
|
|
41
42
|
"VECTORAMP_EMBEDDING_4B",
|
|
@@ -9,7 +9,13 @@ from typing import Iterator, Optional, Type
|
|
|
9
9
|
import httpx
|
|
10
10
|
|
|
11
11
|
from .connections import ConnectionsResource
|
|
12
|
-
from .resources import
|
|
12
|
+
from .resources import (
|
|
13
|
+
DatasetsResource,
|
|
14
|
+
IngestionResource,
|
|
15
|
+
IntelligenceResource,
|
|
16
|
+
OrgSecretsResource,
|
|
17
|
+
SchedulesResource,
|
|
18
|
+
)
|
|
13
19
|
from .transport import BaseTransport, RestTransport
|
|
14
20
|
from .types import JSON, ConversationTurn
|
|
15
21
|
|
|
@@ -54,6 +60,8 @@ class VectorAmp:
|
|
|
54
60
|
self.ingestion = IngestionResource(self.transport)
|
|
55
61
|
self.sources = self.ingestion
|
|
56
62
|
self.connections = ConnectionsResource(self.transport)
|
|
63
|
+
self.secrets = OrgSecretsResource(self.transport)
|
|
64
|
+
self.org_secrets = self.secrets
|
|
57
65
|
self.schedules = SchedulesResource(self.transport)
|
|
58
66
|
self.intelligence = IntelligenceResource(self.transport)
|
|
59
67
|
|
|
@@ -7,8 +7,15 @@ import uuid
|
|
|
7
7
|
from collections.abc import ItemsView, KeysView, ValuesView
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, Iterator, Mapping, Optional, Sequence, Union
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
|
|
12
|
+
from .embeddings import (
|
|
13
|
+
EMBEDDING_DIMENSIONS,
|
|
14
|
+
OPENAI_TEXT_EMBEDDING_3_SMALL,
|
|
15
|
+
VECTORAMP_EMBEDDING_4B,
|
|
16
|
+
OpenAIEmbeddingSize,
|
|
17
|
+
openai,
|
|
18
|
+
)
|
|
12
19
|
from .sources import (
|
|
13
20
|
ConfluenceSource,
|
|
14
21
|
FileUploadSource,
|
|
@@ -137,6 +144,15 @@ class Dataset:
|
|
|
137
144
|
"""
|
|
138
145
|
return self.service.insert_vectors(self.id, vectors)
|
|
139
146
|
|
|
147
|
+
def delete_vectors(
|
|
148
|
+
self,
|
|
149
|
+
ids: Sequence[VectorId],
|
|
150
|
+
*,
|
|
151
|
+
write_concern: Optional[Mapping[str, Any]] = None,
|
|
152
|
+
) -> JSON:
|
|
153
|
+
"""Delete vectors from this dataset by id."""
|
|
154
|
+
return self.service.delete_vectors(self.id, ids, write_concern=write_concern)
|
|
155
|
+
|
|
140
156
|
def add_texts(
|
|
141
157
|
self,
|
|
142
158
|
texts: Union[str, Sequence[str]],
|
|
@@ -362,6 +378,9 @@ class DatasetsResource:
|
|
|
362
378
|
filters: Optional[Mapping[str, Any]] = None,
|
|
363
379
|
metadata_schema: Optional[Mapping[str, Any]] = None,
|
|
364
380
|
tuning: Optional[Mapping[str, Any]] = None,
|
|
381
|
+
openai_api_key: Optional[str] = None,
|
|
382
|
+
openai_secret_ref: str = "emb:openai:api_key",
|
|
383
|
+
validate_openai_key: bool = False,
|
|
365
384
|
) -> Dataset:
|
|
366
385
|
"""Create a SABLE dataset.
|
|
367
386
|
|
|
@@ -384,13 +403,38 @@ class DatasetsResource:
|
|
|
384
403
|
filters: Optional filter schema/configuration.
|
|
385
404
|
metadata_schema: Optional metadata schema.
|
|
386
405
|
tuning: Optional SABLE tuning parameters.
|
|
406
|
+
openai_api_key: Optional OpenAI API key to store as an organization
|
|
407
|
+
secret before creating the dataset. When provided, and no
|
|
408
|
+
explicit embedding is passed, the dataset uses OpenAI
|
|
409
|
+
``text-embedding-3-small`` with ``secret_ref`` set to
|
|
410
|
+
``openai_secret_ref``.
|
|
411
|
+
openai_secret_ref: Organization-secret name used for the OpenAI key.
|
|
412
|
+
validate_openai_key: Ask the API to validate the OpenAI key before
|
|
413
|
+
storing it. Defaults to ``False``.
|
|
387
414
|
|
|
388
415
|
Returns:
|
|
389
416
|
Created ``Dataset`` object.
|
|
390
417
|
"""
|
|
418
|
+
if openai_api_key is not None:
|
|
419
|
+
if self.client is None or not hasattr(self.client, "secrets"):
|
|
420
|
+
raise TypeError("openai_api_key requires a DatasetResource owned by VectorAmp.")
|
|
421
|
+
validation_model = str(
|
|
422
|
+
(embedding or {}).get("model") or OPENAI_TEXT_EMBEDDING_3_SMALL
|
|
423
|
+
)
|
|
424
|
+
self.client.secrets.put(
|
|
425
|
+
openai_secret_ref,
|
|
426
|
+
openai_api_key,
|
|
427
|
+
validate=validate_openai_key,
|
|
428
|
+
model=validation_model,
|
|
429
|
+
)
|
|
430
|
+
if embedding is None and embedding_provider == "vectoramp":
|
|
431
|
+
embedding = openai("small")
|
|
432
|
+
|
|
391
433
|
embedding_config = {"provider": embedding_provider, "model": embedding_model}
|
|
392
434
|
if embedding is not None:
|
|
393
435
|
embedding_config.update(dict(embedding))
|
|
436
|
+
if openai_api_key is not None and embedding_config.get("provider") == "openai":
|
|
437
|
+
embedding_config["secret_ref"] = openai_secret_ref
|
|
394
438
|
resolved_dim = dim or EMBEDDING_DIMENSIONS.get(str(embedding_config.get("model")))
|
|
395
439
|
if resolved_dim is None:
|
|
396
440
|
raise ValueError("dim is required for custom embedding models")
|
|
@@ -412,6 +456,38 @@ class DatasetsResource:
|
|
|
412
456
|
body["tuning"] = dict(tuning)
|
|
413
457
|
return self._to_dataset(self._transport.request("POST", "/datasets", json_body=body))
|
|
414
458
|
|
|
459
|
+
def create_openai(
|
|
460
|
+
self,
|
|
461
|
+
*,
|
|
462
|
+
name: str,
|
|
463
|
+
api_key: str,
|
|
464
|
+
size: OpenAIEmbeddingSize = "small",
|
|
465
|
+
secret_ref: str = "emb:openai:api_key",
|
|
466
|
+
validate: bool = False,
|
|
467
|
+
dim: Optional[int] = None,
|
|
468
|
+
metric: Metric = "cosine",
|
|
469
|
+
hybrid: bool = False,
|
|
470
|
+
filters: Optional[Mapping[str, Any]] = None,
|
|
471
|
+
metadata_schema: Optional[Mapping[str, Any]] = None,
|
|
472
|
+
tuning: Optional[Mapping[str, Any]] = None,
|
|
473
|
+
) -> Dataset:
|
|
474
|
+
"""Store an OpenAI org secret, then create an OpenAI-backed dataset."""
|
|
475
|
+
embedding_config = openai(size)
|
|
476
|
+
embedding_config["secret_ref"] = secret_ref
|
|
477
|
+
return self.create(
|
|
478
|
+
name=name,
|
|
479
|
+
dim=dim,
|
|
480
|
+
metric=metric,
|
|
481
|
+
embedding=embedding_config,
|
|
482
|
+
hybrid=hybrid,
|
|
483
|
+
filters=filters,
|
|
484
|
+
metadata_schema=metadata_schema,
|
|
485
|
+
tuning=tuning,
|
|
486
|
+
openai_api_key=api_key,
|
|
487
|
+
openai_secret_ref=secret_ref,
|
|
488
|
+
validate_openai_key=validate,
|
|
489
|
+
)
|
|
490
|
+
|
|
415
491
|
def delete(self, dataset_id: str) -> Any:
|
|
416
492
|
"""Delete a dataset and return the API response.
|
|
417
493
|
|
|
@@ -578,6 +654,23 @@ class DatasetsResource:
|
|
|
578
654
|
"""Alias for ``insert_vectors``."""
|
|
579
655
|
return self.insert_vectors(dataset_id, vectors)
|
|
580
656
|
|
|
657
|
+
def delete_vectors(
|
|
658
|
+
self,
|
|
659
|
+
dataset_id: str,
|
|
660
|
+
ids: Sequence[VectorId],
|
|
661
|
+
*,
|
|
662
|
+
write_concern: Optional[Mapping[str, Any]] = None,
|
|
663
|
+
) -> JSON:
|
|
664
|
+
"""Delete vectors from a dataset by id."""
|
|
665
|
+
if not ids:
|
|
666
|
+
raise ValueError("ids must contain at least one vector id.")
|
|
667
|
+
body: JSON = {"ids": list(ids)}
|
|
668
|
+
if write_concern is not None:
|
|
669
|
+
body["write_concern"] = dict(write_concern)
|
|
670
|
+
return self._transport.request(
|
|
671
|
+
"DELETE", f"/api/v1/datasets/{dataset_id}/vectors", json_body=body
|
|
672
|
+
)
|
|
673
|
+
|
|
581
674
|
def embed(
|
|
582
675
|
self,
|
|
583
676
|
dataset_id: str,
|
|
@@ -657,6 +750,58 @@ class DatasetsResource:
|
|
|
657
750
|
return Dataset(self, raw_data)
|
|
658
751
|
|
|
659
752
|
|
|
753
|
+
class OrgSecretsResource:
|
|
754
|
+
"""Organization secret helpers."""
|
|
755
|
+
|
|
756
|
+
def __init__(self, transport: BaseTransport) -> None:
|
|
757
|
+
self._transport = transport
|
|
758
|
+
|
|
759
|
+
def put(
|
|
760
|
+
self,
|
|
761
|
+
name: str,
|
|
762
|
+
value: str,
|
|
763
|
+
*,
|
|
764
|
+
validate: bool = False,
|
|
765
|
+
model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
|
|
766
|
+
) -> Any:
|
|
767
|
+
"""Create or update an organization secret.
|
|
768
|
+
|
|
769
|
+
Stores provider credentials through the generic organization-secret
|
|
770
|
+
REST endpoint. Plaintext is sent once and is never returned by reads.
|
|
771
|
+
"""
|
|
772
|
+
if not name:
|
|
773
|
+
raise ValueError("name is required.")
|
|
774
|
+
if not value:
|
|
775
|
+
raise ValueError("value is required.")
|
|
776
|
+
return self._transport.request(
|
|
777
|
+
"PUT",
|
|
778
|
+
f"/org-secrets/{quote(name, safe='')}",
|
|
779
|
+
json_body={"value": value},
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
def update(
|
|
783
|
+
self,
|
|
784
|
+
name: str,
|
|
785
|
+
value: str,
|
|
786
|
+
*,
|
|
787
|
+
validate: bool = False,
|
|
788
|
+
model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
|
|
789
|
+
) -> Any:
|
|
790
|
+
"""Alias for :meth:`put`; org secrets are upserted by name."""
|
|
791
|
+
return self.put(name, value, validate=validate, model=model)
|
|
792
|
+
|
|
793
|
+
def put_openai_api_key(
|
|
794
|
+
self,
|
|
795
|
+
api_key: str,
|
|
796
|
+
*,
|
|
797
|
+
secret_ref: str = "emb:openai:api_key",
|
|
798
|
+
validate: bool = False,
|
|
799
|
+
model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
|
|
800
|
+
) -> Any:
|
|
801
|
+
"""Create or update the OpenAI API key organization secret."""
|
|
802
|
+
return self.put(secret_ref, api_key, validate=validate, model=model)
|
|
803
|
+
|
|
804
|
+
|
|
660
805
|
class IngestionResource:
|
|
661
806
|
"""Sources, upload sessions, and ingestion jobs."""
|
|
662
807
|
|
|
@@ -157,7 +157,7 @@ class RestTransport(BaseTransport):
|
|
|
157
157
|
return urljoin(self.base_url, path.lstrip("/"))
|
|
158
158
|
|
|
159
159
|
def _headers(self, headers: Optional[Mapping[str, str]] = None) -> Dict[str, str]:
|
|
160
|
-
merged = {"X-API-Key": self.api_key, "User-Agent": "vectoramp-python/0.
|
|
160
|
+
merged = {"X-API-Key": self.api_key, "User-Agent": "vectoramp-python/0.2.0"}
|
|
161
161
|
if headers:
|
|
162
162
|
merged.update(headers)
|
|
163
163
|
return merged
|
|
@@ -133,6 +133,96 @@ def test_dataset_create_openai_helper_infers_dimension() -> None:
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def test_dataset_create_openai_stores_secret_then_uses_secret_ref() -> None:
|
|
137
|
+
calls: list[tuple[str, str, dict[str, Any]]] = []
|
|
138
|
+
|
|
139
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
140
|
+
body = json.loads(request.content) if request.content else {}
|
|
141
|
+
calls.append((request.method, request.url.path, body))
|
|
142
|
+
if request.url.path == "/org-secrets/emb%3Aopenai%3Aapi_key":
|
|
143
|
+
return httpx.Response(204)
|
|
144
|
+
return json_response({"id": "ds_openai", "index_type": "sable"}, 201)
|
|
145
|
+
|
|
146
|
+
client = make_client(handler)
|
|
147
|
+
dataset = client.datasets.create_openai(
|
|
148
|
+
name="openai-docs",
|
|
149
|
+
api_key="sk-test",
|
|
150
|
+
secret_ref="emb:openai:customer-a",
|
|
151
|
+
validate=True,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert dataset.id == "ds_openai"
|
|
155
|
+
assert calls == [
|
|
156
|
+
(
|
|
157
|
+
"PUT",
|
|
158
|
+
"/org-secrets/emb:openai:customer-a",
|
|
159
|
+
{"value": "sk-test"},
|
|
160
|
+
),
|
|
161
|
+
(
|
|
162
|
+
"POST",
|
|
163
|
+
"/datasets",
|
|
164
|
+
{
|
|
165
|
+
"name": "openai-docs",
|
|
166
|
+
"dim": 1536,
|
|
167
|
+
"metric": "cosine",
|
|
168
|
+
"embedding": {
|
|
169
|
+
"provider": "openai",
|
|
170
|
+
"model": "text-embedding-3-small",
|
|
171
|
+
"secret_ref": "emb:openai:customer-a",
|
|
172
|
+
},
|
|
173
|
+
"index_type": "sable",
|
|
174
|
+
},
|
|
175
|
+
),
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_org_secrets_put_update_openai_key() -> None:
|
|
180
|
+
calls: list[dict[str, Any]] = []
|
|
181
|
+
|
|
182
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
183
|
+
calls.append(json.loads(request.content))
|
|
184
|
+
return httpx.Response(204)
|
|
185
|
+
|
|
186
|
+
client = make_client(handler)
|
|
187
|
+
assert client.secrets.put("emb:openai:api_key", "sk-new") is None
|
|
188
|
+
assert client.org_secrets.update("emb:openai:api_key", "sk-rotated") is None
|
|
189
|
+
|
|
190
|
+
assert calls[0]["value"] == "sk-new"
|
|
191
|
+
assert calls[1]["value"] == "sk-rotated"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_delete_vectors_payload_and_dataset_helper() -> None:
|
|
195
|
+
calls: list[tuple[str, str, dict[str, Any]]] = []
|
|
196
|
+
|
|
197
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
198
|
+
body = json.loads(request.content) if request.content else {}
|
|
199
|
+
calls.append((request.method, request.url.path, body))
|
|
200
|
+
return json_response({"deleted": 2, "ids": body.get("ids", [])})
|
|
201
|
+
|
|
202
|
+
client = make_client(handler)
|
|
203
|
+
result = client.datasets.delete_vectors(
|
|
204
|
+
"ds_1", ["vec-a", 42], write_concern={"wait": True}
|
|
205
|
+
)
|
|
206
|
+
dataset = Dataset(client.datasets, {"id": "ds_1"})
|
|
207
|
+
dataset.delete_vectors(["vec-c"])
|
|
208
|
+
|
|
209
|
+
assert result["deleted"] == 2
|
|
210
|
+
assert calls == [
|
|
211
|
+
(
|
|
212
|
+
"DELETE",
|
|
213
|
+
"/api/v1/datasets/ds_1/vectors",
|
|
214
|
+
{"ids": ["vec-a", 42], "write_concern": {"wait": True}},
|
|
215
|
+
),
|
|
216
|
+
("DELETE", "/api/v1/datasets/ds_1/vectors", {"ids": ["vec-c"]}),
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_delete_vectors_requires_ids() -> None:
|
|
221
|
+
client = make_client(lambda request: json_response({}))
|
|
222
|
+
with pytest.raises(ValueError):
|
|
223
|
+
client.datasets.delete_vectors("ds_1", [])
|
|
224
|
+
|
|
225
|
+
|
|
136
226
|
def test_list_get_delete_and_stats() -> None:
|
|
137
227
|
calls = []
|
|
138
228
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|