schift 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.
- schift/__init__.py +14 -0
- schift/async_client.py +82 -0
- schift/bucket.py +153 -0
- schift/client.py +87 -0
- schift/collection.py +85 -0
- schift/models.py +25 -0
- schift/workflow.py +206 -0
- schift/workflow_models.py +53 -0
- schift-0.1.0.dist-info/METADATA +12 -0
- schift-0.1.0.dist-info/RECORD +11 -0
- schift-0.1.0.dist-info/WHEEL +4 -0
schift/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from schift.client import Client
|
|
2
|
+
from schift.async_client import AsyncClient
|
|
3
|
+
from schift.models import EmbedResponse, SearchResult, ClusterResult, ClassifyResult
|
|
4
|
+
from schift.workflow import Workflow, AsyncWorkflow
|
|
5
|
+
from schift.workflow_models import WorkflowResponse, RunResponse, BlockType, WorkflowTemplate, ValidationResult
|
|
6
|
+
from schift.bucket import Bucket, AsyncBucket, JobInfo, UploadResult, BucketStats
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Client", "AsyncClient",
|
|
9
|
+
"EmbedResponse", "SearchResult", "ClusterResult", "ClassifyResult",
|
|
10
|
+
"Workflow", "AsyncWorkflow",
|
|
11
|
+
"WorkflowResponse", "RunResponse", "BlockType", "WorkflowTemplate", "ValidationResult",
|
|
12
|
+
"Bucket", "AsyncBucket", "JobInfo", "UploadResult", "BucketStats",
|
|
13
|
+
]
|
|
14
|
+
__version__ = "0.1.0"
|
schift/async_client.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os, asyncio
|
|
3
|
+
import httpx
|
|
4
|
+
from schift.collection import AsyncCollection
|
|
5
|
+
from schift.models import EmbedResponse, ClusterResult, ClassifyResult
|
|
6
|
+
from schift.workflow import AsyncWorkflow
|
|
7
|
+
from schift.bucket import AsyncBucket
|
|
8
|
+
|
|
9
|
+
_DEFAULT_BASE_URL = "https://api.schift.io"
|
|
10
|
+
|
|
11
|
+
class AsyncClient:
|
|
12
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None, timeout: float = 60.0):
|
|
13
|
+
self._api_key = api_key or os.environ.get("SCHIFT_API_KEY", "")
|
|
14
|
+
if not self._api_key:
|
|
15
|
+
raise ValueError("api_key required (or set SCHIFT_API_KEY env var)")
|
|
16
|
+
self._base_url = base_url or os.environ.get("SCHIFT_BASE_URL", _DEFAULT_BASE_URL)
|
|
17
|
+
self._headers = {"Authorization": f"Bearer {self._api_key}"}
|
|
18
|
+
self._http = httpx.AsyncClient(base_url=self._base_url, timeout=timeout)
|
|
19
|
+
|
|
20
|
+
async def _post(self, path: str, body: dict) -> dict:
|
|
21
|
+
for _ in range(3):
|
|
22
|
+
resp = await self._http.post(path, json=body, headers=self._headers)
|
|
23
|
+
if resp.status_code == 429:
|
|
24
|
+
await asyncio.sleep(float(resp.headers.get("retry-after", 1)))
|
|
25
|
+
continue
|
|
26
|
+
resp.raise_for_status()
|
|
27
|
+
return resp.json()
|
|
28
|
+
resp.raise_for_status()
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
async def embed(self, text: str, task: str | None = None, model: str | None = None, dimensions: int | None = None) -> EmbedResponse:
|
|
32
|
+
body: dict = {"text": text}
|
|
33
|
+
if task: body["task_type"] = task
|
|
34
|
+
if model: body["model"] = model
|
|
35
|
+
if dimensions: body["dimensions"] = dimensions
|
|
36
|
+
data = await self._post("/v1/embed", body)
|
|
37
|
+
return EmbedResponse(values=data["embedding"], model=data.get("model", ""), dimensions=data.get("dimensions", len(data["embedding"])))
|
|
38
|
+
|
|
39
|
+
async def embed_batch(self, texts: list[str], task: str | None = None, model: str | None = None, dimensions: int | None = None) -> list[EmbedResponse]:
|
|
40
|
+
body: dict = {"texts": texts}
|
|
41
|
+
if task: body["task_type"] = task
|
|
42
|
+
if model: body["model"] = model
|
|
43
|
+
if dimensions: body["dimensions"] = dimensions
|
|
44
|
+
data = await self._post("/v1/embed/batch", body)
|
|
45
|
+
return [EmbedResponse(values=emb, model=data.get("model", ""), dimensions=data.get("dimensions", len(emb))) for emb in data["embeddings"]]
|
|
46
|
+
|
|
47
|
+
def collection(self, name: str) -> AsyncCollection:
|
|
48
|
+
return AsyncCollection(name, self._http, self._headers)
|
|
49
|
+
|
|
50
|
+
def bucket(self, bucket_id: str) -> AsyncBucket:
|
|
51
|
+
return AsyncBucket(bucket_id, self._http, self._headers)
|
|
52
|
+
|
|
53
|
+
async def create_bucket(self, name: str, model: str | None = None, dimension: int = 1024) -> dict:
|
|
54
|
+
body: dict = {"name": name, "dimension": dimension}
|
|
55
|
+
if model:
|
|
56
|
+
body["model"] = model
|
|
57
|
+
return await self._post("/v1/buckets", body)
|
|
58
|
+
|
|
59
|
+
async def similarity(self, text_a: str, text_b: str, model: str | None = None) -> float:
|
|
60
|
+
body: dict = {"text_a": text_a, "text_b": text_b}
|
|
61
|
+
if model: body["model"] = model
|
|
62
|
+
return (await self._post("/v1/similarity", body))["score"]
|
|
63
|
+
|
|
64
|
+
async def cluster(self, texts: list[str], n_clusters: int = 5, model: str | None = None) -> ClusterResult:
|
|
65
|
+
body: dict = {"texts": texts, "n_clusters": n_clusters}
|
|
66
|
+
if model: body["model"] = model
|
|
67
|
+
return ClusterResult(**(await self._post("/v1/cluster", body)))
|
|
68
|
+
|
|
69
|
+
async def classify(self, text: str, labels: list[str], model: str | None = None) -> ClassifyResult:
|
|
70
|
+
body: dict = {"text": text, "labels": labels}
|
|
71
|
+
if model: body["model"] = model
|
|
72
|
+
return ClassifyResult(**(await self._post("/v1/classify", body)))
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def workflow(self) -> AsyncWorkflow:
|
|
76
|
+
if not hasattr(self, "_workflow"):
|
|
77
|
+
self._workflow = AsyncWorkflow(self._http, self._headers)
|
|
78
|
+
return self._workflow
|
|
79
|
+
|
|
80
|
+
async def close(self): await self._http.aclose()
|
|
81
|
+
async def __aenter__(self): return self
|
|
82
|
+
async def __aexit__(self, *a): await self.close()
|
schift/bucket.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Bucket operations for Schift SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class JobInfo:
|
|
11
|
+
job_id: str
|
|
12
|
+
file_name: str
|
|
13
|
+
status: str
|
|
14
|
+
estimated_cost: float = 0
|
|
15
|
+
actual_cost: float | None = None
|
|
16
|
+
chunks_count: int | None = None
|
|
17
|
+
error_message: str | None = None
|
|
18
|
+
created_at: str = ""
|
|
19
|
+
completed_at: str | None = None
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BucketStats:
|
|
23
|
+
vector_count: int
|
|
24
|
+
file_count: int
|
|
25
|
+
total_cost: float
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class UploadResult:
|
|
29
|
+
jobs: list[JobInfo]
|
|
30
|
+
total_estimated_cost: float
|
|
31
|
+
credit_balance_after_hold: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Bucket:
|
|
35
|
+
"""Sync bucket operations."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, bucket_id: str, http: httpx.Client, headers: dict):
|
|
38
|
+
self._id = bucket_id
|
|
39
|
+
self._http = http
|
|
40
|
+
self._headers = headers
|
|
41
|
+
|
|
42
|
+
def upload(self, files: list[tuple[str, bytes]], task: str = "retrieval_document") -> UploadResult:
|
|
43
|
+
"""Upload files to bucket. files = [(filename, bytes), ...]"""
|
|
44
|
+
multipart = [("files", (name, data)) for name, data in files]
|
|
45
|
+
resp = self._http.post(
|
|
46
|
+
f"/v1/buckets/{self._id}/upload",
|
|
47
|
+
files=multipart,
|
|
48
|
+
headers=self._headers,
|
|
49
|
+
)
|
|
50
|
+
resp.raise_for_status()
|
|
51
|
+
d = resp.json()
|
|
52
|
+
return UploadResult(
|
|
53
|
+
jobs=[JobInfo(**j) for j in d.get("jobs", [])],
|
|
54
|
+
total_estimated_cost=d.get("total_estimated_cost", 0),
|
|
55
|
+
credit_balance_after_hold=d.get("credit_balance_after_hold", 0),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def upload_file(self, filepath: str, task: str = "retrieval_document") -> UploadResult:
|
|
59
|
+
"""Upload a single file by path."""
|
|
60
|
+
import os
|
|
61
|
+
name = os.path.basename(filepath)
|
|
62
|
+
with open(filepath, "rb") as f:
|
|
63
|
+
data = f.read()
|
|
64
|
+
return self.upload([(name, data)], task=task)
|
|
65
|
+
|
|
66
|
+
def jobs(self, status: str | None = None) -> list[JobInfo]:
|
|
67
|
+
"""List processing jobs for this bucket."""
|
|
68
|
+
params = {"bucket_id": self._id}
|
|
69
|
+
if status:
|
|
70
|
+
params["status"] = status
|
|
71
|
+
resp = self._http.get("/v1/jobs", params=params, headers=self._headers)
|
|
72
|
+
resp.raise_for_status()
|
|
73
|
+
return [JobInfo(**{k: j.get(k) for k in JobInfo.__dataclass_fields__}) for j in resp.json()]
|
|
74
|
+
|
|
75
|
+
def job(self, job_id: str) -> JobInfo:
|
|
76
|
+
"""Get specific job status."""
|
|
77
|
+
resp = self._http.get(f"/v1/jobs/{job_id}", headers=self._headers)
|
|
78
|
+
resp.raise_for_status()
|
|
79
|
+
j = resp.json()
|
|
80
|
+
return JobInfo(**{k: j.get(k) for k in JobInfo.__dataclass_fields__})
|
|
81
|
+
|
|
82
|
+
def cancel_job(self, job_id: str) -> dict:
|
|
83
|
+
"""Cancel a queued job."""
|
|
84
|
+
resp = self._http.post(f"/v1/jobs/{job_id}/cancel", json={}, headers=self._headers)
|
|
85
|
+
resp.raise_for_status()
|
|
86
|
+
return resp.json()
|
|
87
|
+
|
|
88
|
+
def search(self, query: str, top_k: int = 10, task: str = "retrieval_query",
|
|
89
|
+
filter: dict | None = None) -> list[dict]:
|
|
90
|
+
"""Search within bucket."""
|
|
91
|
+
body = {"query": query, "top_k": top_k, "task": task}
|
|
92
|
+
if filter:
|
|
93
|
+
body["filter"] = filter
|
|
94
|
+
resp = self._http.post(f"/v1/buckets/{self._id}/search", json=body, headers=self._headers)
|
|
95
|
+
resp.raise_for_status()
|
|
96
|
+
return resp.json().get("results", [])
|
|
97
|
+
|
|
98
|
+
def stats(self) -> BucketStats:
|
|
99
|
+
"""Get bucket stats."""
|
|
100
|
+
resp = self._http.get(f"/v1/collections/{self._id}/stats", headers=self._headers)
|
|
101
|
+
resp.raise_for_status()
|
|
102
|
+
d = resp.json()
|
|
103
|
+
return BucketStats(
|
|
104
|
+
vector_count=d.get("vector_count", 0),
|
|
105
|
+
file_count=d.get("file_count", 0),
|
|
106
|
+
total_cost=d.get("total_cost", 0),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def delete(self) -> None:
|
|
110
|
+
"""Delete the bucket and all its data."""
|
|
111
|
+
resp = self._http.delete(f"/v1/buckets/{self._id}", headers=self._headers)
|
|
112
|
+
resp.raise_for_status()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AsyncBucket:
|
|
116
|
+
"""Async bucket operations."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, bucket_id: str, http: httpx.AsyncClient, headers: dict):
|
|
119
|
+
self._id = bucket_id
|
|
120
|
+
self._http = http
|
|
121
|
+
self._headers = headers
|
|
122
|
+
|
|
123
|
+
async def upload(self, files: list[tuple[str, bytes]], task: str = "retrieval_document") -> UploadResult:
|
|
124
|
+
multipart = [("files", (name, data)) for name, data in files]
|
|
125
|
+
resp = await self._http.post(f"/v1/buckets/{self._id}/upload", files=multipart, headers=self._headers)
|
|
126
|
+
resp.raise_for_status()
|
|
127
|
+
d = resp.json()
|
|
128
|
+
return UploadResult(
|
|
129
|
+
jobs=[JobInfo(**j) for j in d.get("jobs", [])],
|
|
130
|
+
total_estimated_cost=d.get("total_estimated_cost", 0),
|
|
131
|
+
credit_balance_after_hold=d.get("credit_balance_after_hold", 0),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def jobs(self, status: str | None = None) -> list[JobInfo]:
|
|
135
|
+
params = {"bucket_id": self._id}
|
|
136
|
+
if status:
|
|
137
|
+
params["status"] = status
|
|
138
|
+
resp = await self._http.get("/v1/jobs", params=params, headers=self._headers)
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
return [JobInfo(**{k: j.get(k) for k in JobInfo.__dataclass_fields__}) for j in resp.json()]
|
|
141
|
+
|
|
142
|
+
async def search(self, query: str, top_k: int = 10, task: str = "retrieval_query",
|
|
143
|
+
filter: dict | None = None) -> list[dict]:
|
|
144
|
+
body = {"query": query, "top_k": top_k, "task": task}
|
|
145
|
+
if filter:
|
|
146
|
+
body["filter"] = filter
|
|
147
|
+
resp = await self._http.post(f"/v1/buckets/{self._id}/search", json=body, headers=self._headers)
|
|
148
|
+
resp.raise_for_status()
|
|
149
|
+
return resp.json().get("results", [])
|
|
150
|
+
|
|
151
|
+
async def delete(self) -> None:
|
|
152
|
+
resp = await self._http.delete(f"/v1/buckets/{self._id}", headers=self._headers)
|
|
153
|
+
resp.raise_for_status()
|
schift/client.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os, time
|
|
3
|
+
import httpx
|
|
4
|
+
from schift.collection import Collection
|
|
5
|
+
from schift.models import EmbedResponse, ClusterResult, ClassifyResult
|
|
6
|
+
from schift.workflow import Workflow
|
|
7
|
+
from schift.bucket import Bucket
|
|
8
|
+
|
|
9
|
+
_DEFAULT_BASE_URL = "https://api.schift.io"
|
|
10
|
+
|
|
11
|
+
class Client:
|
|
12
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None, timeout: float = 60.0):
|
|
13
|
+
self._api_key = api_key or os.environ.get("SCHIFT_API_KEY", "")
|
|
14
|
+
if not self._api_key:
|
|
15
|
+
raise ValueError("api_key required (or set SCHIFT_API_KEY env var)")
|
|
16
|
+
self._base_url = base_url or os.environ.get("SCHIFT_BASE_URL", _DEFAULT_BASE_URL)
|
|
17
|
+
self._headers = {"Authorization": f"Bearer {self._api_key}"}
|
|
18
|
+
self._http = httpx.Client(base_url=self._base_url, timeout=timeout)
|
|
19
|
+
|
|
20
|
+
def _post(self, path: str, body: dict) -> dict:
|
|
21
|
+
for _ in range(3):
|
|
22
|
+
resp = self._http.post(path, json=body, headers=self._headers)
|
|
23
|
+
if resp.status_code == 429:
|
|
24
|
+
time.sleep(float(resp.headers.get("retry-after", 1)))
|
|
25
|
+
continue
|
|
26
|
+
resp.raise_for_status()
|
|
27
|
+
return resp.json()
|
|
28
|
+
resp.raise_for_status()
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
def embed(self, text: str, task: str | None = None, model: str | None = None, dimensions: int | None = None) -> EmbedResponse:
|
|
32
|
+
body: dict = {"text": text}
|
|
33
|
+
if task: body["task_type"] = task
|
|
34
|
+
if model: body["model"] = model
|
|
35
|
+
if dimensions: body["dimensions"] = dimensions
|
|
36
|
+
data = self._post("/v1/embed", body)
|
|
37
|
+
return EmbedResponse(values=data["embedding"], model=data.get("model", ""), dimensions=data.get("dimensions", len(data["embedding"])))
|
|
38
|
+
|
|
39
|
+
def embed_batch(self, texts: list[str], task: str | None = None, model: str | None = None, dimensions: int | None = None) -> list[EmbedResponse]:
|
|
40
|
+
body: dict = {"texts": texts}
|
|
41
|
+
if task: body["task_type"] = task
|
|
42
|
+
if model: body["model"] = model
|
|
43
|
+
if dimensions: body["dimensions"] = dimensions
|
|
44
|
+
data = self._post("/v1/embed/batch", body)
|
|
45
|
+
return [EmbedResponse(values=emb, model=data.get("model", ""), dimensions=data.get("dimensions", len(emb))) for emb in data["embeddings"]]
|
|
46
|
+
|
|
47
|
+
def collection(self, name: str) -> Collection:
|
|
48
|
+
return Collection(name, self._http, self._headers)
|
|
49
|
+
|
|
50
|
+
def bucket(self, bucket_id: str) -> Bucket:
|
|
51
|
+
return Bucket(bucket_id, self._http, self._headers)
|
|
52
|
+
|
|
53
|
+
def create_bucket(self, name: str, model: str | None = None, dimension: int = 1024) -> dict:
|
|
54
|
+
body: dict = {"name": name, "dimension": dimension}
|
|
55
|
+
if model:
|
|
56
|
+
body["model"] = model
|
|
57
|
+
return self._post("/v1/buckets", body)
|
|
58
|
+
|
|
59
|
+
def list_buckets(self) -> list[dict]:
|
|
60
|
+
resp = self._http.get("/v1/buckets", headers=self._headers)
|
|
61
|
+
resp.raise_for_status()
|
|
62
|
+
return resp.json()
|
|
63
|
+
|
|
64
|
+
def similarity(self, text_a: str, text_b: str, model: str | None = None) -> float:
|
|
65
|
+
body: dict = {"text_a": text_a, "text_b": text_b}
|
|
66
|
+
if model: body["model"] = model
|
|
67
|
+
return self._post("/v1/similarity", body)["score"]
|
|
68
|
+
|
|
69
|
+
def cluster(self, texts: list[str], n_clusters: int = 5, model: str | None = None) -> ClusterResult:
|
|
70
|
+
body: dict = {"texts": texts, "n_clusters": n_clusters}
|
|
71
|
+
if model: body["model"] = model
|
|
72
|
+
return ClusterResult(**self._post("/v1/cluster", body))
|
|
73
|
+
|
|
74
|
+
def classify(self, text: str, labels: list[str], model: str | None = None) -> ClassifyResult:
|
|
75
|
+
body: dict = {"text": text, "labels": labels}
|
|
76
|
+
if model: body["model"] = model
|
|
77
|
+
return ClassifyResult(**self._post("/v1/classify", body))
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def workflow(self) -> Workflow:
|
|
81
|
+
if not hasattr(self, "_workflow"):
|
|
82
|
+
self._workflow = Workflow(self._http, self._headers)
|
|
83
|
+
return self._workflow
|
|
84
|
+
|
|
85
|
+
def close(self): self._http.close()
|
|
86
|
+
def __enter__(self): return self
|
|
87
|
+
def __exit__(self, *a): self.close()
|
schift/collection.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from schift.models import SearchResult
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
class Collection:
|
|
9
|
+
def __init__(self, name: str, http: httpx.Client, headers: dict):
|
|
10
|
+
self._name = name
|
|
11
|
+
self._http = http
|
|
12
|
+
self._headers = headers
|
|
13
|
+
|
|
14
|
+
def add(self, documents: list[str], ids: list[str] | None = None,
|
|
15
|
+
metadata: list[dict] | None = None, task: str | None = None,
|
|
16
|
+
model: str | None = None) -> dict:
|
|
17
|
+
body: dict = {"documents": documents}
|
|
18
|
+
if ids: body["ids"] = ids
|
|
19
|
+
if metadata: body["metadata"] = metadata
|
|
20
|
+
if task: body["task"] = task
|
|
21
|
+
if model: body["model"] = model
|
|
22
|
+
resp = self._http.post(f"/v1/collections/{self._name}/add", json=body, headers=self._headers)
|
|
23
|
+
resp.raise_for_status()
|
|
24
|
+
return resp.json()
|
|
25
|
+
|
|
26
|
+
def search(self, query: str, task: str | None = None, top_k: int = 10,
|
|
27
|
+
filter: dict | None = None, model: str | None = None) -> list[SearchResult]:
|
|
28
|
+
body: dict = {"query": query, "top_k": top_k}
|
|
29
|
+
if task: body["task"] = task
|
|
30
|
+
if filter: body["filter"] = filter
|
|
31
|
+
if model: body["model"] = model
|
|
32
|
+
resp = self._http.post(f"/v1/collections/{self._name}/search", json=body, headers=self._headers)
|
|
33
|
+
resp.raise_for_status()
|
|
34
|
+
return [SearchResult(**r) for r in resp.json().get("results", [])]
|
|
35
|
+
|
|
36
|
+
def upsert(self, vectors: list[dict]) -> dict:
|
|
37
|
+
resp = self._http.post(f"/v1/collections/{self._name}/vectors", json={"vectors": vectors}, headers=self._headers)
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
return resp.json()
|
|
40
|
+
|
|
41
|
+
def search_by_vector(self, query_vector: list[float], top_k: int = 10) -> list[SearchResult]:
|
|
42
|
+
resp = self._http.post("/v1/query", json={"query_vector": query_vector, "collection": self._name, "top_k": top_k}, headers=self._headers)
|
|
43
|
+
resp.raise_for_status()
|
|
44
|
+
data = resp.json()
|
|
45
|
+
return [SearchResult(**r) for r in (data.get("results", data) if isinstance(data, dict) else data)]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AsyncCollection:
|
|
49
|
+
def __init__(self, name: str, http: httpx.AsyncClient, headers: dict):
|
|
50
|
+
self._name = name
|
|
51
|
+
self._http = http
|
|
52
|
+
self._headers = headers
|
|
53
|
+
|
|
54
|
+
async def add(self, documents: list[str], ids: list[str] | None = None,
|
|
55
|
+
metadata: list[dict] | None = None, task: str | None = None,
|
|
56
|
+
model: str | None = None) -> dict:
|
|
57
|
+
body: dict = {"documents": documents}
|
|
58
|
+
if ids: body["ids"] = ids
|
|
59
|
+
if metadata: body["metadata"] = metadata
|
|
60
|
+
if task: body["task"] = task
|
|
61
|
+
if model: body["model"] = model
|
|
62
|
+
resp = await self._http.post(f"/v1/collections/{self._name}/add", json=body, headers=self._headers)
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
return resp.json()
|
|
65
|
+
|
|
66
|
+
async def search(self, query: str, task: str | None = None, top_k: int = 10,
|
|
67
|
+
filter: dict | None = None, model: str | None = None) -> list[SearchResult]:
|
|
68
|
+
body: dict = {"query": query, "top_k": top_k}
|
|
69
|
+
if task: body["task"] = task
|
|
70
|
+
if filter: body["filter"] = filter
|
|
71
|
+
if model: body["model"] = model
|
|
72
|
+
resp = await self._http.post(f"/v1/collections/{self._name}/search", json=body, headers=self._headers)
|
|
73
|
+
resp.raise_for_status()
|
|
74
|
+
return [SearchResult(**r) for r in resp.json().get("results", [])]
|
|
75
|
+
|
|
76
|
+
async def upsert(self, vectors: list[dict]) -> dict:
|
|
77
|
+
resp = await self._http.post(f"/v1/collections/{self._name}/vectors", json={"vectors": vectors}, headers=self._headers)
|
|
78
|
+
resp.raise_for_status()
|
|
79
|
+
return resp.json()
|
|
80
|
+
|
|
81
|
+
async def search_by_vector(self, query_vector: list[float], top_k: int = 10) -> list[SearchResult]:
|
|
82
|
+
resp = await self._http.post("/v1/query", json={"query_vector": query_vector, "collection": self._name, "top_k": top_k}, headers=self._headers)
|
|
83
|
+
resp.raise_for_status()
|
|
84
|
+
data = resp.json()
|
|
85
|
+
return [SearchResult(**r) for r in (data.get("results", data) if isinstance(data, dict) else data)]
|
schift/models.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class EmbedResponse:
|
|
6
|
+
values: list[float]
|
|
7
|
+
model: str
|
|
8
|
+
dimensions: int
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SearchResult:
|
|
12
|
+
id: str
|
|
13
|
+
metadata: dict
|
|
14
|
+
score: float
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ClusterResult:
|
|
18
|
+
labels: list[int]
|
|
19
|
+
centroids: list[list[float]]
|
|
20
|
+
n_clusters: int
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ClassifyResult:
|
|
24
|
+
label: str
|
|
25
|
+
scores: dict[str, float]
|
schift/workflow.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Workflow operations for Schift SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from schift.workflow_models import WorkflowResponse, RunResponse, ValidationResult
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Workflow:
|
|
11
|
+
"""Sync workflow operations."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, http: httpx.Client, headers: dict):
|
|
14
|
+
self._http = http
|
|
15
|
+
self._headers = headers
|
|
16
|
+
|
|
17
|
+
def _post(self, path: str, body: dict | None = None):
|
|
18
|
+
resp = self._http.post(path, json=body or {}, headers=self._headers)
|
|
19
|
+
resp.raise_for_status()
|
|
20
|
+
return resp.json() if resp.content else None
|
|
21
|
+
|
|
22
|
+
def _get(self, path: str):
|
|
23
|
+
resp = self._http.get(path, headers=self._headers)
|
|
24
|
+
resp.raise_for_status()
|
|
25
|
+
return resp.json()
|
|
26
|
+
|
|
27
|
+
def _patch(self, path: str, body: dict):
|
|
28
|
+
resp = self._http.patch(path, json=body, headers=self._headers)
|
|
29
|
+
resp.raise_for_status()
|
|
30
|
+
return resp.json()
|
|
31
|
+
|
|
32
|
+
def _delete(self, path: str):
|
|
33
|
+
resp = self._http.delete(path, headers=self._headers)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
|
|
36
|
+
def _parse_workflow(self, data: dict) -> WorkflowResponse:
|
|
37
|
+
return WorkflowResponse(
|
|
38
|
+
id=data.get("id", ""),
|
|
39
|
+
name=data.get("name", ""),
|
|
40
|
+
status=data.get("status", ""),
|
|
41
|
+
description=data.get("description", ""),
|
|
42
|
+
graph=data.get("graph", {}),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _parse_run(self, data: dict) -> RunResponse:
|
|
46
|
+
return RunResponse(
|
|
47
|
+
run_id=data.get("run_id", ""),
|
|
48
|
+
status=data.get("status", ""),
|
|
49
|
+
outputs=data.get("outputs") or {},
|
|
50
|
+
error=data.get("error"),
|
|
51
|
+
block_states=data.get("block_states") or {},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def create(self, name: str, template: str | None = None,
|
|
55
|
+
description: str = "") -> WorkflowResponse:
|
|
56
|
+
body: dict = {"name": name, "description": description}
|
|
57
|
+
if template:
|
|
58
|
+
body["template"] = template
|
|
59
|
+
data = self._post("/v1/workflows", body)
|
|
60
|
+
return self._parse_workflow(data)
|
|
61
|
+
|
|
62
|
+
def get(self, workflow_id: str) -> WorkflowResponse:
|
|
63
|
+
data = self._get(f"/v1/workflows/{workflow_id}")
|
|
64
|
+
return self._parse_workflow(data)
|
|
65
|
+
|
|
66
|
+
def list(self) -> list[WorkflowResponse]:
|
|
67
|
+
data = self._get("/v1/workflows")
|
|
68
|
+
return [self._parse_workflow(d) for d in data]
|
|
69
|
+
|
|
70
|
+
def update(self, workflow_id: str, **kwargs) -> WorkflowResponse:
|
|
71
|
+
data = self._patch(f"/v1/workflows/{workflow_id}", kwargs)
|
|
72
|
+
return self._parse_workflow(data)
|
|
73
|
+
|
|
74
|
+
def delete(self, workflow_id: str) -> None:
|
|
75
|
+
self._delete(f"/v1/workflows/{workflow_id}")
|
|
76
|
+
|
|
77
|
+
def run(self, workflow_id: str, inputs: dict | None = None) -> RunResponse:
|
|
78
|
+
data = self._post(f"/v1/workflows/{workflow_id}/run", {"inputs": inputs or {}})
|
|
79
|
+
return self._parse_run(data)
|
|
80
|
+
|
|
81
|
+
def runs(self, workflow_id: str) -> list[RunResponse]:
|
|
82
|
+
data = self._get(f"/v1/workflows/{workflow_id}/runs")
|
|
83
|
+
return [self._parse_run(d) for d in data]
|
|
84
|
+
|
|
85
|
+
def validate(self, workflow_id: str) -> ValidationResult:
|
|
86
|
+
data = self._post(f"/v1/workflows/{workflow_id}/validate")
|
|
87
|
+
return ValidationResult(valid=data.get("valid", False), errors=data.get("errors", []))
|
|
88
|
+
|
|
89
|
+
def add_block(self, workflow_id: str, block_type: str, title: str = "",
|
|
90
|
+
config: dict | None = None) -> dict:
|
|
91
|
+
return self._post(f"/v1/workflows/{workflow_id}/blocks",
|
|
92
|
+
{"type": block_type, "title": title, "config": config or {}})
|
|
93
|
+
|
|
94
|
+
def add_edge(self, workflow_id: str, source: str, target: str) -> dict:
|
|
95
|
+
return self._post(f"/v1/workflows/{workflow_id}/edges",
|
|
96
|
+
{"source": source, "target": target})
|
|
97
|
+
|
|
98
|
+
# Template shortcuts
|
|
99
|
+
def create_rag(self, name: str = "RAG Pipeline") -> WorkflowResponse:
|
|
100
|
+
return self.create(name, template="BASIC_RAG")
|
|
101
|
+
|
|
102
|
+
def create_doc_qa(self, name: str = "Document QA") -> WorkflowResponse:
|
|
103
|
+
return self.create(name, template="DOCUMENT_QA")
|
|
104
|
+
|
|
105
|
+
def create_chat(self, name: str = "Chat RAG") -> WorkflowResponse:
|
|
106
|
+
return self.create(name, template="CHAT_RAG")
|
|
107
|
+
|
|
108
|
+
def create_ocr_ingest(self, name: str = "OCR Ingest") -> WorkflowResponse:
|
|
109
|
+
return self.create(name, template="IMAGE_OCR_INGEST")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AsyncWorkflow:
|
|
113
|
+
"""Async workflow operations."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, http: httpx.AsyncClient, headers: dict):
|
|
116
|
+
self._http = http
|
|
117
|
+
self._headers = headers
|
|
118
|
+
|
|
119
|
+
async def _post(self, path: str, body: dict | None = None):
|
|
120
|
+
resp = await self._http.post(path, json=body or {}, headers=self._headers)
|
|
121
|
+
resp.raise_for_status()
|
|
122
|
+
return resp.json() if resp.content else None
|
|
123
|
+
|
|
124
|
+
async def _get(self, path: str):
|
|
125
|
+
resp = await self._http.get(path, headers=self._headers)
|
|
126
|
+
resp.raise_for_status()
|
|
127
|
+
return resp.json()
|
|
128
|
+
|
|
129
|
+
def _parse_workflow(self, data: dict) -> WorkflowResponse:
|
|
130
|
+
return WorkflowResponse(
|
|
131
|
+
id=data.get("id", ""),
|
|
132
|
+
name=data.get("name", ""),
|
|
133
|
+
status=data.get("status", ""),
|
|
134
|
+
description=data.get("description", ""),
|
|
135
|
+
graph=data.get("graph", {}),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _parse_run(self, data: dict) -> RunResponse:
|
|
139
|
+
return RunResponse(
|
|
140
|
+
run_id=data.get("run_id", ""),
|
|
141
|
+
status=data.get("status", ""),
|
|
142
|
+
outputs=data.get("outputs") or {},
|
|
143
|
+
error=data.get("error"),
|
|
144
|
+
block_states=data.get("block_states") or {},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def create(self, name: str, template: str | None = None,
|
|
148
|
+
description: str = "") -> WorkflowResponse:
|
|
149
|
+
body: dict = {"name": name, "description": description}
|
|
150
|
+
if template:
|
|
151
|
+
body["template"] = template
|
|
152
|
+
data = await self._post("/v1/workflows", body)
|
|
153
|
+
return self._parse_workflow(data)
|
|
154
|
+
|
|
155
|
+
async def get(self, workflow_id: str) -> WorkflowResponse:
|
|
156
|
+
data = await self._get(f"/v1/workflows/{workflow_id}")
|
|
157
|
+
return self._parse_workflow(data)
|
|
158
|
+
|
|
159
|
+
async def list(self) -> list[WorkflowResponse]:
|
|
160
|
+
data = await self._get("/v1/workflows")
|
|
161
|
+
return [self._parse_workflow(d) for d in data]
|
|
162
|
+
|
|
163
|
+
async def update(self, workflow_id: str, **kwargs) -> WorkflowResponse:
|
|
164
|
+
resp = await self._http.patch(f"/v1/workflows/{workflow_id}",
|
|
165
|
+
json=kwargs, headers=self._headers)
|
|
166
|
+
resp.raise_for_status()
|
|
167
|
+
return self._parse_workflow(resp.json())
|
|
168
|
+
|
|
169
|
+
async def delete(self, workflow_id: str) -> None:
|
|
170
|
+
resp = await self._http.delete(f"/v1/workflows/{workflow_id}",
|
|
171
|
+
headers=self._headers)
|
|
172
|
+
resp.raise_for_status()
|
|
173
|
+
|
|
174
|
+
async def run(self, workflow_id: str, inputs: dict | None = None) -> RunResponse:
|
|
175
|
+
data = await self._post(f"/v1/workflows/{workflow_id}/run", {"inputs": inputs or {}})
|
|
176
|
+
return self._parse_run(data)
|
|
177
|
+
|
|
178
|
+
async def runs(self, workflow_id: str) -> list[RunResponse]:
|
|
179
|
+
data = await self._get(f"/v1/workflows/{workflow_id}/runs")
|
|
180
|
+
return [self._parse_run(d) for d in data]
|
|
181
|
+
|
|
182
|
+
async def validate(self, workflow_id: str) -> ValidationResult:
|
|
183
|
+
data = await self._post(f"/v1/workflows/{workflow_id}/validate")
|
|
184
|
+
return ValidationResult(valid=data.get("valid", False), errors=data.get("errors", []))
|
|
185
|
+
|
|
186
|
+
async def add_block(self, workflow_id: str, block_type: str, title: str = "",
|
|
187
|
+
config: dict | None = None) -> dict:
|
|
188
|
+
return await self._post(f"/v1/workflows/{workflow_id}/blocks",
|
|
189
|
+
{"type": block_type, "title": title, "config": config or {}})
|
|
190
|
+
|
|
191
|
+
async def add_edge(self, workflow_id: str, source: str, target: str) -> dict:
|
|
192
|
+
return await self._post(f"/v1/workflows/{workflow_id}/edges",
|
|
193
|
+
{"source": source, "target": target})
|
|
194
|
+
|
|
195
|
+
# Template shortcuts
|
|
196
|
+
async def create_rag(self, name: str = "RAG Pipeline") -> WorkflowResponse:
|
|
197
|
+
return await self.create(name, template="BASIC_RAG")
|
|
198
|
+
|
|
199
|
+
async def create_doc_qa(self, name: str = "Document QA") -> WorkflowResponse:
|
|
200
|
+
return await self.create(name, template="DOCUMENT_QA")
|
|
201
|
+
|
|
202
|
+
async def create_chat(self, name: str = "Chat RAG") -> WorkflowResponse:
|
|
203
|
+
return await self.create(name, template="CHAT_RAG")
|
|
204
|
+
|
|
205
|
+
async def create_ocr_ingest(self, name: str = "OCR Ingest") -> WorkflowResponse:
|
|
206
|
+
return await self.create(name, template="IMAGE_OCR_INGEST")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Workflow response models."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BlockType(str, Enum):
|
|
8
|
+
START = "START"
|
|
9
|
+
END = "END"
|
|
10
|
+
DOCUMENT_LOADER = "DOCUMENT_LOADER"
|
|
11
|
+
DOCUMENT_PARSER = "DOCUMENT_PARSER"
|
|
12
|
+
CHUNKER = "CHUNKER"
|
|
13
|
+
EMBEDDER = "EMBEDDER"
|
|
14
|
+
VECTOR_STORE = "VECTOR_STORE"
|
|
15
|
+
RETRIEVER = "RETRIEVER"
|
|
16
|
+
RERANKER = "RERANKER"
|
|
17
|
+
LLM = "LLM"
|
|
18
|
+
PROMPT_TEMPLATE = "PROMPT_TEMPLATE"
|
|
19
|
+
CONDITION = "CONDITION"
|
|
20
|
+
CODE = "CODE"
|
|
21
|
+
ANSWER = "ANSWER"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkflowTemplate(str, Enum):
|
|
25
|
+
BASIC_RAG = "BASIC_RAG"
|
|
26
|
+
DOCUMENT_QA = "DOCUMENT_QA"
|
|
27
|
+
CONVERSATIONAL_RAG = "CONVERSATIONAL_RAG"
|
|
28
|
+
IMAGE_OCR_INGEST = "IMAGE_OCR_INGEST"
|
|
29
|
+
CHAT_RAG = "CHAT_RAG"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class WorkflowResponse:
|
|
34
|
+
id: str
|
|
35
|
+
name: str
|
|
36
|
+
status: str
|
|
37
|
+
description: str = ""
|
|
38
|
+
graph: dict = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RunResponse:
|
|
43
|
+
run_id: str
|
|
44
|
+
status: str
|
|
45
|
+
outputs: dict = field(default_factory=dict)
|
|
46
|
+
error: str | None = None
|
|
47
|
+
block_states: dict = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ValidationResult:
|
|
52
|
+
valid: bool
|
|
53
|
+
errors: list[str] = field(default_factory=list)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schift
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Schift SDK — Canonical Embedding Space
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Provides-Extra: async
|
|
8
|
+
Requires-Dist: httpx[http2]>=0.27; extra == 'async'
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
schift/__init__.py,sha256=ERGZ9W6p95sVCP-J2l-fa1NvjbL0pVfq9SAR70VoRbU,741
|
|
2
|
+
schift/async_client.py,sha256=NmPgpGiHQzV37YsGniIuNlH-07O1Z9PZJPPCZ9IEaIM,4005
|
|
3
|
+
schift/bucket.py,sha256=_uV9W5fMc8JytSzpFPeJcNJ9WGO8_8Xej8yTvtjB240,5744
|
|
4
|
+
schift/client.py,sha256=Ab_3o2Ek_7gw-LRTjf92E9Yh8zJ5IhGT1AzecErCeTY,3985
|
|
5
|
+
schift/collection.py,sha256=aYWUVpjcGnqj-Kf7jaU75RQwhjVaB3V8GIPkDvIi300,4080
|
|
6
|
+
schift/models.py,sha256=RVRdNpdOerJJ2FPYAvA0CCfYj42XXmcKOd4e1zM2QsM,427
|
|
7
|
+
schift/workflow.py,sha256=vxMN9extCUuVX86ewwt5nMg239xhHo2GfY7uOcpkWzw,8478
|
|
8
|
+
schift/workflow_models.py,sha256=VOq_ixu6odvHGt_zh1OFjwYQRv794IBaREZQF85Vk-Y,1199
|
|
9
|
+
schift-0.1.0.dist-info/METADATA,sha256=4LIgnG3ksqwLPo_Crf-P-rGLIt2c5IKaQICCc5PjRLk,382
|
|
10
|
+
schift-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
schift-0.1.0.dist-info/RECORD,,
|