use heapless::Vec as HVec;
use heapless::String as HString;
use super::{MicroHNSW, HNSWConfig, SearchResult, MicroVector, DistanceMetric};
pub const MAX_DOCUMENTS: usize = 256;
pub const MAX_CHUNKS: usize = 512;
pub const CHUNK_DIM: usize = 32;
pub const MAX_CHUNK_TEXT: usize = 128;
pub const MAX_CONTEXT: usize = 256;
#[derive(Debug, Clone)]
pub struct RAGConfig {
pub top_k: usize,
pub min_similarity: i32,
pub max_context_tokens: usize,
pub include_sources: bool,
pub enable_reranking: bool,
}
impl Default for RAGConfig {
fn default() -> Self {
Self {
top_k: 3,
min_similarity: 200, max_context_tokens: 128,
include_sources: true,
enable_reranking: false,
}
}
}
#[derive(Debug, Clone)]
pub struct Chunk {
pub id: u32,
pub doc_id: u16,
pub chunk_idx: u8,
pub text: HString<MAX_CHUNK_TEXT>,
pub embedding: HVec<i8, CHUNK_DIM>,
}
impl Chunk {
pub fn new(id: u32, doc_id: u16, chunk_idx: u8, text: &str, embedding: &[i8]) -> Option<Self> {
let mut text_str = HString::new();
for c in text.chars().take(MAX_CHUNK_TEXT) {
text_str.push(c).ok()?;
}
let mut embed = HVec::new();
for &v in embedding.iter().take(CHUNK_DIM) {
embed.push(v).ok()?;
}
Some(Self {
id,
doc_id,
chunk_idx,
text: text_str,
embedding: embed,
})
}
}
#[derive(Debug)]
pub struct RAGResult {
pub context: HString<MAX_CONTEXT>,
pub source_ids: HVec<u32, 8>,
pub scores: HVec<i32, 8>,
pub truncated: bool,
}
pub struct MicroRAG {
config: RAGConfig,
index: MicroHNSW<CHUNK_DIM, MAX_CHUNKS>,
chunks: HVec<Chunk, MAX_CHUNKS>,
doc_count: u16,
next_chunk_id: u32,
}
impl MicroRAG {
pub fn new(config: RAGConfig) -> Self {
let hnsw_config = HNSWConfig {
m: 6,
m_max0: 12,
ef_construction: 24,
ef_search: 16,
metric: DistanceMetric::Euclidean,
binary_mode: false,
};
Self {
config,
index: MicroHNSW::new(hnsw_config),
chunks: HVec::new(),
doc_count: 0,
next_chunk_id: 0,
}
}
pub fn chunk_count(&self) -> usize {
self.chunks.len()
}
pub fn doc_count(&self) -> u16 {
self.doc_count
}
pub fn memory_bytes(&self) -> usize {
self.index.memory_bytes() + self.chunks.len() * core::mem::size_of::<Chunk>()
}
pub fn add_document(&mut self, chunks: &[(&str, &[i8])]) -> Result<u16, &'static str> {
let doc_id = self.doc_count;
self.doc_count += 1;
for (idx, (text, embedding)) in chunks.iter().enumerate() {
if self.chunks.len() >= MAX_CHUNKS {
return Err("Chunk limit reached");
}
let chunk_id = self.next_chunk_id;
self.next_chunk_id += 1;
let chunk = Chunk::new(chunk_id, doc_id, idx as u8, text, embedding)
.ok_or("Failed to create chunk")?;
let vec = MicroVector {
data: chunk.embedding.clone(),
id: chunk_id,
};
self.index.insert(&vec)?;
self.chunks.push(chunk).map_err(|_| "Chunk storage full")?;
}
Ok(doc_id)
}
pub fn add_knowledge(&mut self, text: &str, embedding: &[i8]) -> Result<u32, &'static str> {
if self.chunks.len() >= MAX_CHUNKS {
return Err("Chunk limit reached");
}
let chunk_id = self.next_chunk_id;
self.next_chunk_id += 1;
let chunk = Chunk::new(chunk_id, self.doc_count, 0, text, embedding)
.ok_or("Failed to create chunk")?;
let vec = MicroVector {
data: chunk.embedding.clone(),
id: chunk_id,
};
self.index.insert(&vec)?;
self.chunks.push(chunk).map_err(|_| "Chunk storage full")?;
self.doc_count += 1;
Ok(chunk_id)
}
pub fn retrieve(&self, query_embedding: &[i8]) -> RAGResult {
let search_results = self.index.search(query_embedding, self.config.top_k * 2);
let mut context = HString::new();
let mut source_ids = HVec::new();
let mut scores = HVec::new();
let mut truncated = false;
let mut added = 0;
for result in search_results.iter() {
if result.distance > self.config.min_similarity && added > 0 {
continue;
}
if let Some(chunk) = self.find_chunk_by_id(result.id) {
if context.len() + chunk.text.len() + 2 > MAX_CONTEXT {
if added > 0 {
truncated = true;
break;
}
}
if !context.is_empty() {
let _ = context.push_str(" | ");
}
for c in chunk.text.chars() {
if context.push(c).is_err() {
truncated = true;
break;
}
}
let _ = source_ids.push(result.id);
let _ = scores.push(result.distance);
added += 1;
if added >= self.config.top_k {
break;
}
}
}
RAGResult {
context,
source_ids,
scores,
truncated,
}
}
pub fn retrieve_prompt(&self, query_embedding: &[i8], question: &str) -> HString<512> {
let rag_result = self.retrieve(query_embedding);
let mut prompt = HString::new();
let _ = prompt.push_str("Context: ");
for c in rag_result.context.chars() {
let _ = prompt.push(c);
}
let _ = prompt.push_str("\n\nQuestion: ");
for c in question.chars().take(128) {
let _ = prompt.push(c);
}
let _ = prompt.push_str("\n\nAnswer: ");
prompt
}
fn find_chunk_by_id(&self, id: u32) -> Option<&Chunk> {
self.chunks.iter().find(|c| c.id == id)
}
pub fn get_document_chunks(&self, doc_id: u16) -> HVec<&Chunk, 16> {
let mut result = HVec::new();
for chunk in self.chunks.iter() {
if chunk.doc_id == doc_id {
let _ = result.push(chunk);
}
}
result.sort_by_key(|c| c.chunk_idx);
result
}
}
impl Default for MicroRAG {
fn default() -> Self {
Self::new(RAGConfig::default())
}
}
pub fn chunk_text(text: &str, chunk_size: usize, overlap: usize) -> HVec<HString<MAX_CHUNK_TEXT>, 16> {
let mut chunks = HVec::new();
let chars: HVec<char, 1024> = text.chars().collect();
let mut start = 0;
while start < chars.len() {
let end = (start + chunk_size).min(chars.len());
let mut chunk = HString::new();
for &c in chars[start..end].iter() {
let _ = chunk.push(c);
}
if !chunk.is_empty() {
let _ = chunks.push(chunk);
}
if end >= chars.len() {
break;
}
start = end.saturating_sub(overlap);
}
chunks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rag_basic() {
let mut rag = MicroRAG::default();
let embed1 = [10i8; CHUNK_DIM];
let embed2 = [20i8; CHUNK_DIM];
rag.add_knowledge("Paris is the capital of France", &embed1).unwrap();
rag.add_knowledge("London is the capital of UK", &embed2).unwrap();
assert_eq!(rag.chunk_count(), 2);
}
#[test]
fn test_rag_retrieve() {
let mut rag = MicroRAG::default();
let embed1 = [10i8; CHUNK_DIM];
let embed2 = [50i8; CHUNK_DIM];
rag.add_knowledge("The sky is blue", &embed1).unwrap();
rag.add_knowledge("Grass is green", &embed2).unwrap();
let query = [11i8; CHUNK_DIM];
let result = rag.retrieve(&query);
assert!(!result.context.is_empty());
assert!(!result.source_ids.is_empty());
}
#[test]
fn test_chunk_text() {
let text = "Hello world this is a test";
let chunks = chunk_text(text, 10, 3);
assert!(!chunks.is_empty());
}
}