use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChunkId(String);
impl ChunkId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl From<String> for ChunkId {
fn from(id: String) -> Self {
Self(id)
}
}
impl From<&str> for ChunkId {
fn from(id: &str) -> Self {
Self(id.to_string())
}
}
impl AsRef<str> for ChunkId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ChunkId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
pub struct TokenCount(usize);
impl TokenCount {
pub fn new(count: usize) -> Self {
Self(count)
}
pub fn get(&self) -> usize {
self.0
}
}
impl From<usize> for TokenCount {
fn from(count: usize) -> Self {
Self(count)
}
}
impl From<TokenCount> for usize {
fn from(count: TokenCount) -> usize {
count.0
}
}
impl std::fmt::Display for TokenCount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq<usize> for TokenCount {
fn eq(&self, other: &usize) -> bool {
self.0 == *other
}
}
impl PartialEq<TokenCount> for usize {
fn eq(&self, other: &TokenCount) -> bool {
*self == other.0
}
}
impl PartialOrd<usize> for TokenCount {
fn partial_cmp(&self, other: &usize) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(other)
}
}
impl PartialOrd<TokenCount> for usize {
fn partial_cmp(&self, other: &TokenCount) -> Option<std::cmp::Ordering> {
self.partial_cmp(&other.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
pub struct ChunkIndexNum(usize);
impl ChunkIndexNum {
pub fn new(index: usize) -> Self {
Self(index)
}
pub fn get(&self) -> usize {
self.0
}
}
impl From<usize> for ChunkIndexNum {
fn from(index: usize) -> Self {
Self(index)
}
}
impl From<ChunkIndexNum> for usize {
fn from(index: ChunkIndexNum) -> usize {
index.0
}
}
impl std::fmt::Display for ChunkIndexNum {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq<usize> for ChunkIndexNum {
fn eq(&self, other: &usize) -> bool {
self.0 == *other
}
}
impl PartialEq<ChunkIndexNum> for usize {
fn eq(&self, other: &ChunkIndexNum) -> bool {
*self == other.0
}
}
impl PartialOrd<usize> for ChunkIndexNum {
fn partial_cmp(&self, other: &usize) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(other)
}
}
impl PartialOrd<ChunkIndexNum> for usize {
fn partial_cmp(&self, other: &ChunkIndexNum) -> Option<std::cmp::Ordering> {
self.partial_cmp(&other.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EmbeddingDimension(usize);
impl EmbeddingDimension {
pub fn new(dimension: usize) -> Self {
Self(dimension)
}
pub fn get(&self) -> usize {
self.0
}
}
impl From<usize> for EmbeddingDimension {
fn from(dimension: usize) -> Self {
Self(dimension)
}
}
impl From<EmbeddingDimension> for usize {
fn from(dimension: EmbeddingDimension) -> usize {
dimension.0
}
}
impl std::fmt::Display for EmbeddingDimension {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub const DEFAULT_TARGET_CHUNK_SIZE_TOKENS: usize = 300;
pub const DEFAULT_OVERLAP_TOKENS: usize = 50;
pub const DEFAULT_MAX_CHUNK_SIZE_TOKENS: usize = 500;
pub const DEFAULT_MIN_CHUNK_SIZE_TOKENS: usize = 100;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SourceLocation {
pub file_path: PathBuf,
pub start_line: usize,
pub end_line: usize,
pub start_char: usize,
pub end_char: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentChunk {
pub id: ChunkId,
pub content: String,
pub token_count: TokenCount,
pub source_location: SourceLocation,
pub chunk_index: ChunkIndexNum,
pub total_chunks: usize,
}
impl ContentChunk {
pub const PLACEHOLDER_CONTENT_PREFIX: &'static str = "[PLACEHOLDER_CONTENT_FROM:";
pub fn has_real_content(&self) -> bool {
!self.content.starts_with(Self::PLACEHOLDER_CONTENT_PREFIX)
}
pub fn real_content(&self) -> Option<&str> {
if self.has_real_content() {
Some(&self.content)
} else {
None
}
}
pub fn rehydrate_content(&self) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
if self.has_real_content() {
return Ok(self.content.clone());
}
match fs::read_to_string(&self.source_location.file_path) {
Ok(file_content) => {
let chars: Vec<char> = file_content.chars().collect();
let start_char = self.source_location.start_char.min(chars.len());
let end_char = self
.source_location
.end_char
.min(chars.len())
.max(start_char);
let chunk_content: String = chars[start_char..end_char].iter().collect();
Ok(chunk_content)
}
Err(err) => {
Err(format!(
"Failed to rehydrate content from {}: {}",
self.source_location.file_path.display(),
err
)
.into())
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkingConfig {
pub target_chunk_size_tokens: usize,
pub overlap_tokens: usize,
pub max_chunk_size_tokens: usize,
pub min_chunk_size_tokens: usize,
}
impl Default for ChunkingConfig {
fn default() -> Self {
Self {
target_chunk_size_tokens: DEFAULT_TARGET_CHUNK_SIZE_TOKENS,
overlap_tokens: DEFAULT_OVERLAP_TOKENS,
max_chunk_size_tokens: DEFAULT_MAX_CHUNK_SIZE_TOKENS,
min_chunk_size_tokens: DEFAULT_MIN_CHUNK_SIZE_TOKENS,
}
}
}
impl ChunkingConfig {
pub fn with_target_size(mut self, size: usize) -> Self {
self.target_chunk_size_tokens = size;
self
}
pub fn with_overlap(mut self, overlap: usize) -> Self {
self.overlap_tokens = overlap;
self
}
pub fn with_max_size(mut self, max_size: usize) -> Self {
self.max_chunk_size_tokens = max_size;
self
}
pub fn with_min_size(mut self, min_size: usize) -> Self {
self.min_chunk_size_tokens = min_size;
self
}
}