// ============================================================================== // The-Tired-AGI Core: Ingest Pipeline Implementation // Read-Only Text Processing & Embedding Generation // ============================================================================== #include "ingest_pipeline.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace tired_agi { // ============================================================================== // Constructor / Destructor // ============================================================================== IngestPipeline::IngestPipeline(IngestConfig config) noexcept : config_(config) { stat_start_time_ms_.store(get_time_ms(), std::memory_order_relaxed); } IngestPipeline::~IngestPipeline() = default; // ============================================================================== // File Processing // ============================================================================== std::vector IngestPipeline::process_file(const std::filesystem::path& path) noexcept { std::vector embeddings; stat_files_scanned_.fetch_add(1, std::memory_order_relaxed); // Check if file is allowed if (!is_allowed_file(path)) { report_error("process_file", "File extension not allowed: " + path.string()); return embeddings; } // Check file size std::error_code ec; auto file_size = std::filesystem::file_size(path, ec); if (ec) { report_error("process_file", "Cannot get file size: " + path.string()); return embeddings; } if (file_size > config_.max_file_size_mb * 1024 * 1024) { report_error("process_file", "File too large: " + path.string()); return embeddings; } // Check if binary if (config_.skip_binary_files && is_binary_file(path)) { return embeddings; // Skip silently } // Read and chunk auto chunks = read_and_chunk(path); if (chunks.empty()) { report_error("process_file", "No chunks generated: " + path.string()); return embeddings; } // Generate embeddings embeddings = chunks_to_embeddings(chunks); stat_files_processed_.fetch_add(1, std::memory_order_relaxed); return embeddings; } std::vector IngestPipeline::process_directory( const std::filesystem::path& dir, bool recursive ) noexcept { std::vector all_embeddings; std::vector files; // Collect all files try { if (recursive) { for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { if (entry.is_regular_file()) { if (config_.skip_hidden_files && entry.path().filename().string()[0] == '.') { continue; } files.push_back(entry.path()); } } } else { for (const auto& entry : std::filesystem::directory_iterator(dir)) { if (entry.is_regular_file()) { if (config_.skip_hidden_files && entry.path().filename().string()[0] == '.') { continue; } files.push_back(entry.path()); } } } } catch (const std::exception& e) { report_error("process_directory", std::string("Directory iteration failed: ") + e.what()); return all_embeddings; } // Process files in parallel all_embeddings = process_files_parallel(files); return all_embeddings; } std::vector IngestPipeline::process_files_parallel( const std::vector& paths ) noexcept { std::vector> per_file_embeddings(paths.size()); // Process in parallel std::vector indices(paths.size()); std::iota(indices.begin(), indices.end(), 0); std::for_each( std::execution::par_unseq, indices.begin(), indices.end(), [&](size_t i) { per_file_embeddings[i] = process_file(paths[i]); } ); // Flatten results std::vector all_embeddings; for (const auto& file_embs : per_file_embeddings) { all_embeddings.insert(all_embeddings.end(), file_embs.begin(), file_embs.end()); } return all_embeddings; } // ============================================================================== // Low-Level Processing // ============================================================================== std::vector IngestPipeline::read_and_chunk(const std::filesystem::path& path) noexcept { std::vector chunks; if (config_.use_mmap) { // Memory-mapped I/O (faster for large files) int fd = open(path.c_str(), O_RDONLY); if (fd < 0) { report_error("read_and_chunk", "Cannot open file: " + path.string()); return chunks; } struct stat sb; if (fstat(fd, &sb) < 0) { close(fd); report_error("read_and_chunk", "Cannot stat file: " + path.string()); return chunks; } size_t file_size = sb.st_size; if (file_size == 0) { close(fd); return chunks; } void* mapped = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); if (mapped == MAP_FAILED) { close(fd); report_error("read_and_chunk", "Cannot mmap file: " + path.string()); return chunks; } // Advise sequential access madvise(mapped, file_size, MADV_SEQUENTIAL); // Chunk the data chunks = chunk_data( static_cast(mapped), file_size, path.string(), 0 ); munmap(mapped, file_size); close(fd); stat_bytes_read_.fetch_add(file_size, std::memory_order_relaxed); } else { // Standard file I/O std::ifstream file(path, std::ios::binary); if (!file) { report_error("read_and_chunk", "Cannot open file: " + path.string()); return chunks; } // Read entire file file.seekg(0, std::ios::end); size_t file_size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(file_size); file.read(reinterpret_cast(buffer.data()), file_size); if (!file) { report_error("read_and_chunk", "Read error: " + path.string()); return chunks; } chunks = chunk_data(buffer.data(), buffer.size(), path.string(), 0); stat_bytes_read_.fetch_add(file_size, std::memory_order_relaxed); } return chunks; } std::vector IngestPipeline::chunk_data( const uint8_t* data, size_t size, const std::string& source_file, uint64_t file_offset ) noexcept { std::vector chunks; size_t pos = 0; uint64_t chunk_id = 0; while (pos < size) { TextChunk chunk; // Determine chunk size size_t remaining = size - pos; size_t chunk_size = std::min(remaining, config_.chunk_size); if (chunk_size < config_.min_chunk_size && pos > 0) { // Skip small trailing chunk, merge with previous break; } // Copy data std::memcpy(chunk.data.data(), data + pos, chunk_size); chunk.size = chunk_size; chunk.chunk_id = chunk_id++; chunk.source_file = source_file; chunk.file_offset = file_offset + pos; chunk.timestamp = get_time_ms(); // Compute hash chunk.hash = compute_hash(chunk.data.data(), chunk.size); // Compute entropy chunk.entropy = compute_entropy(chunk.data.data(), chunk.size); // Filter low-entropy chunks if (chunk.entropy < config_.min_entropy_threshold) { pos += config_.chunk_size - config_.chunk_overlap; continue; } chunks.push_back(chunk); update_statistics(chunk); // Invoke callback if set if (chunk_callback_) { chunk_callback_(chunk); } // Advance position with overlap pos += config_.chunk_size - config_.chunk_overlap; } return chunks; } Embedding IngestPipeline::chunk_to_embedding(const TextChunk& chunk) noexcept { return simple_embedding_generation(chunk); } std::vector IngestPipeline::chunks_to_embeddings( const std::vector& chunks ) noexcept { std::vector embeddings(chunks.size()); // Parallel embedding generation std::vector indices(chunks.size()); std::iota(indices.begin(), indices.end(), 0); std::for_each( std::execution::par_unseq, indices.begin(), indices.end(), [&](size_t i) { embeddings[i] = chunk_to_embedding(chunks[i]); } ); return embeddings; } // ============================================================================== // Utilities // ============================================================================== uint64_t IngestPipeline::compute_hash(const uint8_t* data, size_t size) noexcept { return XXHash64::hash(data, size, 0); } double IngestPipeline::compute_entropy(const uint8_t* data, size_t size) noexcept { if (size == 0) return 0.0; // Build frequency histogram (256 bins for bytes) std::array histogram{}; for (size_t i = 0; i < size; ++i) { histogram[data[i]]++; } // Compute Shannon entropy: H = -Σ p_i * log2(p_i) double entropy = 0.0; double size_inv = 1.0 / static_cast(size); for (uint64_t count : histogram) { if (count > 0) { double p = static_cast(count) * size_inv; entropy -= p * std::log2(p); } } // Normalize to [0,1] (max entropy for uniform distribution is log2(256) = 8) return entropy / 8.0; } bool IngestPipeline::is_binary_file(const std::filesystem::path& path) noexcept { // Read first 8KB and check for binary content std::ifstream file(path, std::ios::binary); if (!file) return true; // Assume binary if can't open constexpr size_t SAMPLE_SIZE = 8192; std::array buffer; file.read(reinterpret_cast(buffer.data()), SAMPLE_SIZE); size_t bytes_read = file.gcount(); if (bytes_read == 0) return false; // Empty file is text // Count non-printable characters size_t non_printable = 0; for (size_t i = 0; i < bytes_read; ++i) { uint8_t c = buffer[i]; // Allow common text characters: printable ASCII, newline, tab, carriage return if (c < 32 && c != '\n' && c != '\t' && c != '\r') { non_printable++; } if (c >= 127 && c < 160) { non_printable++; } } // If more than 10% non-printable, consider binary return (static_cast(non_printable) / bytes_read) > 0.10; } bool IngestPipeline::is_allowed_file(const std::filesystem::path& path) const noexcept { std::string ext = path.extension().string(); return std::find( config_.allowed_extensions.begin(), config_.allowed_extensions.end(), ext ) != config_.allowed_extensions.end(); } // ============================================================================== // Statistics // ============================================================================== IngestStatistics IngestPipeline::get_statistics() const noexcept { IngestStatistics stats; stats.total_files_scanned = stat_files_scanned_.load(std::memory_order_relaxed); stats.total_files_processed = stat_files_processed_.load(std::memory_order_relaxed); stats.total_bytes_read = stat_bytes_read_.load(std::memory_order_relaxed); stats.total_chunks_generated = stat_chunks_generated_.load(std::memory_order_relaxed); stats.total_embeddings_generated = stat_embeddings_generated_.load(std::memory_order_relaxed); stats.total_errors = stat_errors_.load(std::memory_order_relaxed); if (stats.total_chunks_generated > 0) { stats.average_entropy = stat_sum_entropy_.load(std::memory_order_relaxed) / stats.total_chunks_generated; stats.average_chunk_size = stat_sum_chunk_size_.load(std::memory_order_relaxed) / stats.total_chunks_generated; } uint64_t start_time = stat_start_time_ms_.load(std::memory_order_relaxed); uint64_t current_time = get_time_ms(); stats.elapsed_time_ms = current_time - start_time; if (stats.elapsed_time_ms > 0) { double seconds = stats.elapsed_time_ms / 1000.0; double megabytes = stats.total_bytes_read / (1024.0 * 1024.0); stats.processing_rate_mbps = megabytes / seconds; } return stats; } void IngestPipeline::reset_statistics() noexcept { stat_files_scanned_.store(0, std::memory_order_relaxed); stat_files_processed_.store(0, std::memory_order_relaxed); stat_bytes_read_.store(0, std::memory_order_relaxed); stat_chunks_generated_.store(0, std::memory_order_relaxed); stat_embeddings_generated_.store(0, std::memory_order_relaxed); stat_errors_.store(0, std::memory_order_relaxed); stat_sum_entropy_.store(0.0, std::memory_order_relaxed); stat_sum_chunk_size_.store(0.0, std::memory_order_relaxed); stat_start_time_ms_.store(get_time_ms(), std::memory_order_relaxed); } // ============================================================================== // Helper Functions // ============================================================================== Embedding IngestPipeline::simple_embedding_generation(const TextChunk& chunk) noexcept { Embedding emb(config_.embedding_dim); emb.chunk_id = chunk.chunk_id; emb.hash = chunk.hash; emb.timestamp = get_time_ms(); // Simple embedding: statistical features of text // This is a placeholder - a real system would use a neural network // For now, generate deterministic features from hash and content // Use hash to seed the embedding uint64_t seed = chunk.hash; for (size_t i = 0; i < config_.embedding_dim; ++i) { // Simple pseudo-random generation from hash seed = seed * 6364136223846793005ULL + 1442695040888963407ULL; emb.vector[i] = static_cast(seed) / static_cast(UINT64_MAX) * 2.0 - 1.0; } // Mix in entropy and chunk size if (config_.embedding_dim > 0) { emb.vector[0] += chunk.entropy; } if (config_.embedding_dim > 1) { emb.vector[1] += static_cast(chunk.size) / TextChunk::MAX_SIZE; } // Normalize to unit sphere if requested if (config_.normalize_embeddings) { double norm = 0.0; for (double val : emb.vector) { norm += val * val; } norm = std::sqrt(norm); if (norm > 1e-10) { for (double& val : emb.vector) { val /= norm; } } emb.magnitude = 1.0; } else { double norm = 0.0; for (double val : emb.vector) { norm += val * val; } emb.magnitude = std::sqrt(norm); } update_statistics(emb); // Invoke callback if set if (embedding_callback_) { embedding_callback_(emb); } return emb; } void IngestPipeline::update_statistics(const TextChunk& chunk) noexcept { stat_chunks_generated_.fetch_add(1, std::memory_order_relaxed); stat_sum_entropy_.fetch_add(chunk.entropy, std::memory_order_relaxed); stat_sum_chunk_size_.fetch_add(chunk.size, std::memory_order_relaxed); } void IngestPipeline::update_statistics(const Embedding& embedding) noexcept { stat_embeddings_generated_.fetch_add(1, std::memory_order_relaxed); } void IngestPipeline::report_error(const std::string& context, const std::string& message) noexcept { stat_errors_.fetch_add(1, std::memory_order_relaxed); if (error_callback_) { error_callback_(context, message); } } uint64_t IngestPipeline::get_time_ms() const noexcept { using namespace std::chrono; return duration_cast(steady_clock::now().time_since_epoch()).count(); } // ============================================================================== // xxHash64 Implementation // ============================================================================== XXHash64::XXHash64(uint64_t seed) noexcept : total_len_(0) , seed_(seed) , buffer_size_(0) { state_[0] = seed + PRIME64_1 + PRIME64_2; state_[1] = seed + PRIME64_2; state_[2] = seed; state_[3] = seed - PRIME64_1; std::memset(buffer_, 0, sizeof(buffer_)); } void XXHash64::update(const void* data, size_t size) noexcept { const uint8_t* ptr = static_cast(data); total_len_ += size; // Process buffered data first if (buffer_size_ > 0) { size_t to_copy = std::min(size, 32 - buffer_size_); std::memcpy(buffer_ + buffer_size_, ptr, to_copy); buffer_size_ += to_copy; ptr += to_copy; size -= to_copy; if (buffer_size_ == 32) { const uint64_t* lanes = reinterpret_cast(buffer_); state_[0] = round(state_[0], lanes[0]); state_[1] = round(state_[1], lanes[1]); state_[2] = round(state_[2], lanes[2]); state_[3] = round(state_[3], lanes[3]); buffer_size_ = 0; } } // Process 32-byte blocks while (size >= 32) { const uint64_t* lanes = reinterpret_cast(ptr); state_[0] = round(state_[0], lanes[0]); state_[1] = round(state_[1], lanes[1]); state_[2] = round(state_[2], lanes[2]); state_[3] = round(state_[3], lanes[3]); ptr += 32; size -= 32; } // Buffer remaining data if (size > 0) { std::memcpy(buffer_, ptr, size); buffer_size_ = size; } } uint64_t XXHash64::digest() noexcept { uint64_t h64; if (total_len_ >= 32) { h64 = rotl64(state_[0], 1) + rotl64(state_[1], 7) + rotl64(state_[2], 12) + rotl64(state_[3], 18); h64 = merge_round(h64, state_[0]); h64 = merge_round(h64, state_[1]); h64 = merge_round(h64, state_[2]); h64 = merge_round(h64, state_[3]); } else { h64 = seed_ + PRIME64_5; } h64 += total_len_; // Process remaining bytes const uint8_t* ptr = buffer_; size_t remaining = buffer_size_; while (remaining >= 8) { uint64_t lane = *reinterpret_cast(ptr); lane = round(0, lane); h64 ^= lane; h64 = rotl64(h64, 27) * PRIME64_1 + PRIME64_4; ptr += 8; remaining -= 8; } if (remaining >= 4) { uint32_t lane = *reinterpret_cast(ptr); h64 ^= static_cast(lane) * PRIME64_1; h64 = rotl64(h64, 23) * PRIME64_2 + PRIME64_3; ptr += 4; remaining -= 4; } while (remaining > 0) { h64 ^= static_cast(*ptr) * PRIME64_5; h64 = rotl64(h64, 11) * PRIME64_1; ptr++; remaining--; } return avalanche(h64); } uint64_t XXHash64::hash(const void* data, size_t size, uint64_t seed) noexcept { XXHash64 hasher(seed); hasher.update(data, size); return hasher.digest(); } } // namespace tired_agi