vectoramp 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. vectoramp-0.3.0/CHANGELOG.md +28 -0
  2. {vectoramp-0.1.0 → vectoramp-0.3.0}/PKG-INFO +49 -8
  3. {vectoramp-0.1.0 → vectoramp-0.3.0}/README.md +48 -7
  4. {vectoramp-0.1.0 → vectoramp-0.3.0}/pyproject.toml +1 -1
  5. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/__init__.py +6 -1
  6. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/client.py +9 -1
  7. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/resources.py +193 -6
  8. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/transport.py +1 -1
  9. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/types.py +12 -0
  10. {vectoramp-0.1.0 → vectoramp-0.3.0}/tests/test_client.py +135 -4
  11. vectoramp-0.1.0/CHANGELOG.md +0 -12
  12. {vectoramp-0.1.0 → vectoramp-0.3.0}/.github/images/logo-full-dark.svg +0 -0
  13. {vectoramp-0.1.0 → vectoramp-0.3.0}/.github/images/logo-full-light.svg +0 -0
  14. {vectoramp-0.1.0 → vectoramp-0.3.0}/.github/workflows/ci.yml +0 -0
  15. {vectoramp-0.1.0 → vectoramp-0.3.0}/.github/workflows/publish.yml +0 -0
  16. {vectoramp-0.1.0 → vectoramp-0.3.0}/.gitignore +0 -0
  17. {vectoramp-0.1.0 → vectoramp-0.3.0}/CONTRIBUTING.md +0 -0
  18. {vectoramp-0.1.0 → vectoramp-0.3.0}/LICENSE +0 -0
  19. {vectoramp-0.1.0 → vectoramp-0.3.0}/NOTICE +0 -0
  20. {vectoramp-0.1.0 → vectoramp-0.3.0}/SECURITY.md +0 -0
  21. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/connections.py +0 -0
  22. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/embeddings.py +0 -0
  23. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/exceptions.py +0 -0
  24. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/py.typed +0 -0
  25. {vectoramp-0.1.0 → vectoramp-0.3.0}/src/vectoramp/sources.py +0 -0
  26. {vectoramp-0.1.0 → vectoramp-0.3.0}/tests/test_connections_and_sources.py +0 -0
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ This project follows semantic versioning.
6
+
7
+ ## [0.3.0] - 2026-07-20
8
+
9
+ ### Added
10
+
11
+ - Add typed metadata-schema fields when creating datasets.
12
+ - Add metadata-schema merge/patch and full replacement operations.
13
+ - Document create, merge, and replace schema workflows.
14
+
15
+ ## [0.2.0] - 2026-07-14
16
+
17
+ ### Added
18
+
19
+ - Add dataset/vector deletion helpers for deleting vectors by id.
20
+ - Add organization secret helpers and OpenAI-backed dataset creation convenience flow.
21
+ - Document OpenAI secret setup and vector deletion examples.
22
+
23
+ ## [0.1.0] - 2026-07-02
24
+
25
+ ### Added
26
+
27
+ - Initial public-ready package baseline for VectorAmp SDK/CLI migration to GitHub.
28
+ - GitHub Actions CI workflow.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectoramp
3
- Version: 0.1.0
3
+ Version: 0.3.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=".github/images/logo-full-light.svg">
35
- <source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-full-dark.svg">
36
- <img alt="VectorAmp Logo" src=".github/images/logo-full-dark.svg" width="50%">
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 VectorAmp 4B and OpenAI `text-embedding-3-small`/`text-embedding-3-large`.
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,48 @@ Enable hybrid (dense + sparse) indexing with `hybrid=True`:
103
103
  dataset = client.datasets.create("product-docs", hybrid=True)
104
104
  ```
105
105
 
106
- Bring your own embedding model with the `openai` helper (dimension is inferred):
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
- "product-docs",
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
+
132
+ Declare typed metadata at creation, then merge fields or replace the complete schema:
133
+
134
+ ```python
135
+ from vectoramp import MetadataSchemaField
136
+
137
+ schema: list[MetadataSchemaField] = [
138
+ {"name": "category", "type": "string"},
139
+ {"name": "price", "type": "f32"},
140
+ ]
141
+ dataset = client.datasets.create(name="products", metadata_schema=schema)
142
+ dataset.patch_metadata_schema([{"name": "inventory", "type": "u32"}])
143
+ dataset.replace_metadata_schema(schema) # [] removes all declared fields
144
+ ```
145
+
146
+ Canonical field types are `string`, `u32`, `i32`, `i64`, `f32`, and `f64`.
147
+
117
148
  For a custom/unknown model you must pass `dim` explicitly:
118
149
 
119
150
  ```python
@@ -184,6 +215,13 @@ dataset.add_texts(
184
215
  )
185
216
  ```
186
217
 
218
+ Delete vectors by id:
219
+
220
+ ```python
221
+ dataset.delete_vectors(["doc-001", 42])
222
+ client.datasets.delete_vectors(dataset_id, ["doc-001", 42])
223
+ ```
224
+
187
225
  ## Search
188
226
 
189
227
  Search by text:
@@ -412,13 +450,16 @@ listed first; optional arguments show their default.
412
450
 
413
451
  ### Datasets (`client.datasets` / `Dataset`)
414
452
 
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.
453
+ - `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
454
  - `list(*, limit=50, offset=0)` → page with `Dataset` objects.
417
455
  - `get(dataset_id)` → `Dataset`.
456
+ - `patch_metadata_schema(dataset_id, schema)` / `Dataset.patch_metadata_schema(schema)` → updated `Dataset`.
457
+ - `replace_metadata_schema(dataset_id, schema)` / `Dataset.replace_metadata_schema(schema)` → updated `Dataset`.
418
458
  - `delete(dataset_id)` / `dataset.delete()`.
419
459
  - `stats(dataset_id)` / `dataset.stats()`.
420
460
  - `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
461
  - `insert(dataset_id, vectors)` and `insert_vectors(dataset_id, vectors)` / `dataset.insert(vectors)`. Record `id` may be `str` or `int` (integers stay JSON numbers).
462
+ - `delete_vectors(dataset_id, ids, *, write_concern=None)` / `dataset.delete_vectors(ids)`.
422
463
  - `embed(dataset_id, *, text=None, texts=None)` / `dataset.embed(…)`.
423
464
  - `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
465
  - `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=".github/images/logo-full-light.svg">
5
- <source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-full-dark.svg">
6
- <img alt="VectorAmp Logo" src=".github/images/logo-full-dark.svg" width="50%">
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 VectorAmp 4B and OpenAI `text-embedding-3-small`/`text-embedding-3-large`.
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,48 @@ Enable hybrid (dense + sparse) indexing with `hybrid=True`:
73
73
  dataset = client.datasets.create("product-docs", hybrid=True)
74
74
  ```
75
75
 
76
- Bring your own embedding model with the `openai` helper (dimension is inferred):
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
- "product-docs",
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
+
102
+ Declare typed metadata at creation, then merge fields or replace the complete schema:
103
+
104
+ ```python
105
+ from vectoramp import MetadataSchemaField
106
+
107
+ schema: list[MetadataSchemaField] = [
108
+ {"name": "category", "type": "string"},
109
+ {"name": "price", "type": "f32"},
110
+ ]
111
+ dataset = client.datasets.create(name="products", metadata_schema=schema)
112
+ dataset.patch_metadata_schema([{"name": "inventory", "type": "u32"}])
113
+ dataset.replace_metadata_schema(schema) # [] removes all declared fields
114
+ ```
115
+
116
+ Canonical field types are `string`, `u32`, `i32`, `i64`, `f32`, and `f64`.
117
+
87
118
  For a custom/unknown model you must pass `dim` explicitly:
88
119
 
89
120
  ```python
@@ -154,6 +185,13 @@ dataset.add_texts(
154
185
  )
155
186
  ```
156
187
 
188
+ Delete vectors by id:
189
+
190
+ ```python
191
+ dataset.delete_vectors(["doc-001", 42])
192
+ client.datasets.delete_vectors(dataset_id, ["doc-001", 42])
193
+ ```
194
+
157
195
  ## Search
158
196
 
159
197
  Search by text:
@@ -382,13 +420,16 @@ listed first; optional arguments show their default.
382
420
 
383
421
  ### Datasets (`client.datasets` / `Dataset`)
384
422
 
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.
423
+ - `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
424
  - `list(*, limit=50, offset=0)` → page with `Dataset` objects.
387
425
  - `get(dataset_id)` → `Dataset`.
426
+ - `patch_metadata_schema(dataset_id, schema)` / `Dataset.patch_metadata_schema(schema)` → updated `Dataset`.
427
+ - `replace_metadata_schema(dataset_id, schema)` / `Dataset.replace_metadata_schema(schema)` → updated `Dataset`.
388
428
  - `delete(dataset_id)` / `dataset.delete()`.
389
429
  - `stats(dataset_id)` / `dataset.stats()`.
390
430
  - `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
431
  - `insert(dataset_id, vectors)` and `insert_vectors(dataset_id, vectors)` / `dataset.insert(vectors)`. Record `id` may be `str` or `int` (integers stay JSON numbers).
432
+ - `delete_vectors(dataset_id, ids, *, write_concern=None)` / `dataset.delete_vectors(ids)`.
392
433
  - `embed(dataset_id, *, text=None, texts=None)` / `dataset.embed(…)`.
393
434
  - `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
435
  - `list_documents(dataset_id, *, limit=50, cursor=None, status=None)` / `dataset.list_documents(…)`.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vectoramp"
7
- version = "0.1.0"
7
+ version = "0.3.0"
8
8
  description = "Python SDK for the VectorAmp API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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,
@@ -21,6 +21,7 @@ from .sources import (
21
21
  S3Source,
22
22
  WebSource,
23
23
  )
24
+ from .types import MetadataFieldType, MetadataSchema, MetadataSchemaField
24
25
 
25
26
  __all__ = [
26
27
  "APIError",
@@ -34,8 +35,12 @@ __all__ = [
34
35
  "GenericSource",
35
36
  "GoogleDriveSource",
36
37
  "JiraSource",
38
+ "MetadataFieldType",
39
+ "MetadataSchema",
40
+ "MetadataSchemaField",
37
41
  "OPENAI_TEXT_EMBEDDING_3_LARGE",
38
42
  "OPENAI_TEXT_EMBEDDING_3_SMALL",
43
+ "OrgSecretsResource",
39
44
  "S3Source",
40
45
  "VectorAmp",
41
46
  "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 DatasetsResource, IngestionResource, IntelligenceResource, SchedulesResource
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
 
@@ -6,9 +6,16 @@ import mimetypes
6
6
  import uuid
7
7
  from collections.abc import ItemsView, KeysView, ValuesView
8
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
9
+ from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Union
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,
@@ -21,7 +28,17 @@ from .sources import (
21
28
  WebSource,
22
29
  )
23
30
  from .transport import BaseTransport, RestTransport
24
- from .types import JSON, AdvancedFilter, ConversationTurn, Filters, Metric, Vector, VectorId
31
+ from .types import (
32
+ JSON,
33
+ AdvancedFilter,
34
+ ConversationTurn,
35
+ Filters,
36
+ MetadataSchema,
37
+ MetadataSchemaInput,
38
+ Metric,
39
+ Vector,
40
+ VectorId,
41
+ )
25
42
 
26
43
  PathLike = Union[str, Path]
27
44
 
@@ -137,6 +154,15 @@ class Dataset:
137
154
  """
138
155
  return self.service.insert_vectors(self.id, vectors)
139
156
 
157
+ def delete_vectors(
158
+ self,
159
+ ids: Sequence[VectorId],
160
+ *,
161
+ write_concern: Optional[Mapping[str, Any]] = None,
162
+ ) -> JSON:
163
+ """Delete vectors from this dataset by id."""
164
+ return self.service.delete_vectors(self.id, ids, write_concern=write_concern)
165
+
140
166
  def add_texts(
141
167
  self,
142
168
  texts: Union[str, Sequence[str]],
@@ -274,6 +300,14 @@ class Dataset:
274
300
  pipeline_id=pipeline_id,
275
301
  )
276
302
 
303
+ def patch_metadata_schema(self, schema: MetadataSchema) -> "Dataset":
304
+ """Add or update typed metadata fields while retaining other fields."""
305
+ return self.service.patch_metadata_schema(self.id, schema)
306
+
307
+ def replace_metadata_schema(self, schema: MetadataSchema) -> "Dataset":
308
+ """Replace the complete typed metadata schema."""
309
+ return self.service.replace_metadata_schema(self.id, schema)
310
+
277
311
  def get(self, key: str, default: Any = None) -> Any:
278
312
  """Return a value from the raw dataset payload, or ``default``."""
279
313
  return self.raw_data.get(key, default)
@@ -360,8 +394,11 @@ class DatasetsResource:
360
394
  embedding_model: str = VECTORAMP_EMBEDDING_4B,
361
395
  hybrid: bool = False,
362
396
  filters: Optional[Mapping[str, Any]] = None,
363
- metadata_schema: Optional[Mapping[str, Any]] = None,
397
+ metadata_schema: Optional[MetadataSchemaInput] = None,
364
398
  tuning: Optional[Mapping[str, Any]] = None,
399
+ openai_api_key: Optional[str] = None,
400
+ openai_secret_ref: str = "emb:openai:api_key",
401
+ validate_openai_key: bool = False,
365
402
  ) -> Dataset:
366
403
  """Create a SABLE dataset.
367
404
 
@@ -384,13 +421,38 @@ class DatasetsResource:
384
421
  filters: Optional filter schema/configuration.
385
422
  metadata_schema: Optional metadata schema.
386
423
  tuning: Optional SABLE tuning parameters.
424
+ openai_api_key: Optional OpenAI API key to store as an organization
425
+ secret before creating the dataset. When provided, and no
426
+ explicit embedding is passed, the dataset uses OpenAI
427
+ ``text-embedding-3-small`` with ``secret_ref`` set to
428
+ ``openai_secret_ref``.
429
+ openai_secret_ref: Organization-secret name used for the OpenAI key.
430
+ validate_openai_key: Ask the API to validate the OpenAI key before
431
+ storing it. Defaults to ``False``.
387
432
 
388
433
  Returns:
389
434
  Created ``Dataset`` object.
390
435
  """
436
+ if openai_api_key is not None:
437
+ if self.client is None or not hasattr(self.client, "secrets"):
438
+ raise TypeError("openai_api_key requires a DatasetResource owned by VectorAmp.")
439
+ validation_model = str(
440
+ (embedding or {}).get("model") or OPENAI_TEXT_EMBEDDING_3_SMALL
441
+ )
442
+ self.client.secrets.put(
443
+ openai_secret_ref,
444
+ openai_api_key,
445
+ validate=validate_openai_key,
446
+ model=validation_model,
447
+ )
448
+ if embedding is None and embedding_provider == "vectoramp":
449
+ embedding = openai("small")
450
+
391
451
  embedding_config = {"provider": embedding_provider, "model": embedding_model}
392
452
  if embedding is not None:
393
453
  embedding_config.update(dict(embedding))
454
+ if openai_api_key is not None and embedding_config.get("provider") == "openai":
455
+ embedding_config["secret_ref"] = openai_secret_ref
394
456
  resolved_dim = dim or EMBEDDING_DIMENSIONS.get(str(embedding_config.get("model")))
395
457
  if resolved_dim is None:
396
458
  raise ValueError("dim is required for custom embedding models")
@@ -407,11 +469,67 @@ class DatasetsResource:
407
469
  if filters is not None:
408
470
  body["filters"] = dict(filters)
409
471
  if metadata_schema is not None:
410
- body["metadata_schema"] = dict(metadata_schema)
472
+ body["schema"] = self._normalize_metadata_schema(metadata_schema)
411
473
  if tuning is not None:
412
474
  body["tuning"] = dict(tuning)
413
475
  return self._to_dataset(self._transport.request("POST", "/datasets", json_body=body))
414
476
 
477
+ def create_openai(
478
+ self,
479
+ *,
480
+ name: str,
481
+ api_key: str,
482
+ size: OpenAIEmbeddingSize = "small",
483
+ secret_ref: str = "emb:openai:api_key",
484
+ validate: bool = False,
485
+ dim: Optional[int] = None,
486
+ metric: Metric = "cosine",
487
+ hybrid: bool = False,
488
+ filters: Optional[Mapping[str, Any]] = None,
489
+ metadata_schema: Optional[Mapping[str, Any]] = None,
490
+ tuning: Optional[Mapping[str, Any]] = None,
491
+ ) -> Dataset:
492
+ """Store an OpenAI org secret, then create an OpenAI-backed dataset."""
493
+ embedding_config = openai(size)
494
+ embedding_config["secret_ref"] = secret_ref
495
+ return self.create(
496
+ name=name,
497
+ dim=dim,
498
+ metric=metric,
499
+ embedding=embedding_config,
500
+ hybrid=hybrid,
501
+ filters=filters,
502
+ metadata_schema=metadata_schema,
503
+ tuning=tuning,
504
+ openai_api_key=api_key,
505
+ openai_secret_ref=secret_ref,
506
+ validate_openai_key=validate,
507
+ )
508
+
509
+ def patch_metadata_schema(self, dataset_id: str, schema: MetadataSchema) -> Dataset:
510
+ """Add or update schema fields without removing omitted fields."""
511
+ body = {"schema": [dict(field) for field in schema], "mode": "merge"}
512
+ return self._to_dataset(
513
+ self._transport.request("PATCH", f"/datasets/{dataset_id}/schema", json_body=body)
514
+ )
515
+
516
+ def replace_metadata_schema(self, dataset_id: str, schema: MetadataSchema) -> Dataset:
517
+ """Replace a dataset's complete typed metadata schema."""
518
+ body = {"schema": [dict(field) for field in schema], "mode": "replace"}
519
+ return self._to_dataset(
520
+ self._transport.request("PATCH", f"/datasets/{dataset_id}/schema", json_body=body)
521
+ )
522
+
523
+ @staticmethod
524
+ def _normalize_metadata_schema(schema: MetadataSchemaInput) -> List[Dict[str, Any]]:
525
+ """Normalize the legacy field-name mapping and the canonical field array."""
526
+ if isinstance(schema, Mapping):
527
+ return [
528
+ {"name": name, **dict(config)}
529
+ for name, config in schema.items()
530
+ ]
531
+ return [dict(field) for field in schema]
532
+
415
533
  def delete(self, dataset_id: str) -> Any:
416
534
  """Delete a dataset and return the API response.
417
535
 
@@ -578,6 +696,23 @@ class DatasetsResource:
578
696
  """Alias for ``insert_vectors``."""
579
697
  return self.insert_vectors(dataset_id, vectors)
580
698
 
699
+ def delete_vectors(
700
+ self,
701
+ dataset_id: str,
702
+ ids: Sequence[VectorId],
703
+ *,
704
+ write_concern: Optional[Mapping[str, Any]] = None,
705
+ ) -> JSON:
706
+ """Delete vectors from a dataset by id."""
707
+ if not ids:
708
+ raise ValueError("ids must contain at least one vector id.")
709
+ body: JSON = {"ids": list(ids)}
710
+ if write_concern is not None:
711
+ body["write_concern"] = dict(write_concern)
712
+ return self._transport.request(
713
+ "DELETE", f"/api/v1/datasets/{dataset_id}/vectors", json_body=body
714
+ )
715
+
581
716
  def embed(
582
717
  self,
583
718
  dataset_id: str,
@@ -657,6 +792,58 @@ class DatasetsResource:
657
792
  return Dataset(self, raw_data)
658
793
 
659
794
 
795
+ class OrgSecretsResource:
796
+ """Organization secret helpers."""
797
+
798
+ def __init__(self, transport: BaseTransport) -> None:
799
+ self._transport = transport
800
+
801
+ def put(
802
+ self,
803
+ name: str,
804
+ value: str,
805
+ *,
806
+ validate: bool = False,
807
+ model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
808
+ ) -> Any:
809
+ """Create or update an organization secret.
810
+
811
+ Stores provider credentials through the generic organization-secret
812
+ REST endpoint. Plaintext is sent once and is never returned by reads.
813
+ """
814
+ if not name:
815
+ raise ValueError("name is required.")
816
+ if not value:
817
+ raise ValueError("value is required.")
818
+ return self._transport.request(
819
+ "PUT",
820
+ f"/org-secrets/{quote(name, safe='')}",
821
+ json_body={"value": value},
822
+ )
823
+
824
+ def update(
825
+ self,
826
+ name: str,
827
+ value: str,
828
+ *,
829
+ validate: bool = False,
830
+ model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
831
+ ) -> Any:
832
+ """Alias for :meth:`put`; org secrets are upserted by name."""
833
+ return self.put(name, value, validate=validate, model=model)
834
+
835
+ def put_openai_api_key(
836
+ self,
837
+ api_key: str,
838
+ *,
839
+ secret_ref: str = "emb:openai:api_key",
840
+ validate: bool = False,
841
+ model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
842
+ ) -> Any:
843
+ """Create or update the OpenAI API key organization secret."""
844
+ return self.put(secret_ref, api_key, validate=validate, model=model)
845
+
846
+
660
847
  class IngestionResource:
661
848
  """Sources, upload sessions, and ingestion jobs."""
662
849
 
@@ -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.1.0"}
160
+ merged = {"X-API-Key": self.api_key, "User-Agent": "vectoramp-python/0.3.0"}
161
161
  if headers:
162
162
  merged.update(headers)
163
163
  return merged
@@ -7,6 +7,18 @@ from typing import Any, Dict, List, Literal, Mapping, Optional, Sequence, TypedD
7
7
  JSON = Dict[str, Any]
8
8
  Metadata = Mapping[str, Any]
9
9
  Metric = Literal["cosine", "dot", "euclidean"]
10
+ MetadataFieldType = Literal["string", "u32", "i32", "i64", "f32", "f64"]
11
+
12
+
13
+ class MetadataSchemaField(TypedDict):
14
+ """A typed metadata field declared on a dataset."""
15
+
16
+ name: str
17
+ type: MetadataFieldType
18
+
19
+
20
+ MetadataSchema = Sequence[MetadataSchemaField]
21
+ MetadataSchemaInput = Union[MetadataSchema, Mapping[str, Mapping[str, Any]]]
10
22
 
11
23
 
12
24
  class EmbeddingConfig(TypedDict):
@@ -52,7 +52,7 @@ def test_dataset_create_forces_sable_and_auth_header() -> None:
52
52
  name="docs",
53
53
  dim=2560,
54
54
  filters={"category": "string"},
55
- metadata_schema={"title": {"type": "string"}},
55
+ metadata_schema=[{"name": "title", "type": "string"}],
56
56
  tuning={"replicas": 1},
57
57
  )
58
58
 
@@ -64,8 +64,10 @@ def test_dataset_create_forces_sable_and_auth_header() -> None:
64
64
  "provider": "vectoramp",
65
65
  "model": "VectorAmp-Embedding-4B",
66
66
  }
67
+ # hybrid is not sent unless explicitly requested.
68
+ assert "hybrid" not in seen["body"]
67
69
  assert seen["body"]["filters"] == {"category": "string"}
68
- assert seen["body"]["metadata_schema"] == {"title": {"type": "string"}}
70
+ assert seen["body"]["schema"] == [{"name": "title", "type": "string"}]
69
71
  assert seen["body"]["tuning"] == {"replicas": 1}
70
72
 
71
73
 
@@ -88,8 +90,47 @@ def test_dataset_create_minimal_name_only_defaults() -> None:
88
90
  "provider": "vectoramp",
89
91
  "model": "VectorAmp-Embedding-4B",
90
92
  }
91
- # hybrid is not sent unless explicitly requested.
92
- assert "hybrid" not in seen["body"]
93
+
94
+
95
+ def test_dataset_create_normalizes_legacy_metadata_schema_mapping() -> None:
96
+ seen = {}
97
+
98
+ def handler(request: httpx.Request) -> httpx.Response:
99
+ seen["body"] = json.loads(request.content)
100
+ return json_response({"id": "ds_legacy"}, 201)
101
+
102
+ client = make_client(handler)
103
+ client.datasets.create(
104
+ name="legacy",
105
+ metadata_schema={"title": {"type": "string"}},
106
+ )
107
+
108
+ assert seen["body"]["schema"] == [{"name": "title", "type": "string"}]
109
+
110
+
111
+ @pytest.mark.parametrize(
112
+ ("method_name", "mode"),
113
+ [("patch_metadata_schema", "merge"), ("replace_metadata_schema", "replace")],
114
+ )
115
+ def test_update_metadata_schema(method_name: str, mode: str) -> None:
116
+ seen = {}
117
+
118
+ def handler(request: httpx.Request) -> httpx.Response:
119
+ seen["method"] = request.method
120
+ seen["url"] = str(request.url)
121
+ seen["body"] = json.loads(request.content)
122
+ return json_response({"id": "ds_1", "schema_version": 2})
123
+
124
+ client = make_client(handler)
125
+ schema = [{"name": "price", "type": "f32"}]
126
+ dataset = getattr(client.datasets, method_name)("ds_1", schema)
127
+
128
+ assert dataset.id == "ds_1"
129
+ assert seen == {
130
+ "method": "PATCH",
131
+ "url": "https://api.test/datasets/ds_1/schema",
132
+ "body": {"schema": schema, "mode": mode},
133
+ }
93
134
 
94
135
 
95
136
  def test_dataset_create_hybrid() -> None:
@@ -133,6 +174,96 @@ def test_dataset_create_openai_helper_infers_dimension() -> None:
133
174
  }
134
175
 
135
176
 
177
+ def test_dataset_create_openai_stores_secret_then_uses_secret_ref() -> None:
178
+ calls: list[tuple[str, str, dict[str, Any]]] = []
179
+
180
+ def handler(request: httpx.Request) -> httpx.Response:
181
+ body = json.loads(request.content) if request.content else {}
182
+ calls.append((request.method, request.url.path, body))
183
+ if request.url.path == "/org-secrets/emb%3Aopenai%3Aapi_key":
184
+ return httpx.Response(204)
185
+ return json_response({"id": "ds_openai", "index_type": "sable"}, 201)
186
+
187
+ client = make_client(handler)
188
+ dataset = client.datasets.create_openai(
189
+ name="openai-docs",
190
+ api_key="sk-test",
191
+ secret_ref="emb:openai:customer-a",
192
+ validate=True,
193
+ )
194
+
195
+ assert dataset.id == "ds_openai"
196
+ assert calls == [
197
+ (
198
+ "PUT",
199
+ "/org-secrets/emb:openai:customer-a",
200
+ {"value": "sk-test"},
201
+ ),
202
+ (
203
+ "POST",
204
+ "/datasets",
205
+ {
206
+ "name": "openai-docs",
207
+ "dim": 1536,
208
+ "metric": "cosine",
209
+ "embedding": {
210
+ "provider": "openai",
211
+ "model": "text-embedding-3-small",
212
+ "secret_ref": "emb:openai:customer-a",
213
+ },
214
+ "index_type": "sable",
215
+ },
216
+ ),
217
+ ]
218
+
219
+
220
+ def test_org_secrets_put_update_openai_key() -> None:
221
+ calls: list[dict[str, Any]] = []
222
+
223
+ def handler(request: httpx.Request) -> httpx.Response:
224
+ calls.append(json.loads(request.content))
225
+ return httpx.Response(204)
226
+
227
+ client = make_client(handler)
228
+ assert client.secrets.put("emb:openai:api_key", "sk-new") is None
229
+ assert client.org_secrets.update("emb:openai:api_key", "sk-rotated") is None
230
+
231
+ assert calls[0]["value"] == "sk-new"
232
+ assert calls[1]["value"] == "sk-rotated"
233
+
234
+
235
+ def test_delete_vectors_payload_and_dataset_helper() -> None:
236
+ calls: list[tuple[str, str, dict[str, Any]]] = []
237
+
238
+ def handler(request: httpx.Request) -> httpx.Response:
239
+ body = json.loads(request.content) if request.content else {}
240
+ calls.append((request.method, request.url.path, body))
241
+ return json_response({"deleted": 2, "ids": body.get("ids", [])})
242
+
243
+ client = make_client(handler)
244
+ result = client.datasets.delete_vectors(
245
+ "ds_1", ["vec-a", 42], write_concern={"wait": True}
246
+ )
247
+ dataset = Dataset(client.datasets, {"id": "ds_1"})
248
+ dataset.delete_vectors(["vec-c"])
249
+
250
+ assert result["deleted"] == 2
251
+ assert calls == [
252
+ (
253
+ "DELETE",
254
+ "/api/v1/datasets/ds_1/vectors",
255
+ {"ids": ["vec-a", 42], "write_concern": {"wait": True}},
256
+ ),
257
+ ("DELETE", "/api/v1/datasets/ds_1/vectors", {"ids": ["vec-c"]}),
258
+ ]
259
+
260
+
261
+ def test_delete_vectors_requires_ids() -> None:
262
+ client = make_client(lambda request: json_response({}))
263
+ with pytest.raises(ValueError):
264
+ client.datasets.delete_vectors("ds_1", [])
265
+
266
+
136
267
  def test_list_get_delete_and_stats() -> None:
137
268
  calls = []
138
269
 
@@ -1,12 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- This project follows semantic versioning.
6
-
7
- ## [0.1.0] - 2026-07-02
8
-
9
- ### Added
10
-
11
- - Initial public-ready package baseline for VectorAmp SDK/CLI migration to GitHub.
12
- - GitHub Actions CI workflow.
File without changes
File without changes
File without changes
File without changes
File without changes