Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chunk-size-and-file-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mixedbread/cli": minor
---

Add `--max-chunk-size` to `store sync` and `store upload` to control the chunk size used when processing files, and `--with-file-context` to `store create` to enable LLM file contextualization at the store level. Update `@mixedbread/sdk` to 0.77.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
},
"dependencies": {
"@clack/prompts": "^1.0.1",
"@mixedbread/sdk": "^0.57.0",
"@mixedbread/sdk": "^0.77.0",
"@pnpm/tabtab": "^0.5.4",
"chalk": "^5.6.2",
"cli-table3": "^0.6.5",
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/commands/store/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const CreateStoreSchema = extendGlobalOptions({
description: z.string().optional(),
public: z.union([z.boolean(), z.string()]).optional(),
contextualization: z.union([z.boolean(), z.string()]).optional(),
withFileContext: z
.boolean({ error: '"with-file-context" must be a boolean' })
.optional(),
expiresAfter: z.coerce
.number({ error: '"expires-after" must be a number' })
.int({ error: '"expires-after" must be an integer' })
Expand All @@ -42,6 +45,10 @@ export function createCreateCommand(): Command {
"--contextualization [fields]",
"Enable contextualization, optionally with specific metadata fields (comma-separated)"
)
.option(
"--with-file-context",
"Use an LLM to situate each text chunk within the full document during ingestion"
)
.option("--expires-after <days>", "Expire after number of days")
.option("--metadata <json>", "Additional metadata as JSON string")
);
Expand All @@ -66,7 +73,10 @@ export function createCreateCommand(): Command {
name: parsedOptions.name,
description: parsedOptions.description,
is_public: parsePublicFlag(parsedOptions.public),
config: buildStoreConfig(parsedOptions.contextualization),
config: buildStoreConfig(
parsedOptions.contextualization,
parsedOptions.withFileContext
),
expires_after: parsedOptions.expiresAfter
? {
anchor: "last_active_at",
Expand Down
30 changes: 22 additions & 8 deletions packages/cli/src/commands/store/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,32 @@ type ParsedSearchOptions = z.infer<typeof SearchStoreSchema> & {
storeIdentifier: string;
};

interface StoreFileSearchResponse {
data: Array<{
filename: string;
score: number;
store_id: string;
chunk_index?: number;
metadata?: unknown;
}>;
}

async function searchStoreFiles(
client: Mixedbread,
parsedOptions: ParsedSearchOptions
) {
return await client.stores.files.search({
query: parsedOptions.query,
store_identifiers: [parsedOptions.storeIdentifier],
top_k: parsedOptions.topK,
search_options: {
return_metadata: parsedOptions.returnMetadata,
score_threshold: parsedOptions.threshold,
rerank: parsedOptions.rerank,
// The file-level search endpoint is no longer exposed as a typed method on
// the SDK client, so call it directly.
return await client.post<StoreFileSearchResponse>("/v1/stores/files/search", {
body: {
query: parsedOptions.query,
store_identifiers: [parsedOptions.storeIdentifier],
top_k: parsedOptions.topK,
search_options: {
return_metadata: parsedOptions.returnMetadata,
score_threshold: parsedOptions.threshold,
rerank: parsedOptions.rerank,
},
},
});
}
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/commands/store/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const SyncStoreSchema = extendGlobalOptions({
.array(z.string())
.min(1, { error: "At least one pattern is required" }),
strategy: z.enum(["fast", "high_quality"]).optional(),
maxChunkSize: z.coerce
.number({ error: '"max-chunk-size" must be a number' })
.int({ error: '"max-chunk-size" must be an integer' })
.positive({ error: '"max-chunk-size" must be positive' })
.optional(),
contextualization: z
.boolean({ error: '"contextualization" must be a boolean' })
.optional(),
Expand Down Expand Up @@ -69,6 +74,10 @@ export function createSyncCommand(): Command {
"File patterns, folders, or paths to sync (supports ./** and folder names)"
)
.option("--strategy <strategy>", "Upload strategy (fast|high_quality)")
.option(
"--max-chunk-size <n>",
"Maximum chunk size used when processing files"
)
.option(
"--contextualization",
"Deprecated (ignored): contextualization is now configured at the store level"
Expand Down Expand Up @@ -231,6 +240,7 @@ export function createSyncCommand(): Command {
// Execute changes
const syncResults = await executeSyncChanges(client, store.id, analysis, {
strategy: parsedOptions.strategy,
maxChunkSize: parsedOptions.maxChunkSize,
metadata: additionalMetadata,
gitInfo: gitInfo.isRepo ? gitInfo : undefined,
parallel: parsedOptions.parallel,
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/commands/store/upload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { stat } from "node:fs/promises";
import type { FileCreateParams } from "@mixedbread/sdk/resources/stores";
import type { StoreFileConfig } from "@mixedbread/sdk/resources/stores";
import chalk from "chalk";
import { Command } from "commander";
import { glob } from "glob";
Expand Down Expand Up @@ -32,6 +32,11 @@ const UploadStoreSchema = extendGlobalOptions({
error: '"strategy" must be either "fast" or "high_quality"',
})
.optional(),
maxChunkSize: z.coerce
.number({ error: '"max-chunk-size" must be a number' })
.int({ error: '"max-chunk-size" must be an integer' })
.positive({ error: '"max-chunk-size" must be positive' })
.optional(),
contextualization: z
.boolean({ error: '"contextualization" must be a boolean' })
.optional(),
Expand Down Expand Up @@ -61,7 +66,8 @@ const UploadStoreSchema = extendGlobalOptions({
});

export interface UploadOptions extends GlobalOptions {
strategy?: FileCreateParams.Config["parsing_strategy"];
strategy?: StoreFileConfig["parsing_strategy"];
maxChunkSize?: number;
contextualization?: boolean;
metadata?: string;
dryRun?: boolean;
Expand All @@ -83,6 +89,10 @@ export function createUploadCommand(): Command {
'File patterns to upload (e.g., "*.md", "docs/**/*.pdf")'
)
.option("--strategy <strategy>", "Processing strategy")
.option(
"--max-chunk-size <n>",
"Maximum chunk size used when processing files"
)
.option(
"--contextualization",
"Deprecated (ignored): contextualization is now configured at the store level"
Expand Down Expand Up @@ -258,6 +268,7 @@ export function createUploadCommand(): Command {
unique: parsedOptions.unique || false,
existingFiles,
parallel,
maxChunkSize: parsedOptions.maxChunkSize,
multipartUpload,
});
} catch (error) {
Expand Down
21 changes: 16 additions & 5 deletions packages/cli/src/utils/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isCancel, select } from "@clack/prompts";
import type { Mixedbread } from "@mixedbread/sdk";
import type {
ContextualizationConfig,
FileListParams,
Store,
StoreFile,
Expand Down Expand Up @@ -74,11 +75,13 @@ export function parsePublicFlag(value?: boolean | string): boolean | undefined {
}

export function buildStoreConfig(
contextualization?: boolean | string
): { contextualization: boolean | { with_metadata: string[] } } | undefined {
if (contextualization === undefined) {
contextualization?: boolean | string,
withFileContext?: boolean
): { contextualization: boolean | ContextualizationConfig } | undefined {
if (contextualization === undefined && !withFileContext) {
return undefined;
}
const config: ContextualizationConfig = {};
if (typeof contextualization === "string") {
const fields = contextualization
.split(",")
Expand All @@ -89,9 +92,17 @@ export function buildStoreConfig(
`Invalid value for --contextualization: "${contextualization}". Use a comma-separated list of metadata fields.`
);
}
return { contextualization: { with_metadata: fields } };
config.with_metadata = fields;
} else if (contextualization === true) {
if (!withFileContext) {
return { contextualization: true };
}
config.with_metadata = true;
}
if (withFileContext) {
config.with_file_context = true;
}
return { contextualization: true };
return { contextualization: config };
}

export async function checkExistingFiles(
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/utils/sync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import type Mixedbread from "@mixedbread/sdk";
import type { FileCreateParams } from "@mixedbread/sdk/resources/stores";
import type { StoreFileConfig } from "@mixedbread/sdk/resources/stores";
import chalk from "chalk";
import { glob } from "glob";
import pLimit from "p-limit";
Expand Down Expand Up @@ -264,7 +264,8 @@ export async function executeSyncChanges(
storeIdentifier: string,
analysis: SyncAnalysis,
options: {
strategy?: FileCreateParams.Config["parsing_strategy"];
strategy?: StoreFileConfig["parsing_strategy"];
maxChunkSize?: number;
metadata?: Record<string, unknown>;
gitInfo?: { commit: string; branch: string };
parallel?: number;
Expand Down Expand Up @@ -385,6 +386,7 @@ export async function executeSyncChanges(
await uploadFile(client, storeIdentifier, file.path, {
metadata: finalMetadata,
strategy: options.strategy,
maxChunkSize: options.maxChunkSize,
externalId: file.path,
multipartUpload: options.multipartUpload,
onProgress: (progress: UploadProgress) => {
Expand Down Expand Up @@ -455,7 +457,7 @@ export function displaySyncResultsSummary(
gitInfo: { commit: string; branch: string; isRepo: boolean },
fromGit?: string,
uploadOptions?: {
strategy?: FileCreateParams.Config["parsing_strategy"];
strategy?: StoreFileConfig["parsing_strategy"];
}
): void {
console.log(chalk.bold("\nSummary:"));
Expand Down
37 changes: 24 additions & 13 deletions packages/cli/src/utils/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { stat } from "node:fs/promises";
import { cpus, freemem } from "node:os";
import { basename, relative } from "node:path";
import type Mixedbread from "@mixedbread/sdk";
import type { FileCreateParams } from "@mixedbread/sdk/resources/stores";
import type { StoreFileConfig } from "@mixedbread/sdk/resources/stores";
import chalk from "chalk";
import { lookup } from "mime-types";
import pLimit from "p-limit";
Expand Down Expand Up @@ -126,18 +126,33 @@ export interface UploadProgress {

export interface UploadFileOptions {
metadata?: Record<string, unknown>;
strategy?: FileCreateParams.Config["parsing_strategy"];
strategy?: StoreFileConfig["parsing_strategy"];
maxChunkSize?: number;
externalId?: string;
multipartUpload?: MultipartUploadOptions;
onProgress?: (progress: UploadProgress) => void;
}

export interface FileToUpload {
path: string;
strategy: FileCreateParams.Config["parsing_strategy"];
strategy: StoreFileConfig["parsing_strategy"];
metadata: Record<string, unknown>;
}

/**
* Build the file processing config for an upload request. `max_chunk_size` is
* accepted by the API but not yet part of the SDK's StoreFileConfig type.
*/
export function buildFileConfig(
strategy: StoreFileConfig["parsing_strategy"],
maxChunkSize?: number
): StoreFileConfig {
return {
parsing_strategy: strategy,
...(maxChunkSize != null && { max_chunk_size: maxChunkSize }),
} as StoreFileConfig;
}

export interface UploadResults {
uploaded: number;
updated: number;
Expand Down Expand Up @@ -189,6 +204,7 @@ export async function uploadFile(
const {
metadata = {},
strategy,
maxChunkSize,
externalId,
multipartUpload,
onProgress,
Expand Down Expand Up @@ -238,9 +254,7 @@ export async function uploadFile(
file,
body: {
metadata,
config: {
parsing_strategy: strategy,
},
config: buildFileConfig(strategy, maxChunkSize),
...(externalId ? { external_id: externalId } : {}),
},
options: { timeout: UPLOAD_TIMEOUT },
Expand Down Expand Up @@ -279,6 +293,7 @@ export async function uploadFilesInBatch(
existingFiles: Map<string, string>;
parallel: number;
showStrategyPerFile?: boolean;
maxChunkSize?: number;
multipartUpload?: MultipartUploadOptions;
}
): Promise<UploadResults> {
Expand All @@ -287,6 +302,7 @@ export async function uploadFilesInBatch(
existingFiles,
parallel,
showStrategyPerFile = false,
maxChunkSize,
multipartUpload,
} = options;

Expand Down Expand Up @@ -372,10 +388,7 @@ export async function uploadFilesInBatch(
);
}

const mpConfig = resolveMultipartConfig(
stats.size,
multipartUpload
);
const mpConfig = resolveMultipartConfig(stats.size, multipartUpload);
const totalFileBytes = stats.size;

if (totalFileBytes >= mpConfig.threshold) {
Expand All @@ -391,9 +404,7 @@ export async function uploadFilesInBatch(
file: fileToUpload,
body: {
metadata: fileMetadata,
config: {
parsing_strategy: file.strategy,
},
config: buildFileConfig(file.strategy, maxChunkSize),
},
options: { timeout: UPLOAD_TIMEOUT },
multipartUpload: {
Expand Down
Loading
Loading