From f2a6bd94479e2c4946417e869f9880230c95d53a Mon Sep 17 00:00:00 2001 From: zTgx <747674262@qq.com> Date: Sat, 11 Apr 2026 12:53:19 +0800 Subject: [PATCH 1/2] feat: add indexing example and improve event system architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove incomplete cli_tool.rs example - Add comprehensive indexing.rs example demonstrating full pipeline: create engine → index document → inspect metrics with batch and incremental modes - Register indexing example in Cargo.toml - Refactor event system by splitting into modular components: move event types to separate module, implement shared emitter with Arc> for proper handler sharing across clones - Update EngineBuilder to properly pass events parameter - Modify Engine constructor to accept events as explicit parameter - Remove duplicate EventEmitter re-export from client module - Export event types and emitter from root module --- examples/rust/cli_tool.rs | 122 ------------- examples/rust/indexing.rs | 105 +++++++++++ rust/Cargo.toml | 4 + rust/src/client/builder.rs | 3 +- rust/src/client/engine.rs | 2 +- rust/src/client/events.rs | 364 +------------------------------------ rust/src/events/emitter.rs | 288 +++++++++++++++++++++++++++++ rust/src/events/mod.rs | 34 ++++ rust/src/events/types.rs | 151 +++++++++++++++ rust/src/lib.rs | 6 +- 10 files changed, 593 insertions(+), 486 deletions(-) delete mode 100644 examples/rust/cli_tool.rs create mode 100644 examples/rust/indexing.rs create mode 100644 rust/src/events/emitter.rs create mode 100644 rust/src/events/mod.rs create mode 100644 rust/src/events/types.rs diff --git a/examples/rust/cli_tool.rs b/examples/rust/cli_tool.rs deleted file mode 100644 index 62a05f33..00000000 --- a/examples/rust/cli_tool.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2026 vectorless developers -// SPDX-License-Identifier: Apache-2.0 - -//! CLI tool example for vectorless. -//! -//! This example shows how to build a command-line tool -//! using vectorless for document indexing and querying. -//! -//! # What you'll learn: -//! - How to structure a CLI application -//! - How to handle subcommands (index, query, info) -//! - How to manage configuration and workspace -//! - How to provide user-friendly output -//! -//! # Example commands: -//! -//! ```bash -//! # Index a document -//! vectorless-cli index ./document.md -//! -//! # Query a document -//! vectorless-cli query "What is the main topic?" -//! -//! # List indexed documents -//! vectorless-cli list -//! -//! # Show document info -//! vectorless-cli info -//! -//! # Delete a document -//! vectorless-cli delete -//! ``` -//! -//! # Implementation notes: -//! -//! ## Recommended crates: -//! - `clap` for argument parsing -//! - `colored` or `termcolor` for colored output -//! - `indicatif` for progress bars -//! - `serde` for configuration -//! -//! ## Configuration file: -//! ```toml -//! # ~/.vectorless/config.toml -//! [llm] -//! provider = "openai" -//! model = "gpt-4" -//! -//! [index] -//! cache_size = 100 -//! -//! [retrieval] -//! max_iterations = 10 -//! ``` -//! -//! # TODO: Implementation steps -//! -//! 1. Define CLI structure with clap -//! 2. Implement index subcommand -//! 3. Implement query subcommand -//! 4. Implement list/info subcommands -//! 5. Add configuration management -//! 6. Add colored output and progress - -// TODO: Implement CLI tool -// ``` -// use clap::{Parser, Subcommand}; -// use vectorless::client::{Engine, EngineBuilder}; -// -// #[derive(Parser)] -// #[command(name = "vectorless-cli")] -// struct Cli { -// #[command(subcommand)] -// command: Commands, -// } -// -// #[derive(Subcommand)] -// enum Commands { -// /// Index a document -// Index { -// /// Path to document -// path: PathBuf, -// }, -// /// Query an indexed document -// Query { -// /// Document ID -// doc_id: String, -// /// Query string -// query: String, -// }, -// /// List all indexed documents -// List, -// } -// -// #[tokio::main] -// async fn main() -> Result<()> { -// let cli = Cli::parse(); -// let engine = EngineBuilder::new().build()?; -// -// match cli.command { -// Commands::Index { path } => { -// let doc_id = engine.index(&path).await?; -// println!("Indexed: {}", doc_id); -// } -// Commands::Query { doc_id, query } => { -// let result = engine.query(&doc_id, &query).await?; -// println!("{}", result.content); -// } -// Commands::List => { -// // List documents -// } -// } -// -// Ok(()) -// } -// ``` - -fn main() { - // TODO: Implement full CLI tool - - println!("TODO: Implement cli_tool example"); -} diff --git a/examples/rust/indexing.rs b/examples/rust/indexing.rs new file mode 100644 index 00000000..d0a56595 --- /dev/null +++ b/examples/rust/indexing.rs @@ -0,0 +1,105 @@ +// Copyright (c) 2026 vectorless developers +// SPDX-License-Identifier: Apache-2.0 + +//! Index pipeline example for Vectorless. +//! +//! Demonstrates the full indexing flow: create engine → index document → inspect metrics. +//! +//! # Usage +//! +//! ```bash +//! cargo run --example indexing +//! ``` + +use vectorless::{EngineBuilder, IndexContext, IndexMode}; + +#[tokio::main] +async fn main() -> vectorless::Result<()> { + println!("=== Index Pipeline Example ===\n"); + + // 1. Create engine + let engine = EngineBuilder::new() + .with_workspace("./workspace_index_example") + .build() + .await + .map_err(|e: vectorless::BuildError| vectorless::Error::Config(e.to_string()))?; + + println!("Engine created\n"); + + // 2. Index a single document with default options + println!("--- Single document (default mode) ---"); + let result = engine + .index(IndexContext::from_path("./README.md")) + .await?; + + for item in &result.items { + println!(" doc_id: {}", item.doc_id); + println!(" name: {}", item.name); + println!(" format: {:?}", item.format); + + if let Some(ref metrics) = item.metrics { + println!(" metrics:"); + println!(" total time: {}ms", metrics.total_time_ms()); + println!(" parse: {}ms", metrics.parse_time_ms); + println!(" build: {}ms", metrics.build_time_ms); + println!(" enhance: {}ms", metrics.enhance_time_ms); + println!(" enrich: {}ms", metrics.enrich_time_ms); + println!(" optimize: {}ms", metrics.optimize_time_ms); + println!(" reasoning: {}ms", metrics.reasoning_index_time_ms); + println!(" nodes: {}", metrics.nodes_processed); + println!(" summaries: {}", metrics.summaries_generated); + println!(" llm calls: {}", metrics.llm_calls); + println!(" tokens: {}", metrics.total_tokens_generated); + println!(" topics: {}", metrics.topics_indexed); + println!(" keywords: {}", metrics.keywords_indexed); + } + + // doc_id preserved across the loop for readability + let _doc_id = item.doc_id.clone(); + + // 3. Re-index with incremental mode — should detect no change + println!("\n--- Re-index (incremental, unchanged) ---"); + let result2 = engine + .index( + IndexContext::from_path("./README.md") + .with_mode(IndexMode::Incremental), + ) + .await?; + + for item in &result2.items { + println!( + " {} (metrics present: {})", + item.doc_id, + item.metrics.is_some() + ); + } + + // 4. Index multiple documents at once + println!("\n--- Batch indexing ---"); + let batch = engine + .index(IndexContext::from_paths(&["./README.md", "./CLAUDE.md"])) + .await?; + + println!( + " indexed: {}, failed: {}", + batch.items.len(), + batch.failed.len() + ); + for item in &batch.items { + let time = item.metrics.as_ref().map(|m| m.total_time_ms()).unwrap_or(0); + let nodes = item.metrics.as_ref().map(|m| m.nodes_processed).unwrap_or(0); + println!(" {} — {}ms, {} nodes", item.name, time, nodes); + } + + // 5. Cleanup + println!("\n--- Cleanup ---"); + let docs = engine.list().await?; + for doc in &docs { + engine.remove(&doc.id).await?; + } + println!(" removed {} document(s)", docs.len()); + } + + println!("\n=== Done ==="); + Ok(()) +} diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 392fa018..40d3fc9c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -42,6 +42,10 @@ path = "../examples/rust/markdownflow.rs" name = "graph" path = "../examples/rust/graph.rs" +[[example]] +name = "indexing" +path = "../examples/rust/indexing.rs" + [dependencies] # Async runtime tokio = { workspace = true } diff --git a/rust/src/client/builder.rs b/rust/src/client/builder.rs index 91095da2..2dffe492 100644 --- a/rust/src/client/builder.rs +++ b/rust/src/client/builder.rs @@ -596,7 +596,8 @@ impl EngineBuilder { } // Build engine - Engine::with_components(config, workspace, retriever, indexer) + let events = self.events.unwrap_or_default(); + Engine::with_components(config, workspace, retriever, indexer, events) .await .map_err(|e| BuildError::Other(e.to_string())) } diff --git a/rust/src/client/engine.rs b/rust/src/client/engine.rs index 3989481b..bcf551b2 100644 --- a/rust/src/client/engine.rs +++ b/rust/src/client/engine.rs @@ -99,9 +99,9 @@ impl Engine { workspace: Workspace, retriever: PipelineRetriever, indexer: IndexerClient, + events: EventEmitter, ) -> Result { let config = Arc::new(config); - let events = EventEmitter::new(); // Attach event emitter to indexer let indexer = indexer.with_events(events.clone()); diff --git a/rust/src/client/events.rs b/rust/src/client/events.rs index 4681b45b..433498ee 100644 --- a/rust/src/client/events.rs +++ b/rust/src/client/events.rs @@ -1,365 +1,7 @@ // Copyright (c) 2026 vectorless developers // SPDX-License-Identifier: Apache-2.0 -//! Event system for client operations. -//! -//! This module provides event types and handlers for observing -//! and reacting to client operations (indexing, querying, etc.). -//! -//! # Example -//! -//! ```rust,ignore -//! let emitter = EventEmitter::new() -//! .on_index(|e| match e { -//! IndexEvent::Complete { doc_id } => println!("Indexed: {}", doc_id), -//! _ => {} -//! }); -//! -//! let client = EngineBuilder::new() -//! .with_events(emitter) -//! .build()?; -//! ``` +//! Re-export shim — event types and emitter live in the top-level +//! [`events`](crate::events) module. -use std::sync::Arc; - -use async_trait::async_trait; -use tracing::info; - -use crate::parser::DocumentFormat; -use crate::retrieval::SufficiencyLevel; - -/// Event types for client operations. -#[derive(Debug, Clone)] -pub enum Event { - /// Indexing events. - Index(IndexEvent), - - /// Query events. - Query(QueryEvent), - - /// Workspace events. - Workspace(WorkspaceEvent), -} - -/// Indexing operation events. -#[derive(Debug, Clone)] -pub enum IndexEvent { - /// Started indexing a document. - Started { - /// File path being indexed. - path: String, - }, - - /// Document format detected. - FormatDetected { - /// Detected format. - format: DocumentFormat, - }, - - /// Parsing progress update. - ParsingProgress { - /// Percentage complete (0-100). - percent: u8, - }, - - /// Document tree built. - TreeBuilt { - /// Number of nodes in the tree. - node_count: usize, - }, - - /// Summary generation progress. - SummaryProgress { - /// Number of summaries completed. - completed: usize, - /// Total summaries to generate. - total: usize, - }, - - /// Indexing completed successfully. - Complete { - /// Generated document ID. - doc_id: String, - }, - - /// Error occurred during indexing. - Error { - /// Error message. - message: String, - }, -} - -/// Query operation events. -#[derive(Debug, Clone)] -pub enum QueryEvent { - /// Search started. - Started { - /// The query string. - query: String, - }, - - /// Node visited during search. - NodeVisited { - /// Node ID. - node_id: String, - /// Node title. - title: String, - /// Relevance score. - score: f32, - }, - - /// Candidate result found. - CandidateFound { - /// Node ID. - node_id: String, - /// Relevance score. - score: f32, - }, - - /// Sufficiency check result. - SufficiencyCheck { - /// Sufficiency level. - level: SufficiencyLevel, - /// Total tokens collected. - tokens: usize, - }, - - /// Query completed. - Complete { - /// Total results found. - total_results: usize, - /// Overall confidence score. - confidence: f32, - }, - - /// Error occurred during query. - Error { - /// Error message. - message: String, - }, -} - -/// Workspace operation events. -#[derive(Debug, Clone)] -pub enum WorkspaceEvent { - /// Document saved to workspace. - Saved { - /// Document ID. - doc_id: String, - }, - - /// Document loaded from workspace. - Loaded { - /// Document ID. - doc_id: String, - /// Whether it was a cache hit. - cache_hit: bool, - }, - - /// Document removed from workspace. - Removed { - /// Document ID. - doc_id: String, - }, - - /// Workspace cleared. - Cleared { - /// Number of documents removed. - count: usize, - }, -} - -/// Sync event handler trait. -pub(crate) trait EventHandler: Send + Sync { - /// Handle an event. - fn handle(&self, event: &Event); -} - -/// Async event handler trait. -#[async_trait] -pub(crate) trait AsyncEventHandler: Send + Sync { - /// Handle an event asynchronously. - async fn handle(&self, event: &Event); -} - -/// Type alias for sync index handler. -pub(crate) type IndexHandler = Box; - -/// Type alias for sync query handler. -pub(crate) type QueryHandler = Box; - -/// Type alias for sync workspace handler. -pub(crate) type WorkspaceHandler = Box; - -/// Event emitter for client operations. -/// -/// Collects event handlers and dispatches events to them. -#[derive(Default)] -pub struct EventEmitter { - /// Index event handlers. - index_handlers: Vec, - - /// Query event handlers. - query_handlers: Vec, - - /// Workspace event handlers. - workspace_handlers: Vec, - - /// Async handlers. - async_handlers: Vec>, -} - -impl EventEmitter { - /// Create a new event emitter with no handlers. - pub fn new() -> Self { - Self::default() - } - - /// Add an index event handler. - pub fn on_index(mut self, handler: F) -> Self - where - F: Fn(&IndexEvent) + Send + Sync + 'static, - { - self.index_handlers.push(Box::new(handler)); - self - } - - /// Add a query event handler. - pub fn on_query(mut self, handler: F) -> Self - where - F: Fn(&QueryEvent) + Send + Sync + 'static, - { - self.query_handlers.push(Box::new(handler)); - self - } - - /// Add a workspace event handler. - pub fn on_workspace(mut self, handler: F) -> Self - where - F: Fn(&WorkspaceEvent) + Send + Sync + 'static, - { - self.workspace_handlers.push(Box::new(handler)); - self - } - - /// Add an async event handler. - pub(crate) fn with_async_handler(mut self, handler: Arc) -> Self - where - H: AsyncEventHandler + 'static, - { - self.async_handlers.push(handler); - self - } - - /// Emit an index event. - pub fn emit_index(&self, event: IndexEvent) { - for handler in &self.index_handlers { - handler(&event); - } - for handler in &self.async_handlers { - // For sync context, we just log async handlers - let event = Event::Index(event.clone()); - info!("Async event: {:?}", event); - } - } - - /// Emit a query event. - pub fn emit_query(&self, event: QueryEvent) { - for handler in &self.query_handlers { - handler(&event); - } - } - - /// Emit a workspace event. - pub fn emit_workspace(&self, event: WorkspaceEvent) { - for handler in &self.workspace_handlers { - handler(&event); - } - } - - /// Check if there are any handlers registered. - pub fn has_handlers(&self) -> bool { - !self.index_handlers.is_empty() - || !self.query_handlers.is_empty() - || !self.workspace_handlers.is_empty() - || !self.async_handlers.is_empty() - } - - /// Merge another emitter into this one. - pub fn merge(mut self, other: EventEmitter) -> Self { - self.index_handlers.extend(other.index_handlers); - self.query_handlers.extend(other.query_handlers); - self.workspace_handlers.extend(other.workspace_handlers); - self.async_handlers.extend(other.async_handlers); - self - } -} - -impl std::fmt::Debug for EventEmitter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EventEmitter") - .field("index_handlers", &self.index_handlers.len()) - .field("query_handlers", &self.query_handlers.len()) - .field("workspace_handlers", &self.workspace_handlers.len()) - .field("async_handlers", &self.async_handlers.len()) - .finish() - } -} - -impl Clone for EventEmitter { - fn clone(&self) -> Self { - // Clone returns an empty emitter since we can't clone closures - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - - #[test] - fn test_event_emitter_index() { - let counter = Arc::new(AtomicUsize::new(0)); - let counter_clone = counter.clone(); - - let emitter = EventEmitter::new().on_index(move |_e| { - counter_clone.fetch_add(1, Ordering::SeqCst); - }); - - emitter.emit_index(IndexEvent::Started { - path: "test.md".to_string(), - }); - emitter.emit_index(IndexEvent::Complete { - doc_id: "123".to_string(), - }); - - assert_eq!(counter.load(Ordering::SeqCst), 2); - } - - #[test] - fn test_event_emitter_query() { - let counter = Arc::new(AtomicUsize::new(0)); - let counter_clone = counter.clone(); - - let emitter = EventEmitter::new().on_query(move |_e| { - counter_clone.fetch_add(1, Ordering::SeqCst); - }); - - emitter.emit_query(QueryEvent::Started { - query: "test".to_string(), - }); - - assert_eq!(counter.load(Ordering::SeqCst), 1); - } - - #[test] - fn test_event_emitter_has_handlers() { - let empty = EventEmitter::new(); - assert!(!empty.has_handlers()); - - let with_handler = EventEmitter::new().on_index(|_| {}); - assert!(with_handler.has_handlers()); - } -} +pub use crate::events::{Event, EventEmitter, IndexEvent, QueryEvent, WorkspaceEvent}; diff --git a/rust/src/events/emitter.rs b/rust/src/events/emitter.rs new file mode 100644 index 00000000..a281d18b --- /dev/null +++ b/rust/src/events/emitter.rs @@ -0,0 +1,288 @@ +// Copyright (c) 2026 vectorless developers +// SPDX-License-Identifier: Apache-2.0 + +//! Event emitter for client operations. +//! +//! Collects event handlers and dispatches events to them. +//! Uses `Arc>` so cloning shares handlers instead of losing them. + +use std::sync::Arc; + +use async_trait::async_trait; +use parking_lot::RwLock; +use tracing::info; + +use super::types::{Event, IndexEvent, QueryEvent, WorkspaceEvent}; + +/// Sync event handler trait. +pub(crate) trait EventHandler: Send + Sync { + /// Handle an event. + fn handle(&self, event: &Event); +} + +/// Async event handler trait. +#[async_trait] +pub(crate) trait AsyncEventHandler: Send + Sync { + /// Handle an event asynchronously. + async fn handle(&self, event: &Event); +} + +/// Type alias for sync index handler. +pub(crate) type IndexHandler = Box; + +/// Type alias for sync query handler. +pub(crate) type QueryHandler = Box; + +/// Type alias for sync workspace handler. +pub(crate) type WorkspaceHandler = Box; + +/// Inner state shared via `Arc>`. +struct EventEmitterInner { + /// Index event handlers. + index_handlers: Vec, + + /// Query event handlers. + query_handlers: Vec, + + /// Workspace event handlers. + workspace_handlers: Vec, + + /// Async handlers. + async_handlers: Vec>, +} + +impl Default for EventEmitterInner { + fn default() -> Self { + Self { + index_handlers: Vec::new(), + query_handlers: Vec::new(), + workspace_handlers: Vec::new(), + async_handlers: Vec::new(), + } + } +} + +/// Event emitter for client operations. +/// +/// Collects event handlers and dispatches events to them. +/// Cloning shares the same handlers (via `Arc`), so all clones +/// dispatch to the same registered handlers. +/// +/// # Example +/// +/// ```rust,ignore +/// let emitter = EventEmitter::new() +/// .on_index(|e| match e { +/// IndexEvent::Complete { doc_id } => println!("Indexed: {}", doc_id), +/// _ => {} +/// }); +/// +/// let clone = emitter.clone(); +/// // clone shares the same handlers — emitting on either fires on both +/// ``` +pub struct EventEmitter { + inner: Arc>, +} + +impl EventEmitter { + /// Create a new event emitter with no handlers. + pub fn new() -> Self { + Self::default() + } + + /// Add an index event handler. + pub fn on_index(self, handler: F) -> Self + where + F: Fn(&IndexEvent) + Send + Sync + 'static, + { + self.inner.write().index_handlers.push(Box::new(handler)); + self + } + + /// Add a query event handler. + pub fn on_query(self, handler: F) -> Self + where + F: Fn(&QueryEvent) + Send + Sync + 'static, + { + self.inner.write().query_handlers.push(Box::new(handler)); + self + } + + /// Add a workspace event handler. + pub fn on_workspace(self, handler: F) -> Self + where + F: Fn(&WorkspaceEvent) + Send + Sync + 'static, + { + self.inner + .write() + .workspace_handlers + .push(Box::new(handler)); + self + } + + /// Add an async event handler. + pub(crate) fn with_async_handler(self, handler: Arc) -> Self + where + H: AsyncEventHandler + 'static, + { + self.inner.write().async_handlers.push(handler); + self + } + + /// Emit an index event. + pub fn emit_index(&self, event: IndexEvent) { + let inner = self.inner.read(); + for handler in &inner.index_handlers { + handler(&event); + } + for handler in &inner.async_handlers { + // For sync context, we just log async handlers + let event = Event::Index(event.clone()); + info!("Async event: {:?}", event); + } + } + + /// Emit a query event. + pub fn emit_query(&self, event: QueryEvent) { + let inner = self.inner.read(); + for handler in &inner.query_handlers { + handler(&event); + } + } + + /// Emit a workspace event. + pub fn emit_workspace(&self, event: WorkspaceEvent) { + let inner = self.inner.read(); + for handler in &inner.workspace_handlers { + handler(&event); + } + } + + /// Check if there are any handlers registered. + pub fn has_handlers(&self) -> bool { + let inner = self.inner.read(); + !inner.index_handlers.is_empty() + || !inner.query_handlers.is_empty() + || !inner.workspace_handlers.is_empty() + || !inner.async_handlers.is_empty() + } + + /// Merge another emitter into this one. + pub fn merge(self, other: EventEmitter) -> Self { + let mut other_inner = other.inner.write(); + let mut inner = self.inner.write(); + inner.index_handlers.extend(other_inner.index_handlers.drain(..)); + inner.query_handlers.extend(other_inner.query_handlers.drain(..)); + inner + .workspace_handlers + .extend(other_inner.workspace_handlers.drain(..)); + inner.async_handlers.extend(other_inner.async_handlers.drain(..)); + drop(inner); + drop(other_inner); + self + } +} + +impl Default for EventEmitter { + fn default() -> Self { + Self { + inner: Arc::new(RwLock::new(EventEmitterInner::default())), + } + } +} + +impl Clone for EventEmitter { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl std::fmt::Debug for EventEmitter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = self.inner.read(); + f.debug_struct("EventEmitter") + .field("index_handlers", &inner.index_handlers.len()) + .field("query_handlers", &inner.query_handlers.len()) + .field("workspace_handlers", &inner.workspace_handlers.len()) + .field("async_handlers", &inner.async_handlers.len()) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + #[test] + fn test_event_emitter_index() { + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + + let emitter = EventEmitter::new().on_index(move |_e| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }); + + emitter.emit_index(IndexEvent::Started { + path: "test.md".to_string(), + }); + emitter.emit_index(IndexEvent::Complete { + doc_id: "123".to_string(), + }); + + assert_eq!(counter.load(Ordering::SeqCst), 2); + } + + #[test] + fn test_event_emitter_query() { + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + + let emitter = EventEmitter::new().on_query(move |_e| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }); + + emitter.emit_query(QueryEvent::Started { + query: "test".to_string(), + }); + + assert_eq!(counter.load(Ordering::SeqCst), 1); + } + + #[test] + fn test_event_emitter_has_handlers() { + let empty = EventEmitter::new(); + assert!(!empty.has_handlers()); + + let with_handler = EventEmitter::new().on_index(|_| {}); + assert!(with_handler.has_handlers()); + } + + #[test] + fn test_event_emitter_clone_shares_handlers() { + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + + let emitter = EventEmitter::new().on_index(move |_e| { + counter_clone.fetch_add(1, Ordering::SeqCst); + }); + + let cloned = emitter.clone(); + + // Emit on the clone — original's handler should fire + cloned.emit_index(IndexEvent::Started { + path: "test.md".to_string(), + }); + + assert_eq!(counter.load(Ordering::SeqCst), 1); + + // Emit on the original too + emitter.emit_index(IndexEvent::Complete { + doc_id: "123".to_string(), + }); + + assert_eq!(counter.load(Ordering::SeqCst), 2); + } +} diff --git a/rust/src/events/mod.rs b/rust/src/events/mod.rs new file mode 100644 index 00000000..8e2b1526 --- /dev/null +++ b/rust/src/events/mod.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2026 vectorless developers +// SPDX-License-Identifier: Apache-2.0 + +//! Event system for observing and reacting to client operations. +//! +//! This module provides event types and the [`EventEmitter`] for +//! registering handlers and dispatching events during indexing, +//! querying, and workspace operations. +//! +//! # Example +//! +//! ```rust,ignore +//! use vectorless::events::{EventEmitter, IndexEvent}; +//! +//! let emitter = EventEmitter::new() +//! .on_index(|e| match e { +//! IndexEvent::Complete { doc_id } => println!("Indexed: {}", doc_id), +//! _ => {} +//! }); +//! +//! let client = EngineBuilder::new() +//! .with_events(emitter) +//! .build() +//! .await?; +//! ``` + +mod emitter; +mod types; + +pub use emitter::EventEmitter; +pub use types::{Event, IndexEvent, QueryEvent, WorkspaceEvent}; + +// Re-export handler traits for internal use +pub(crate) use emitter::{AsyncEventHandler, EventHandler}; diff --git a/rust/src/events/types.rs b/rust/src/events/types.rs new file mode 100644 index 00000000..7c8e58ce --- /dev/null +++ b/rust/src/events/types.rs @@ -0,0 +1,151 @@ +// Copyright (c) 2026 vectorless developers +// SPDX-License-Identifier: Apache-2.0 + +//! Event types for client operations. +//! +//! Provides enums for indexing, query, and workspace events +//! that can be observed via [`EventEmitter`](super::EventEmitter). + +use crate::parser::DocumentFormat; +use crate::retrieval::SufficiencyLevel; + +/// Top-level event types for client operations. +#[derive(Debug, Clone)] +pub enum Event { + /// Indexing events. + Index(IndexEvent), + + /// Query events. + Query(QueryEvent), + + /// Workspace events. + Workspace(WorkspaceEvent), +} + +/// Indexing operation events. +#[derive(Debug, Clone)] +pub enum IndexEvent { + /// Started indexing a document. + Started { + /// File path being indexed. + path: String, + }, + + /// Document format detected. + FormatDetected { + /// Detected format. + format: DocumentFormat, + }, + + /// Parsing progress update. + ParsingProgress { + /// Percentage complete (0-100). + percent: u8, + }, + + /// Document tree built. + TreeBuilt { + /// Number of nodes in the tree. + node_count: usize, + }, + + /// Summary generation progress. + SummaryProgress { + /// Number of summaries completed. + completed: usize, + /// Total summaries to generate. + total: usize, + }, + + /// Indexing completed successfully. + Complete { + /// Generated document ID. + doc_id: String, + }, + + /// Error occurred during indexing. + Error { + /// Error message. + message: String, + }, +} + +/// Query operation events. +#[derive(Debug, Clone)] +pub enum QueryEvent { + /// Search started. + Started { + /// The query string. + query: String, + }, + + /// Node visited during search. + NodeVisited { + /// Node ID. + node_id: String, + /// Node title. + title: String, + /// Relevance score. + score: f32, + }, + + /// Candidate result found. + CandidateFound { + /// Node ID. + node_id: String, + /// Relevance score. + score: f32, + }, + + /// Sufficiency check result. + SufficiencyCheck { + /// Sufficiency level. + level: SufficiencyLevel, + /// Total tokens collected. + tokens: usize, + }, + + /// Query completed. + Complete { + /// Total results found. + total_results: usize, + /// Overall confidence score. + confidence: f32, + }, + + /// Error occurred during query. + Error { + /// Error message. + message: String, + }, +} + +/// Workspace operation events. +#[derive(Debug, Clone)] +pub enum WorkspaceEvent { + /// Document saved to workspace. + Saved { + /// Document ID. + doc_id: String, + }, + + /// Document loaded from workspace. + Loaded { + /// Document ID. + doc_id: String, + /// Whether it was a cache hit. + cache_hit: bool, + }, + + /// Document removed from workspace. + Removed { + /// Document ID. + doc_id: String, + }, + + /// Workspace cleared. + Cleared { + /// Number of documents removed. + count: usize, + }, +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 950d64b5..3e13344e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -56,6 +56,7 @@ pub mod client; mod config; pub mod document; pub mod error; +pub mod events; mod graph; mod index; mod llm; @@ -69,7 +70,7 @@ mod utils; // Client API pub use client::{ - BuildError, ClientError, DocumentFormat, DocumentInfo, Engine, EngineBuilder, EventEmitter, + BuildError, ClientError, DocumentFormat, DocumentInfo, Engine, EngineBuilder, IndexContext, IndexItem, IndexMode, IndexOptions, IndexResult, QueryContext, QueryResult, }; @@ -85,6 +86,9 @@ pub use document::{ // Graph types pub use graph::DocumentGraph; +// Event types +pub use events::{EventEmitter, IndexEvent, QueryEvent, WorkspaceEvent}; + // Index metrics pub use metrics::IndexMetrics; From 077cae1591e1a5fa2960f76e9d51cb2fd2875c04 Mon Sep 17 00:00:00 2001 From: zTgx <747674262@qq.com> Date: Sat, 11 Apr 2026 13:04:17 +0800 Subject: [PATCH 2/2] refactor(examples/rust): update event module imports - Move EventEmitter, IndexEvent, and QueryEvent imports from vectorless::client::events to vectorless::events - Remove redundant EventEmitter import from client module --- examples/rust/events.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/rust/events.rs b/examples/rust/events.rs index 4dc558d1..2941ebc5 100644 --- a/examples/rust/events.rs +++ b/examples/rust/events.rs @@ -17,8 +17,8 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; -use vectorless::client::{EngineBuilder, EventEmitter, IndexContext, QueryContext}; -use vectorless::client::events::{IndexEvent, QueryEvent}; +use vectorless::client::{EngineBuilder, IndexContext, QueryContext}; +use vectorless::events::{EventEmitter, IndexEvent, QueryEvent}; #[tokio::main] async fn main() -> Result<(), Box> {