d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import type { Chunk, StructuredDataOptions } from '@/lib/chunkers/types'
|
|
|
|
/** Structured data is denser in tokens (~3 chars/token vs ~4 for prose) */
|
|
function estimateStructuredTokens(text: string): number {
|
|
if (!text?.trim()) return 0
|
|
return Math.ceil(text.length / 3)
|
|
}
|
|
|
|
const logger = createLogger('StructuredDataChunker')
|
|
|
|
const DEFAULT_CONFIG = {
|
|
TARGET_CHUNK_SIZE: 1024,
|
|
MIN_ROWS_PER_CHUNK: 5,
|
|
MAX_ROWS_PER_CHUNK: 500,
|
|
INCLUDE_HEADERS_IN_EACH_CHUNK: true,
|
|
} as const
|
|
|
|
export class StructuredDataChunker {
|
|
static async chunkStructuredData(
|
|
content: string,
|
|
options: StructuredDataOptions = {}
|
|
): Promise<Chunk[]> {
|
|
const chunks: Chunk[] = []
|
|
const lines = content.split('\n').filter((line) => line.trim())
|
|
|
|
if (lines.length === 0) {
|
|
return chunks
|
|
}
|
|
|
|
const targetChunkSize = options.chunkSize ?? DEFAULT_CONFIG.TARGET_CHUNK_SIZE
|
|
|
|
const headerLine = options.headers?.join('\t') || lines[0]
|
|
const dataStartIndex = options.headers ? 0 : 1
|
|
|
|
const estimatedTokensPerRow = StructuredDataChunker.estimateStructuredTokensPerRow(
|
|
lines.slice(dataStartIndex, Math.min(10, lines.length))
|
|
)
|
|
const optimalRowsPerChunk = StructuredDataChunker.calculateOptimalRowsPerChunk(
|
|
estimatedTokensPerRow,
|
|
targetChunkSize
|
|
)
|
|
|
|
logger.info(
|
|
`Structured data chunking: ${lines.length} rows, ~${estimatedTokensPerRow} tokens/row, ${optimalRowsPerChunk} rows/chunk, target: ${targetChunkSize} tokens`
|
|
)
|
|
|
|
let currentChunkRows: string[] = []
|
|
let currentTokenEstimate = 0
|
|
const headerTokens = estimateStructuredTokens(headerLine)
|
|
let chunkStartRow = dataStartIndex
|
|
|
|
for (let i = dataStartIndex; i < lines.length; i++) {
|
|
const row = lines[i]
|
|
const rowTokens = estimateStructuredTokens(row)
|
|
|
|
const projectedTokens =
|
|
currentTokenEstimate +
|
|
rowTokens +
|
|
(DEFAULT_CONFIG.INCLUDE_HEADERS_IN_EACH_CHUNK ? headerTokens : 0)
|
|
|
|
const shouldCreateChunk =
|
|
(projectedTokens > targetChunkSize &&
|
|
currentChunkRows.length >= DEFAULT_CONFIG.MIN_ROWS_PER_CHUNK) ||
|
|
currentChunkRows.length >= optimalRowsPerChunk
|
|
|
|
if (shouldCreateChunk && currentChunkRows.length > 0) {
|
|
const chunkContent = StructuredDataChunker.formatChunk(
|
|
headerLine,
|
|
currentChunkRows,
|
|
options.sheetName
|
|
)
|
|
chunks.push(StructuredDataChunker.createChunk(chunkContent, chunkStartRow, i - 1))
|
|
|
|
currentChunkRows = []
|
|
currentTokenEstimate = 0
|
|
chunkStartRow = i
|
|
}
|
|
|
|
currentChunkRows.push(row)
|
|
currentTokenEstimate += rowTokens
|
|
}
|
|
|
|
if (currentChunkRows.length > 0) {
|
|
const chunkContent = StructuredDataChunker.formatChunk(
|
|
headerLine,
|
|
currentChunkRows,
|
|
options.sheetName
|
|
)
|
|
chunks.push(StructuredDataChunker.createChunk(chunkContent, chunkStartRow, lines.length - 1))
|
|
}
|
|
|
|
logger.info(`Created ${chunks.length} chunks from ${lines.length} rows of structured data`)
|
|
|
|
return chunks
|
|
}
|
|
|
|
private static formatChunk(headerLine: string, rows: string[], sheetName?: string): string {
|
|
let content = ''
|
|
|
|
if (sheetName) {
|
|
content += `=== ${sheetName} ===\n\n`
|
|
}
|
|
|
|
if (DEFAULT_CONFIG.INCLUDE_HEADERS_IN_EACH_CHUNK) {
|
|
content += `Headers: ${headerLine}\n`
|
|
content += `${'-'.repeat(Math.min(80, headerLine.length))}\n`
|
|
}
|
|
|
|
content += rows.join('\n')
|
|
content += `\n\n[${rows.length} rows of data]`
|
|
|
|
return content
|
|
}
|
|
|
|
private static createChunk(content: string, startRow: number, endRow: number): Chunk {
|
|
return {
|
|
text: content,
|
|
tokenCount: estimateStructuredTokens(content),
|
|
metadata: {
|
|
startIndex: startRow,
|
|
endIndex: endRow,
|
|
},
|
|
}
|
|
}
|
|
|
|
private static estimateStructuredTokensPerRow(sampleRows: string[]): number {
|
|
if (sampleRows.length === 0) return 50
|
|
|
|
const totalTokens = sampleRows.reduce((sum, row) => sum + estimateStructuredTokens(row), 0)
|
|
return Math.ceil(totalTokens / sampleRows.length)
|
|
}
|
|
|
|
private static calculateOptimalRowsPerChunk(
|
|
tokensPerRow: number,
|
|
targetChunkSize: number
|
|
): number {
|
|
const optimal = Math.floor(targetChunkSize / tokensPerRow)
|
|
|
|
return Math.min(
|
|
Math.max(optimal, DEFAULT_CONFIG.MIN_ROWS_PER_CHUNK),
|
|
DEFAULT_CONFIG.MAX_ROWS_PER_CHUNK
|
|
)
|
|
}
|
|
|
|
static isStructuredData(content: string, mimeType?: string): boolean {
|
|
if (mimeType) {
|
|
const structuredMimeTypes = [
|
|
'text/csv',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'application/vnd.ms-excel',
|
|
'text/tab-separated-values',
|
|
]
|
|
if (structuredMimeTypes.includes(mimeType)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
const lines = content.split('\n').slice(0, 10)
|
|
if (lines.length < 2) return false
|
|
|
|
const delimiters = [',', '\t', '|']
|
|
for (const delimiter of delimiters) {
|
|
const escaped = delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
const counts = lines.map((line) => (line.match(new RegExp(escaped, 'g')) || []).length)
|
|
const avgCount = counts.reduce((a, b) => a + b, 0) / counts.length
|
|
|
|
const tolerance = Math.max(1, Math.ceil(avgCount * 0.2))
|
|
if (avgCount > 2 && counts.every((c) => Math.abs(c - avgCount) <= tolerance)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|