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 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any