schift-cli 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_cli/__init__.py +1 -0
- schift_cli/client.py +119 -0
- schift_cli/commands/__init__.py +0 -0
- schift_cli/commands/auth.py +68 -0
- schift_cli/commands/bench.py +65 -0
- schift_cli/commands/catalog.py +74 -0
- schift_cli/commands/db.py +96 -0
- schift_cli/commands/embed.py +104 -0
- schift_cli/commands/migrate.py +127 -0
- schift_cli/commands/query.py +66 -0
- schift_cli/commands/skill.py +110 -0
- schift_cli/commands/usage.py +50 -0
- schift_cli/config.py +58 -0
- schift_cli/data/schift-best-practices/AGENTS.md +77 -0
- schift_cli/data/schift-best-practices/CLAUDE.md +77 -0
- schift_cli/data/schift-best-practices/SKILL.md +89 -0
- schift_cli/data/schift-best-practices/references/bucket-organization.md +126 -0
- schift_cli/data/schift-best-practices/references/bucket-upload.md +116 -0
- schift_cli/data/schift-best-practices/references/chatbot-widget.md +238 -0
- schift_cli/data/schift-best-practices/references/cost-batching.md +179 -0
- schift_cli/data/schift-best-practices/references/cost-storage-tiers.md +183 -0
- schift_cli/data/schift-best-practices/references/deploy-cloudrun.md +140 -0
- schift_cli/data/schift-best-practices/references/embed-batch-processing.md +86 -0
- schift_cli/data/schift-best-practices/references/embed-error-handling.md +155 -0
- schift_cli/data/schift-best-practices/references/embed-multimodal.md +100 -0
- schift_cli/data/schift-best-practices/references/embed-task-types.md +135 -0
- schift_cli/data/schift-best-practices/references/rag-chunking.md +173 -0
- schift_cli/data/schift-best-practices/references/rag-workflow-builder.md +205 -0
- schift_cli/data/schift-best-practices/references/sdk-async-patterns.md +103 -0
- schift_cli/data/schift-best-practices/references/sdk-auth-patterns.md +76 -0
- schift_cli/data/schift-best-practices/references/search-collection-design.md +229 -0
- schift_cli/data/schift-best-practices/references/search-hybrid.md +163 -0
- schift_cli/data/schift-best-practices/references/search-similarity-tuning.md +134 -0
- schift_cli/display.py +85 -0
- schift_cli/main.py +39 -0
- schift_cli-0.1.0.dist-info/METADATA +12 -0
- schift_cli-0.1.0.dist-info/RECORD +40 -0
- schift_cli-0.1.0.dist-info/WHEEL +5 -0
- schift_cli-0.1.0.dist-info/entry_points.txt +2 -0
- schift_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Specify task_type for optimal embedding quality
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Using the default task_type for every use case can silently degrade retrieval accuracy by 10-30%. Schift optimizes embeddings differently depending on whether the vector will be used for search, classification, clustering, or other tasks.
|
|
5
|
+
tags: [embedding, task-type, retrieval, classification, quality]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Specify task_type for optimal embedding quality
|
|
9
|
+
|
|
10
|
+
Schift supports seven task types that control how the embedding model is fine-tuned for your specific use case. The default (`retrieval`) is a safe starting point, but explicitly setting the correct task type produces better embeddings and downstream results.
|
|
11
|
+
|
|
12
|
+
| task_type | When to use |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `retrieval` | Asymmetric search — query against document corpus |
|
|
15
|
+
| `similarity` | Symmetric similarity — comparing two pieces of text |
|
|
16
|
+
| `classification` | Labeling or categorizing text |
|
|
17
|
+
| `clustering` | Grouping documents by topic |
|
|
18
|
+
| `QA` | Question-answering — question against answer pairs |
|
|
19
|
+
| `fact_verification` | Claim against evidence matching |
|
|
20
|
+
| `code_retrieval` | Natural-language query against code snippets |
|
|
21
|
+
|
|
22
|
+
**Incorrect** — using default `task_type` for every case reduces embedding quality:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# Python - DON'T do this
|
|
26
|
+
from schift import Schift
|
|
27
|
+
|
|
28
|
+
client = Schift(api_key="sch_...")
|
|
29
|
+
|
|
30
|
+
# Indexing documents for search — no task_type set
|
|
31
|
+
doc_embedding = client.embed("The Eiffel Tower is in Paris.")
|
|
32
|
+
|
|
33
|
+
# Classifying customer feedback — no task_type set
|
|
34
|
+
label_embedding = client.embed("This product is terrible!")
|
|
35
|
+
|
|
36
|
+
# Searching code — no task_type set
|
|
37
|
+
code_query = client.embed("function that sorts a list")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// TypeScript - DON'T do this
|
|
42
|
+
import { Schift } from '@schift-io/sdk';
|
|
43
|
+
|
|
44
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
45
|
+
|
|
46
|
+
// All three use cases use the same default — suboptimal for each
|
|
47
|
+
const docEmbedding = await client.embed('The Eiffel Tower is in Paris.');
|
|
48
|
+
const labelEmbedding = await client.embed('This product is terrible!');
|
|
49
|
+
const codeQuery = await client.embed('function that sorts a list');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Correct** — match `task_type` to your use case:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Python - DO this
|
|
56
|
+
from schift import Schift
|
|
57
|
+
|
|
58
|
+
client = Schift(api_key="sch_...")
|
|
59
|
+
|
|
60
|
+
# Asymmetric document search: index documents as "retrieval"
|
|
61
|
+
doc_embedding = client.embed(
|
|
62
|
+
"The Eiffel Tower is in Paris.",
|
|
63
|
+
task_type="retrieval",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Classifying customer feedback
|
|
67
|
+
label_embedding = client.embed(
|
|
68
|
+
"This product is terrible!",
|
|
69
|
+
task_type="classification",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Natural-language query against a code repository
|
|
73
|
+
code_query = client.embed(
|
|
74
|
+
"function that sorts a list",
|
|
75
|
+
task_type="code_retrieval",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# QA: embed question against pre-embedded answers
|
|
79
|
+
question_vec = client.embed(
|
|
80
|
+
"What year was the Eiffel Tower built?",
|
|
81
|
+
task_type="QA",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Fact verification: claim against supporting evidence
|
|
85
|
+
claim_vec = client.embed(
|
|
86
|
+
"The Eiffel Tower was built in 1889.",
|
|
87
|
+
task_type="fact_verification",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Batch embed with task_type
|
|
91
|
+
docs = ["Doc A content...", "Doc B content...", "Doc C content..."]
|
|
92
|
+
results = client.embed_batch(docs, task_type="retrieval")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// TypeScript - DO this
|
|
97
|
+
import { Schift } from '@schift-io/sdk';
|
|
98
|
+
|
|
99
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
100
|
+
|
|
101
|
+
// Asymmetric document search
|
|
102
|
+
const docEmbedding = await client.embed(
|
|
103
|
+
'The Eiffel Tower is in Paris.',
|
|
104
|
+
{ taskType: 'retrieval' },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Classification
|
|
108
|
+
const labelEmbedding = await client.embed(
|
|
109
|
+
'This product is terrible!',
|
|
110
|
+
{ taskType: 'classification' },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Code search
|
|
114
|
+
const codeQuery = await client.embed(
|
|
115
|
+
'function that sorts a list',
|
|
116
|
+
{ taskType: 'code_retrieval' },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// QA
|
|
120
|
+
const questionVec = await client.embed(
|
|
121
|
+
'What year was the Eiffel Tower built?',
|
|
122
|
+
{ taskType: 'QA' },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Batch with task type
|
|
126
|
+
const docs = ['Doc A content...', 'Doc B content...', 'Doc C content...'];
|
|
127
|
+
const results = await client.embedBatch(docs, { taskType: 'retrieval' });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Use the same `task_type` when indexing documents and when embedding queries — mixing task types in the same collection degrades search quality. For collections that serve multiple use cases, create separate collections per task type.
|
|
131
|
+
|
|
132
|
+
## Reference
|
|
133
|
+
|
|
134
|
+
- https://docs.schift.io/api/embed#task-type
|
|
135
|
+
- https://docs.schift.io/concepts/task-types
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Let Schift Handle Chunking via Bucket Upload
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Custom chunking loses document structure (headings, tables, lists), produces uneven chunk quality, and requires maintaining your own chunking logic. Schift's bucket upload handles OCR, structure-aware chunking, and embedding automatically.
|
|
5
|
+
tags:
|
|
6
|
+
- rag
|
|
7
|
+
- chunking
|
|
8
|
+
- bucket
|
|
9
|
+
- document-structure
|
|
10
|
+
- preprocessing
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Let Schift Handle Chunking via Bucket Upload
|
|
14
|
+
|
|
15
|
+
Chunking is one of the most impactful and underestimated decisions in a RAG pipeline. Naively splitting text by character count or regex destroys semantic units: headings get separated from their content, table rows become orphaned fragments, and list items lose their parent context. The result is chunks that embed poorly and retrieve incorrectly.
|
|
16
|
+
|
|
17
|
+
Schift's bucket upload pipeline runs document parsing, OCR, and structure-aware chunking on your behalf. It understands heading hierarchies, tables, code blocks, and lists — producing semantically coherent chunks that embed and retrieve at higher quality without any custom code.
|
|
18
|
+
|
|
19
|
+
### Incorrect
|
|
20
|
+
|
|
21
|
+
Custom regex or fixed-size chunking that destroys document structure:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
# Python — naive fixed-size chunking, loses all structure
|
|
25
|
+
import re
|
|
26
|
+
from schift import Schift
|
|
27
|
+
|
|
28
|
+
client = Schift(api_key="sch_...")
|
|
29
|
+
|
|
30
|
+
with open("technical_report.pdf", "rb") as f:
|
|
31
|
+
raw_text = extract_text_from_pdf(f) # flat string, all structure lost
|
|
32
|
+
|
|
33
|
+
# Split every 500 characters regardless of sentence or section boundaries
|
|
34
|
+
chunks = [raw_text[i:i+500] for i in range(0, len(raw_text), 500)]
|
|
35
|
+
|
|
36
|
+
# Problems:
|
|
37
|
+
# - Heading "## Performance Results" ends up in a different chunk than the table below it
|
|
38
|
+
# - Table rows split mid-row: "| 98.2% | la" / "tency 12ms |"
|
|
39
|
+
# - Code blocks broken at arbitrary positions
|
|
40
|
+
# - No metadata: no page number, no section, no source
|
|
41
|
+
|
|
42
|
+
for i, chunk in enumerate(chunks):
|
|
43
|
+
client.embed_and_insert(collection_id, chunk, metadata={"index": i})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// TypeScript — naive regex split, loses paragraph context
|
|
48
|
+
import { Schift } from '@schift-io/sdk';
|
|
49
|
+
|
|
50
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
51
|
+
|
|
52
|
+
const rawText = await extractText('technical_report.pdf'); // flat string
|
|
53
|
+
|
|
54
|
+
// Split on double newlines — misses tables, code blocks, lists
|
|
55
|
+
const chunks = rawText.split(/\n\n+/).filter(c => c.trim().length > 0);
|
|
56
|
+
|
|
57
|
+
// No overlap, no metadata, no awareness of document hierarchy
|
|
58
|
+
for (const chunk of chunks) {
|
|
59
|
+
await client.embedAndInsert(collectionId, chunk);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This approach drops all structural signals that models rely on to understand context. A chunk reading `"98.2% 12ms 4.1GB"` is meaningless without the table header that provides column names.
|
|
64
|
+
|
|
65
|
+
### Correct
|
|
66
|
+
|
|
67
|
+
Upload directly to a Schift bucket and let the pipeline handle chunking:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Python — bucket upload: structure-aware chunking, no manual work
|
|
71
|
+
from schift import Schift
|
|
72
|
+
|
|
73
|
+
client = Schift(api_key="sch_...")
|
|
74
|
+
|
|
75
|
+
# Upload the raw file — Schift handles OCR, parsing, chunking, embedding
|
|
76
|
+
with open("technical_report.pdf", "rb") as f:
|
|
77
|
+
result = client.bucket.upload(
|
|
78
|
+
bucket_id=bucket_id,
|
|
79
|
+
file=f,
|
|
80
|
+
filename="technical_report.pdf",
|
|
81
|
+
metadata={
|
|
82
|
+
"source": "internal-research",
|
|
83
|
+
"version": "2026-Q1",
|
|
84
|
+
"department": "engineering",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
print(result.chunk_count) # e.g. 142 semantically coherent chunks
|
|
89
|
+
print(result.document_id) # reference for deletion or re-indexing
|
|
90
|
+
|
|
91
|
+
# Schift preserves:
|
|
92
|
+
# - Heading hierarchy (H1 > H2 > H3 as metadata fields)
|
|
93
|
+
# - Table structure (header row context attached to each data row chunk)
|
|
94
|
+
# - Code blocks (kept intact, tagged as code)
|
|
95
|
+
# - List items (grouped under their parent heading)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// TypeScript — bucket upload: let Schift do the heavy lifting
|
|
100
|
+
import { Schift } from '@schift-io/sdk';
|
|
101
|
+
import { readFileSync } from 'fs';
|
|
102
|
+
|
|
103
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
104
|
+
|
|
105
|
+
const fileBuffer = readFileSync('technical_report.pdf');
|
|
106
|
+
|
|
107
|
+
const result = await client.bucket.upload(bucketId, fileBuffer, {
|
|
108
|
+
filename: 'technical_report.pdf',
|
|
109
|
+
metadata: {
|
|
110
|
+
source: 'internal-research',
|
|
111
|
+
version: '2026-Q1',
|
|
112
|
+
department: 'engineering',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(result.chunkCount); // semantically coherent chunks
|
|
117
|
+
console.log(result.documentId); // for future reference or deletion
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
If you must chunk manually (e.g., streaming data with no file format), follow these rules:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# Python — manual chunking fallback: overlap + boundaries + metadata
|
|
124
|
+
def chunk_text_safely(text: str, source: str, page: int = 0) -> list[dict]:
|
|
125
|
+
paragraphs = text.split("\n\n")
|
|
126
|
+
chunks = []
|
|
127
|
+
window = []
|
|
128
|
+
window_size = 0
|
|
129
|
+
target_size = 400 # tokens (approximate)
|
|
130
|
+
overlap_size = 60 # 10–20% overlap to preserve cross-boundary context
|
|
131
|
+
|
|
132
|
+
for para in paragraphs:
|
|
133
|
+
words = para.split()
|
|
134
|
+
window.extend(words)
|
|
135
|
+
window_size += len(words)
|
|
136
|
+
|
|
137
|
+
if window_size >= target_size:
|
|
138
|
+
chunk_text = " ".join(window)
|
|
139
|
+
chunks.append({
|
|
140
|
+
"text": chunk_text,
|
|
141
|
+
"metadata": {
|
|
142
|
+
"source": source,
|
|
143
|
+
"page": page,
|
|
144
|
+
"char_count": len(chunk_text),
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
# Retain overlap to preserve context across chunk boundary
|
|
148
|
+
window = window[-overlap_size:]
|
|
149
|
+
window_size = len(window)
|
|
150
|
+
|
|
151
|
+
if window: # flush remainder
|
|
152
|
+
chunks.append({
|
|
153
|
+
"text": " ".join(window),
|
|
154
|
+
"metadata": {"source": source, "page": page}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return chunks
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Manual chunking checklist (if unavoidable):**
|
|
161
|
+
|
|
162
|
+
| Rule | Why |
|
|
163
|
+
|------|-----|
|
|
164
|
+
| 10–20% overlap between chunks | Prevents context loss at boundaries |
|
|
165
|
+
| Respect paragraph/sentence boundaries | Keeps semantic units intact |
|
|
166
|
+
| Include source, page, section in metadata | Enables filtered search and attribution |
|
|
167
|
+
| Target 300–500 tokens per chunk | Balances precision and recall |
|
|
168
|
+
| Never split mid-sentence | Broken sentences embed poorly |
|
|
169
|
+
|
|
170
|
+
## Reference
|
|
171
|
+
|
|
172
|
+
- https://docs.schift.io/buckets/upload
|
|
173
|
+
- https://docs.schift.io/buckets/chunking
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use WorkflowBuilder for Reproducible RAG Pipelines
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Ad-hoc inline RAG code is hard to test, version, or share. WorkflowBuilder defines RAG as a versioned DAG — reproducible across environments, observable via run history, and shareable as templates.
|
|
5
|
+
tags:
|
|
6
|
+
- rag
|
|
7
|
+
- workflow
|
|
8
|
+
- dag
|
|
9
|
+
- workflowbuilder
|
|
10
|
+
- pipeline
|
|
11
|
+
- observability
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Use WorkflowBuilder for Reproducible RAG Pipelines
|
|
15
|
+
|
|
16
|
+
Embedding retrieval + reranking + LLM generation is a multi-step pipeline. When this logic lives as scattered inline code, it becomes impossible to test individual steps in isolation, reproduce a specific run for debugging, or share the pipeline as a reusable template across projects.
|
|
17
|
+
|
|
18
|
+
Schift's workflow engine models RAG pipelines as directed acyclic graphs (DAGs). Each step is a typed node (retrieve, rerank, generate, filter, transform). The `WorkflowBuilder` API in TypeScript (and its Python equivalent) lets you define, version, and register these pipelines as first-class resources. You run them by ID, and every run is logged — input, intermediate results, output — for debugging and evaluation.
|
|
19
|
+
|
|
20
|
+
### Incorrect
|
|
21
|
+
|
|
22
|
+
Inline search + LLM call logic scattered across the application:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// TypeScript — ad-hoc RAG scattered across application code
|
|
26
|
+
import { Schift } from '@schift-io/sdk';
|
|
27
|
+
import OpenAI from 'openai';
|
|
28
|
+
|
|
29
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
30
|
+
const openai = new OpenAI();
|
|
31
|
+
|
|
32
|
+
// This logic is duplicated in routes/chat.ts, routes/search.ts, scripts/batch.ts
|
|
33
|
+
// No versioning, no run history, impossible to A/B test
|
|
34
|
+
async function answerQuestion(question: string) {
|
|
35
|
+
// Step 1: retrieve — hardcoded params, no way to tune per environment
|
|
36
|
+
const results = await client.search(collectionId, question, { topK: 5 });
|
|
37
|
+
|
|
38
|
+
// Step 2: no reranking step — order determined by vector score alone
|
|
39
|
+
const context = results.map(r => r.text).join('\n\n');
|
|
40
|
+
|
|
41
|
+
// Step 3: generate — prompt template hardcoded here, not versioned
|
|
42
|
+
const completion = await openai.chat.completions.create({
|
|
43
|
+
model: 'gpt-4o',
|
|
44
|
+
messages: [
|
|
45
|
+
{ role: 'system', content: 'Answer based on context.' },
|
|
46
|
+
{ role: 'user', content: `Context:\n${context}\n\nQuestion: ${question}` },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return completion.choices[0].message.content;
|
|
51
|
+
// No logging of which chunks were retrieved, no run ID, no eval surface
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# Python — same problem: logic copy-pasted across scripts, no observability
|
|
57
|
+
from schift import Schift
|
|
58
|
+
from openai import OpenAI
|
|
59
|
+
|
|
60
|
+
schift = Schift(api_key="sch_...")
|
|
61
|
+
openai = OpenAI()
|
|
62
|
+
|
|
63
|
+
def answer(question: str) -> str:
|
|
64
|
+
results = schift.search(collection_id, question, top_k=5)
|
|
65
|
+
context = "\n\n".join(r.text for r in results)
|
|
66
|
+
resp = openai.chat.completions.create(
|
|
67
|
+
model="gpt-4o",
|
|
68
|
+
messages=[
|
|
69
|
+
{"role": "system", "content": "Answer based on context."},
|
|
70
|
+
{"role": "user", "content": f"Context:\n{context}\n\nQ: {question}"},
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
return resp.choices[0].message.content
|
|
74
|
+
# No run ID, no intermediate state, cannot replay or debug
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
When this breaks in production, you cannot inspect which chunks were retrieved, which reranking score caused a bad answer, or compare two versions of the pipeline.
|
|
78
|
+
|
|
79
|
+
### Correct
|
|
80
|
+
|
|
81
|
+
Define the pipeline once as a versioned workflow, run it by ID:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// TypeScript — WorkflowBuilder: define once, run anywhere, fully observable
|
|
85
|
+
import { Schift, WorkflowBuilder } from '@schift-io/sdk';
|
|
86
|
+
|
|
87
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
88
|
+
|
|
89
|
+
// Define the DAG — do this once during setup (e.g., in a migration script)
|
|
90
|
+
const graph = new WorkflowBuilder()
|
|
91
|
+
.addNode({
|
|
92
|
+
id: 'retrieve',
|
|
93
|
+
type: 'retrieve',
|
|
94
|
+
config: {
|
|
95
|
+
collectionId: 'col_docs_prod',
|
|
96
|
+
topK: 10,
|
|
97
|
+
scoreThreshold: 0.70,
|
|
98
|
+
taskType: 'qa',
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
.addNode({
|
|
102
|
+
id: 'rerank',
|
|
103
|
+
type: 'rerank',
|
|
104
|
+
config: {
|
|
105
|
+
model: 'schift-reranker-v1',
|
|
106
|
+
topN: 3, // keep only the top 3 after reranking
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
.addNode({
|
|
110
|
+
id: 'generate',
|
|
111
|
+
type: 'generate',
|
|
112
|
+
config: {
|
|
113
|
+
model: 'gpt-4o',
|
|
114
|
+
systemPrompt: 'Answer the question using only the provided context. If unsure, say so.',
|
|
115
|
+
maxTokens: 512,
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
.addEdge('retrieve', 'rerank') // retrieve → rerank
|
|
119
|
+
.addEdge('rerank', 'generate') // rerank → generate
|
|
120
|
+
.build();
|
|
121
|
+
|
|
122
|
+
// Register the workflow — returns a versioned workflow ID
|
|
123
|
+
const workflow = await client.workflow.create('docs-qa-v1', graph);
|
|
124
|
+
console.log(workflow.id); // wf_abc123 — store this in your config
|
|
125
|
+
|
|
126
|
+
// Run the workflow — every run gets a run ID
|
|
127
|
+
const run = await client.workflow.run(workflow.id, {
|
|
128
|
+
query: 'What are the latency benchmarks for the v2 model?',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log(run.runId); // run_xyz789 — for debugging
|
|
132
|
+
console.log(run.output.answer); // final generated answer
|
|
133
|
+
console.log(run.steps.retrieve.chunks); // intermediate: which chunks retrieved
|
|
134
|
+
console.log(run.steps.rerank.chunks); // intermediate: which chunks survived reranking
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# Python — same workflow pattern
|
|
139
|
+
from schift import Schift
|
|
140
|
+
|
|
141
|
+
client = Schift(api_key="sch_...")
|
|
142
|
+
|
|
143
|
+
# Build the graph as a dict (Python SDK uses graph spec format)
|
|
144
|
+
graph = {
|
|
145
|
+
"nodes": [
|
|
146
|
+
{
|
|
147
|
+
"id": "retrieve",
|
|
148
|
+
"type": "retrieve",
|
|
149
|
+
"config": {
|
|
150
|
+
"collection_id": "col_docs_prod",
|
|
151
|
+
"top_k": 10,
|
|
152
|
+
"score_threshold": 0.70,
|
|
153
|
+
"task_type": "qa",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "rerank",
|
|
158
|
+
"type": "rerank",
|
|
159
|
+
"config": {"model": "schift-reranker-v1", "top_n": 3},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"id": "generate",
|
|
163
|
+
"type": "generate",
|
|
164
|
+
"config": {
|
|
165
|
+
"model": "gpt-4o",
|
|
166
|
+
"system_prompt": "Answer using only the provided context.",
|
|
167
|
+
"max_tokens": 512,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
"edges": [
|
|
172
|
+
{"from": "retrieve", "to": "rerank"},
|
|
173
|
+
{"from": "rerank", "to": "generate"},
|
|
174
|
+
],
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Register once
|
|
178
|
+
workflow = client.workflow.create("docs-qa-v1", graph)
|
|
179
|
+
|
|
180
|
+
# Run — returns structured run result with full step history
|
|
181
|
+
run = client.workflow.run(workflow.id, {"query": "What are the latency benchmarks?"})
|
|
182
|
+
|
|
183
|
+
print(run.run_id) # for debugging
|
|
184
|
+
print(run.output["answer"]) # final answer
|
|
185
|
+
print(run.steps["retrieve"]) # which chunks were retrieved
|
|
186
|
+
print(run.steps["rerank"]) # which chunks survived reranking
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Why WorkflowBuilder over inline code:**
|
|
190
|
+
|
|
191
|
+
| Concern | Inline Code | WorkflowBuilder |
|
|
192
|
+
|---------|-------------|-----------------|
|
|
193
|
+
| Versioning | Manual, error-prone | Built-in (v1, v2, ...) |
|
|
194
|
+
| Observability | None | Full run history with step-level traces |
|
|
195
|
+
| Testing | Full integration test required | Each node testable in isolation |
|
|
196
|
+
| Sharing | Copy-paste | Export/import workflow templates |
|
|
197
|
+
| A/B testing | Requires code branching | Run two workflow IDs, compare run metrics |
|
|
198
|
+
| Prompt updates | Redeploy required | Update workflow node config, no code change |
|
|
199
|
+
|
|
200
|
+
Store the workflow ID in your application config (environment variable or database). If you need to update the pipeline — change `topK`, swap the reranker, update the system prompt — create a new workflow version and update the config, with no application code changes.
|
|
201
|
+
|
|
202
|
+
## Reference
|
|
203
|
+
|
|
204
|
+
- https://docs.schift.io/workflows/builder
|
|
205
|
+
- https://docs.schift.io/workflows/run-history
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SDK Async Patterns
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Calling the synchronous Schift client inside an async web framework blocks the event loop, serializing all requests and eliminating the concurrency benefit of async frameworks like FastAPI or Express.
|
|
5
|
+
tags: [sdk, async, fastapi, performance, python, typescript]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use the Async Client in Async Applications
|
|
9
|
+
|
|
10
|
+
The Schift Python SDK ships two client classes: `Schift` (synchronous, thread-blocking) and `AsyncSchift` (async/await, non-blocking). The TypeScript SDK is async-only and always returns Promises.
|
|
11
|
+
|
|
12
|
+
Using the synchronous Python client inside an `async def` handler does not raise an error but blocks the entire event loop for the duration of the network call. Under load, this effectively single-threads your server.
|
|
13
|
+
|
|
14
|
+
### Incorrect
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# Python - synchronous client blocks the event loop inside an async handler
|
|
18
|
+
import os
|
|
19
|
+
from fastapi import FastAPI
|
|
20
|
+
from schift import Schift # synchronous client
|
|
21
|
+
|
|
22
|
+
app = FastAPI()
|
|
23
|
+
client = Schift() # uses SCHIFT_API_KEY env var
|
|
24
|
+
|
|
25
|
+
@app.get("/search")
|
|
26
|
+
async def search(q: str, bucket_id: str):
|
|
27
|
+
# client.bucket.search is a blocking I/O call
|
|
28
|
+
# This stalls the event loop while waiting for the HTTP response
|
|
29
|
+
results = client.bucket.search(bucket_id, q)
|
|
30
|
+
return results
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// TypeScript - no sync client exists, but forgetting await is a common mistake
|
|
35
|
+
import { Schift } from '@schift-io/sdk';
|
|
36
|
+
|
|
37
|
+
const client = new Schift();
|
|
38
|
+
|
|
39
|
+
app.get('/search', (req, res) => {
|
|
40
|
+
// Missing await - returns a Promise, not the actual results
|
|
41
|
+
const results = client.bucket.search(req.query.bucketId, req.query.q);
|
|
42
|
+
res.json(results); // serializes the Promise object, not the data
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Correct
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# Python - use AsyncSchift with async def handlers
|
|
50
|
+
import os
|
|
51
|
+
from fastapi import FastAPI
|
|
52
|
+
from schift import AsyncSchift # async client
|
|
53
|
+
|
|
54
|
+
app = FastAPI()
|
|
55
|
+
client = AsyncSchift() # reads SCHIFT_API_KEY automatically
|
|
56
|
+
|
|
57
|
+
@app.get("/search")
|
|
58
|
+
async def search(q: str, bucket_id: str):
|
|
59
|
+
results = await client.bucket.search(bucket_id, q)
|
|
60
|
+
return results
|
|
61
|
+
|
|
62
|
+
# Parallel calls: use asyncio.gather to fan out multiple searches concurrently
|
|
63
|
+
import asyncio
|
|
64
|
+
|
|
65
|
+
@app.get("/multi-search")
|
|
66
|
+
async def multi_search(q: str, bucket_ids: list[str]):
|
|
67
|
+
tasks = [client.bucket.search(bid, q) for bid in bucket_ids]
|
|
68
|
+
results = await asyncio.gather(*tasks)
|
|
69
|
+
return results
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// TypeScript - always await; use Promise.all for concurrent calls
|
|
74
|
+
import { Schift } from '@schift-io/sdk';
|
|
75
|
+
import express from 'express';
|
|
76
|
+
|
|
77
|
+
const app = express();
|
|
78
|
+
const client = new Schift(); // reads SCHIFT_API_KEY automatically
|
|
79
|
+
|
|
80
|
+
app.get('/search', async (req, res) => {
|
|
81
|
+
const results = await client.bucket.search(
|
|
82
|
+
req.query.bucketId as string,
|
|
83
|
+
req.query.q as string
|
|
84
|
+
);
|
|
85
|
+
res.json(results);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Parallel calls
|
|
89
|
+
app.get('/multi-search', async (req, res) => {
|
|
90
|
+
const bucketIds = (req.query.bucketIds as string).split(',');
|
|
91
|
+
const results = await Promise.all(
|
|
92
|
+
bucketIds.map(id => client.bucket.search(id, req.query.q as string))
|
|
93
|
+
);
|
|
94
|
+
res.json(results);
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
For background jobs or scripts where blocking is acceptable, the synchronous `Schift` client is fine and has simpler usage. Reserve `AsyncSchift` for code running inside an event loop.
|
|
99
|
+
|
|
100
|
+
## Reference
|
|
101
|
+
|
|
102
|
+
- https://docs.schift.io/sdk/python#async
|
|
103
|
+
- https://docs.schift.io/sdk/typescript
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SDK Authentication Patterns
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Hardcoded API keys in source code are the most common cause of credential leaks. A leaked `sch_` key can drain your quota and expose your data before you notice.
|
|
5
|
+
tags: [sdk, auth, security, api-key]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Never Hardcode API Keys
|
|
9
|
+
|
|
10
|
+
API keys must never appear as string literals in source code. Keys committed to version control or embedded in build artifacts are exposed to anyone with repository access, container image access, or build log access. Rotate immediately if a key is ever exposed.
|
|
11
|
+
|
|
12
|
+
Schift API keys follow the format `sch_` followed by a random string. Treat them like passwords.
|
|
13
|
+
|
|
14
|
+
### Incorrect
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# Python - key is visible in source and will leak via git history
|
|
18
|
+
from schift import Schift
|
|
19
|
+
|
|
20
|
+
client = Schift(api_key="sch_abc123xyz789...") # NEVER do this
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// TypeScript - same problem; bundlers may embed this in client-side code
|
|
25
|
+
import { Schift } from '@schift-io/sdk';
|
|
26
|
+
|
|
27
|
+
const client = new Schift({ apiKey: 'sch_abc123xyz789...' }); // NEVER do this
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Both examples embed a literal key that will appear in `git log`, CI logs, Docker image layers, and any bundle output.
|
|
31
|
+
|
|
32
|
+
### Correct
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# Python - read from environment variable explicitly
|
|
36
|
+
import os
|
|
37
|
+
from schift import Schift
|
|
38
|
+
|
|
39
|
+
client = Schift(api_key=os.environ["SCHIFT_API_KEY"])
|
|
40
|
+
|
|
41
|
+
# Or rely on the SDK's automatic env-var lookup (recommended for simplicity)
|
|
42
|
+
# The SDK reads SCHIFT_API_KEY automatically when api_key is omitted
|
|
43
|
+
client = Schift()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// TypeScript - read from process.env
|
|
48
|
+
import { Schift } from '@schift-io/sdk';
|
|
49
|
+
|
|
50
|
+
const client = new Schift({ apiKey: process.env.SCHIFT_API_KEY });
|
|
51
|
+
|
|
52
|
+
// Or rely on SDK auto-detection (reads SCHIFT_API_KEY if apiKey is omitted)
|
|
53
|
+
const client = new Schift();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Set the key in your environment:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Local development (.envrc, never committed)
|
|
60
|
+
export SCHIFT_API_KEY=sch_dev_...
|
|
61
|
+
|
|
62
|
+
# CI/CD - set as a masked secret variable in your platform
|
|
63
|
+
# Docker / Kubernetes - inject via --env-file or a Secret resource
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Additional best practices:
|
|
67
|
+
- Use **project-scoped keys** (not org-wide admin keys) so a leaked key has minimal blast radius
|
|
68
|
+
- Create **separate keys** for `dev`, `staging`, and `prod` environments
|
|
69
|
+
- **Rotate keys regularly** from the Schift dashboard; old keys can be revoked without downtime
|
|
70
|
+
- Add `SCHIFT_API_KEY` to `.gitignore`-adjacent `.envrc` or use a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault)
|
|
71
|
+
|
|
72
|
+
## Reference
|
|
73
|
+
|
|
74
|
+
- https://docs.schift.io/authentication
|
|
75
|
+
- https://docs.schift.io/sdk/python#authentication
|
|
76
|
+
- https://docs.schift.io/sdk/typescript#authentication
|