Skip to main content

graphrag_core/builder/
mod.rs

1//! GraphRAG builder module
2//!
3//! This module provides builder patterns for constructing GraphRAG instances.
4//!
5//! ## Two Builder Options
6//!
7//! ### 1. Simple Builder (GraphRAGBuilder) - Flexible, runtime validation
8//! ```no_run
9//! use graphrag_core::builder::GraphRAGBuilder;
10//!
11//! # fn example() -> graphrag_core::Result<()> {
12//! let graphrag = GraphRAGBuilder::new()
13//!     .with_output_dir("./my_output")
14//!     .with_chunk_size(512)
15//!     .build()?;
16//! # Ok(())
17//! # }
18//! ```
19//!
20//! ### 2. Typed Builder (TypedBuilder) - Compile-time validation
21//! ```no_run
22//! use graphrag_core::builder::TypedBuilder;
23//!
24//! # fn example() -> graphrag_core::Result<()> {
25//! // This won't compile until you configure required settings!
26//! let graphrag = TypedBuilder::new()
27//!     .with_output_dir("./my_output")  // Required - transitions state
28//!     .with_ollama()                    // Required - transitions state
29//!     .with_chunk_size(512)             // Optional
30//!     .build()?;                        // Only available when properly configured
31//! # Ok(())
32//! # }
33//! ```
34
35use crate::config::Config;
36use crate::core::Result;
37use std::marker::PhantomData;
38
39// ============================================================================
40// TYPE-STATE BUILDER - Compile-time validation
41// ============================================================================
42
43/// Marker: Output directory not configured
44pub struct NoOutput;
45/// Marker: Output directory is configured
46pub struct HasOutput;
47
48/// Marker: LLM/embedding not configured
49pub struct NoLlm;
50/// Marker: LLM/embedding is configured (Ollama, hash, or other)
51pub struct HasLlm;
52
53/// Typed builder with compile-time validation
54///
55/// Uses Rust's type system to ensure required configuration is set before building.
56/// The builder transitions through states as you configure it:
57///
58/// ```text
59/// TypedBuilder<NoOutput, NoLlm>  -- with_output_dir() -->  TypedBuilder<HasOutput, NoLlm>
60/// TypedBuilder<HasOutput, NoLlm> -- with_ollama()     -->  TypedBuilder<HasOutput, HasLlm>
61/// TypedBuilder<HasOutput, HasLlm> can call .build()
62/// ```
63///
64/// # Example
65/// ```no_run
66/// use graphrag_core::builder::TypedBuilder;
67///
68/// # fn example() -> graphrag_core::Result<()> {
69/// // Configure required settings to unlock build()
70/// let graphrag = TypedBuilder::new()
71///     .with_output_dir("./output")
72///     .with_ollama()  // or .with_hash_embeddings()
73///     .with_chunk_size(512)
74///     .build()?;
75/// # Ok(())
76/// # }
77/// ```
78#[derive(Debug)]
79pub struct TypedBuilder<Output = NoOutput, Llm = NoLlm> {
80    config: Config,
81    _output: PhantomData<Output>,
82    _llm: PhantomData<Llm>,
83}
84
85impl TypedBuilder<NoOutput, NoLlm> {
86    /// Create a new typed builder
87    ///
88    /// Starts in unconfigured state - you must call:
89    /// - `with_output_dir()` to set the output directory
90    /// - `with_ollama()` or `with_hash_embeddings()` to configure LLM/embeddings
91    pub fn new() -> Self {
92        Self {
93            config: Config::default(),
94            _output: PhantomData,
95            _llm: PhantomData,
96        }
97    }
98}
99
100impl Default for TypedBuilder<NoOutput, NoLlm> {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106// Output directory configuration - transitions NoOutput -> HasOutput
107impl<Llm> TypedBuilder<NoOutput, Llm> {
108    /// Set the output directory (required)
109    ///
110    /// This transitions the builder to the `HasOutput` state.
111    pub fn with_output_dir(mut self, dir: &str) -> TypedBuilder<HasOutput, Llm> {
112        self.config.output_dir = dir.to_string();
113        TypedBuilder {
114            config: self.config,
115            _output: PhantomData,
116            _llm: PhantomData,
117        }
118    }
119}
120
121// LLM configuration - transitions NoLlm -> HasLlm
122impl<Output> TypedBuilder<Output, NoLlm> {
123    /// Configure for Ollama LLM (enables semantic extraction)
124    ///
125    /// This transitions the builder to the `HasLlm` state.
126    /// Sets up Ollama with default localhost:11434 configuration.
127    pub fn with_ollama(mut self) -> TypedBuilder<Output, HasLlm> {
128        self.config.ollama.enabled = true;
129        self.config.ollama.host = "localhost".to_string();
130        self.config.ollama.port = 11434;
131        self.config.embeddings.backend = "ollama".to_string();
132        TypedBuilder {
133            config: self.config,
134            _output: PhantomData,
135            _llm: PhantomData,
136        }
137    }
138
139    /// Configure for Ollama with custom settings
140    pub fn with_ollama_custom(
141        mut self,
142        host: &str,
143        port: u16,
144        chat_model: &str,
145    ) -> TypedBuilder<Output, HasLlm> {
146        self.config.ollama.enabled = true;
147        self.config.ollama.host = host.to_string();
148        self.config.ollama.port = port;
149        self.config.ollama.chat_model = chat_model.to_string();
150        self.config.embeddings.backend = "ollama".to_string();
151        TypedBuilder {
152            config: self.config,
153            _output: PhantomData,
154            _llm: PhantomData,
155        }
156    }
157
158    /// Configure for hash-based embeddings (no LLM required, offline)
159    ///
160    /// This transitions the builder to the `HasLlm` state.
161    /// Uses deterministic hash embeddings - fast but less semantic understanding.
162    pub fn with_hash_embeddings(mut self) -> TypedBuilder<Output, HasLlm> {
163        self.config.ollama.enabled = false;
164        self.config.embeddings.backend = "hash".to_string();
165        self.config.approach = "algorithmic".to_string();
166        TypedBuilder {
167            config: self.config,
168            _output: PhantomData,
169            _llm: PhantomData,
170        }
171    }
172
173    /// Configure for Candle neural embeddings (local, no API needed)
174    pub fn with_candle_embeddings(mut self) -> TypedBuilder<Output, HasLlm> {
175        self.config.embeddings.backend = "candle".to_string();
176        TypedBuilder {
177            config: self.config,
178            _output: PhantomData,
179            _llm: PhantomData,
180        }
181    }
182}
183
184// Optional configuration - available in any state
185impl<Output, Llm> TypedBuilder<Output, Llm> {
186    /// Set the chunk size for text processing (optional)
187    pub fn with_chunk_size(mut self, size: usize) -> Self {
188        self.config.chunk_size = size;
189        self.config.text.chunk_size = size;
190        self
191    }
192
193    /// Set the chunk overlap (optional)
194    pub fn with_chunk_overlap(mut self, overlap: usize) -> Self {
195        self.config.chunk_overlap = overlap;
196        self.config.text.chunk_overlap = overlap;
197        self
198    }
199
200    /// Set the top-k results for retrieval (optional)
201    pub fn with_top_k(mut self, k: usize) -> Self {
202        self.config.top_k_results = Some(k);
203        self.config.retrieval.top_k = k;
204        self
205    }
206
207    /// Set the similarity threshold (optional)
208    pub fn with_similarity_threshold(mut self, threshold: f32) -> Self {
209        self.config.similarity_threshold = Some(threshold);
210        self.config.graph.similarity_threshold = threshold;
211        self
212    }
213
214    /// Set the pipeline approach (optional)
215    /// Options: "semantic", "algorithmic", "hybrid"
216    pub fn with_approach(mut self, approach: &str) -> Self {
217        self.config.approach = approach.to_string();
218        self
219    }
220
221    /// Enable/disable parallel processing (optional)
222    pub fn with_parallel(mut self, enabled: bool) -> Self {
223        self.config.parallel.enabled = enabled;
224        self
225    }
226
227    /// Enable entity gleaning with LLM (optional, requires Ollama)
228    pub fn with_gleaning(mut self, max_rounds: usize) -> Self {
229        self.config.entities.use_gleaning = true;
230        self.config.entities.max_gleaning_rounds = max_rounds;
231        self
232    }
233
234    /// Get a reference to the current configuration
235    pub fn config(&self) -> &Config {
236        &self.config
237    }
238}
239
240// Build method - only available when fully configured
241impl TypedBuilder<HasOutput, HasLlm> {
242    /// Build the GraphRAG instance
243    ///
244    /// This method is only available when both output directory and
245    /// LLM/embedding backend are configured.
246    ///
247    /// # Example
248    /// ```no_run
249    /// use graphrag_core::builder::TypedBuilder;
250    ///
251    /// # fn example() -> graphrag_core::Result<()> {
252    /// let graphrag = TypedBuilder::new()
253    ///     .with_output_dir("./output")
254    ///     .with_ollama()
255    ///     .build()?;
256    /// # Ok(())
257    /// # }
258    /// ```
259    pub fn build(self) -> Result<crate::GraphRAG> {
260        crate::GraphRAG::new(self.config)
261    }
262
263    /// Build and initialize the GraphRAG instance
264    ///
265    /// Equivalent to calling `build()?.initialize()?`
266    pub fn build_and_init(self) -> Result<crate::GraphRAG> {
267        let mut graphrag = crate::GraphRAG::new(self.config)?;
268        graphrag.initialize()?;
269        Ok(graphrag)
270    }
271}
272
273// ============================================================================
274// SIMPLE BUILDER - Runtime validation (backward compatible)
275// ============================================================================
276
277/// Builder for GraphRAG instances
278///
279/// Provides a fluent API for configuring GraphRAG with various options.
280#[derive(Debug, Clone)]
281pub struct GraphRAGBuilder {
282    config: Config,
283}
284
285impl Default for GraphRAGBuilder {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291impl GraphRAGBuilder {
292    /// Create a new builder with default configuration
293    ///
294    /// # Example
295    /// ```no_run
296    /// use graphrag_core::builder::GraphRAGBuilder;
297    ///
298    /// let builder = GraphRAGBuilder::new();
299    /// ```
300    pub fn new() -> Self {
301        Self {
302            config: Config::default(),
303        }
304    }
305
306    /// Set the output directory for storing graphs and data
307    ///
308    /// # Example
309    /// ```no_run
310    /// # use graphrag_core::builder::GraphRAGBuilder;
311    /// let builder = GraphRAGBuilder::new()
312    ///     .with_output_dir("./my_workspace");
313    /// ```
314    pub fn with_output_dir(mut self, dir: &str) -> Self {
315        self.config.output_dir = dir.to_string();
316        self
317    }
318
319    /// Set the chunk size for text processing
320    ///
321    /// # Example
322    /// ```no_run
323    /// # use graphrag_core::builder::GraphRAGBuilder;
324    /// let builder = GraphRAGBuilder::new()
325    ///     .with_chunk_size(512);
326    /// ```
327    pub fn with_chunk_size(mut self, size: usize) -> Self {
328        self.config.chunk_size = size;
329        self.config.text.chunk_size = size;
330        self
331    }
332
333    /// Set the chunk overlap for text processing
334    ///
335    /// # Example
336    /// ```no_run
337    /// # use graphrag_core::builder::GraphRAGBuilder;
338    /// let builder = GraphRAGBuilder::new()
339    ///     .with_chunk_overlap(50);
340    /// ```
341    pub fn with_chunk_overlap(mut self, overlap: usize) -> Self {
342        self.config.chunk_overlap = overlap;
343        self.config.text.chunk_overlap = overlap;
344        self
345    }
346
347    /// Set the embedding dimension
348    ///
349    /// # Example
350    /// ```no_run
351    /// # use graphrag_core::builder::GraphRAGBuilder;
352    /// let builder = GraphRAGBuilder::new()
353    ///     .with_embedding_dimension(384);
354    /// ```
355    pub fn with_embedding_dimension(mut self, dimension: usize) -> Self {
356        self.config.embeddings.dimension = dimension;
357        self
358    }
359
360    /// Set the embedding model name
361    ///
362    /// # Example
363    /// ```no_run
364    /// # use graphrag_core::builder::GraphRAGBuilder;
365    /// let builder = GraphRAGBuilder::new()
366    ///     .with_embedding_model("nomic-embed-text:latest");
367    /// ```
368    pub fn with_embedding_model(mut self, model: &str) -> Self {
369        self.config.embeddings.model = Some(model.to_string());
370        self
371    }
372
373    /// Set the embedding backend ("candle", "fastembed", "api", "hash")
374    ///
375    /// # Example
376    /// ```no_run
377    /// # use graphrag_core::builder::GraphRAGBuilder;
378    /// let builder = GraphRAGBuilder::new()
379    ///     .with_embedding_backend("candle");
380    /// ```
381    pub fn with_embedding_backend(mut self, backend: &str) -> Self {
382        self.config.embeddings.backend = backend.to_string();
383        self
384    }
385
386    /// Set the Ollama host
387    ///
388    /// # Example
389    /// ```no_run
390    /// # use graphrag_core::builder::GraphRAGBuilder;
391    /// let builder = GraphRAGBuilder::new()
392    ///     .with_ollama_host("localhost");
393    /// ```
394    pub fn with_ollama_host(mut self, host: &str) -> Self {
395        self.config.ollama.host = host.to_string();
396        self
397    }
398
399    /// Set the Ollama port
400    ///
401    /// # Example
402    /// ```no_run
403    /// # use graphrag_core::builder::GraphRAGBuilder;
404    /// let builder = GraphRAGBuilder::new()
405    ///     .with_ollama_port(11434);
406    /// ```
407    pub fn with_ollama_port(mut self, port: u16) -> Self {
408        self.config.ollama.port = port;
409        self
410    }
411
412    /// Enable Ollama integration
413    ///
414    /// # Example
415    /// ```no_run
416    /// # use graphrag_core::builder::GraphRAGBuilder;
417    /// let builder = GraphRAGBuilder::new()
418    ///     .with_ollama_enabled(true);
419    /// ```
420    pub fn with_ollama_enabled(mut self, enabled: bool) -> Self {
421        self.config.ollama.enabled = enabled;
422        self
423    }
424
425    /// Set the Ollama chat/generation model
426    ///
427    /// # Example
428    /// ```no_run
429    /// # use graphrag_core::builder::GraphRAGBuilder;
430    /// let builder = GraphRAGBuilder::new()
431    ///     .with_chat_model("llama3.2:latest");
432    /// ```
433    pub fn with_chat_model(mut self, model: &str) -> Self {
434        self.config.ollama.chat_model = model.to_string();
435        self
436    }
437
438    /// Set the Ollama embedding model
439    ///
440    /// # Example
441    /// ```no_run
442    /// # use graphrag_core::builder::GraphRAGBuilder;
443    /// let builder = GraphRAGBuilder::new()
444    ///     .with_ollama_embedding_model("nomic-embed-text:latest");
445    /// ```
446    pub fn with_ollama_embedding_model(mut self, model: &str) -> Self {
447        self.config.ollama.embedding_model = model.to_string();
448        self
449    }
450
451    /// Set the top-k results for retrieval
452    ///
453    /// # Example
454    /// ```no_run
455    /// # use graphrag_core::builder::GraphRAGBuilder;
456    /// let builder = GraphRAGBuilder::new()
457    ///     .with_top_k(10);
458    /// ```
459    pub fn with_top_k(mut self, k: usize) -> Self {
460        self.config.top_k_results = Some(k);
461        self.config.retrieval.top_k = k;
462        self
463    }
464
465    /// Set the similarity threshold for retrieval
466    ///
467    /// # Example
468    /// ```no_run
469    /// # use graphrag_core::builder::GraphRAGBuilder;
470    /// let builder = GraphRAGBuilder::new()
471    ///     .with_similarity_threshold(0.7);
472    /// ```
473    pub fn with_similarity_threshold(mut self, threshold: f32) -> Self {
474        self.config.similarity_threshold = Some(threshold);
475        self.config.graph.similarity_threshold = threshold;
476        self
477    }
478
479    /// Set the pipeline approach ("semantic", "algorithmic", or "hybrid")
480    ///
481    /// # Example
482    /// ```no_run
483    /// # use graphrag_core::builder::GraphRAGBuilder;
484    /// let builder = GraphRAGBuilder::new()
485    ///     .with_approach("hybrid");
486    /// ```
487    pub fn with_approach(mut self, approach: &str) -> Self {
488        self.config.approach = approach.to_string();
489        self
490    }
491
492    /// Enable or disable parallel processing
493    ///
494    /// # Example
495    /// ```no_run
496    /// # use graphrag_core::builder::GraphRAGBuilder;
497    /// let builder = GraphRAGBuilder::new()
498    ///     .with_parallel_processing(true);
499    /// ```
500    pub fn with_parallel_processing(mut self, enabled: bool) -> Self {
501        self.config.parallel.enabled = enabled;
502        self
503    }
504
505    /// Set the number of parallel threads
506    ///
507    /// # Example
508    /// ```no_run
509    /// # use graphrag_core::builder::GraphRAGBuilder;
510    /// let builder = GraphRAGBuilder::new()
511    ///     .with_num_threads(4);
512    /// ```
513    pub fn with_num_threads(mut self, num_threads: usize) -> Self {
514        self.config.parallel.num_threads = num_threads;
515        self
516    }
517
518    /// Enable or disable auto-save functionality
519    ///
520    /// # Example
521    /// ```no_run
522    /// # use graphrag_core::builder::GraphRAGBuilder;
523    /// let builder = GraphRAGBuilder::new()
524    ///     .with_auto_save(true, 300); // Save every 5 minutes
525    /// ```
526    pub fn with_auto_save(mut self, enabled: bool, interval_seconds: u64) -> Self {
527        self.config.auto_save.enabled = enabled;
528        self.config.auto_save.interval_seconds = interval_seconds;
529        self
530    }
531
532    /// Set the auto-save workspace name
533    ///
534    /// # Example
535    /// ```no_run
536    /// # use graphrag_core::builder::GraphRAGBuilder;
537    /// let builder = GraphRAGBuilder::new()
538    ///     .with_auto_save_workspace("my_project");
539    /// ```
540    pub fn with_auto_save_workspace(mut self, name: &str) -> Self {
541        self.config.auto_save.workspace_name = Some(name.to_string());
542        self
543    }
544
545    /// Configure for local zero-config setup using Ollama
546    ///
547    /// Sets up:
548    /// - Ollama enabled with localhost:11434
549    /// - Default models (nomic-embed-text for embeddings, llama3.2 for chat)
550    /// - Candle backend for local embeddings
551    ///
552    /// # Example
553    /// ```no_run
554    /// # use graphrag_core::builder::GraphRAGBuilder;
555    /// let builder = GraphRAGBuilder::new()
556    ///     .with_local_defaults();
557    /// ```
558    pub fn with_local_defaults(mut self) -> Self {
559        self.config.ollama.enabled = true;
560        self.config.ollama.host = "localhost".to_string();
561        self.config.ollama.port = 11434;
562        self.config.embeddings.backend = "candle".to_string();
563        self
564    }
565
566    /// Build the GraphRAG instance with the configured settings
567    ///
568    /// # Errors
569    /// Returns an error if the GraphRAG initialization fails
570    ///
571    /// # Example
572    /// ```no_run
573    /// # use graphrag_core::builder::GraphRAGBuilder;
574    /// # fn example() -> graphrag_core::Result<()> {
575    /// let graphrag = GraphRAGBuilder::new()
576    ///     .with_output_dir("./workspace")
577    ///     .build()?;
578    /// # Ok(())
579    /// # }
580    /// ```
581    pub fn build(self) -> Result<crate::GraphRAG> {
582        crate::GraphRAG::new(self.config)
583    }
584
585    /// Get a reference to the current configuration
586    ///
587    /// Useful for inspecting the configuration before building
588    pub fn config(&self) -> &Config {
589        &self.config
590    }
591
592    /// Get a mutable reference to the current configuration
593    ///
594    /// Allows direct manipulation of the config for advanced use cases
595    pub fn config_mut(&mut self) -> &mut Config {
596        &mut self.config
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603
604    #[test]
605    fn test_builder_default() {
606        let builder = GraphRAGBuilder::new();
607        assert_eq!(builder.config().output_dir, "./output");
608    }
609
610    #[test]
611    fn test_builder_with_output_dir() {
612        let builder = GraphRAGBuilder::new().with_output_dir("./custom");
613        assert_eq!(builder.config().output_dir, "./custom");
614    }
615
616    #[test]
617    fn test_builder_with_chunk_size() {
618        let builder = GraphRAGBuilder::new().with_chunk_size(512);
619        assert_eq!(builder.config().chunk_size, 512);
620        assert_eq!(builder.config().text.chunk_size, 512);
621    }
622
623    #[test]
624    fn test_builder_with_embedding_config() {
625        let builder = GraphRAGBuilder::new()
626            .with_embedding_dimension(384)
627            .with_embedding_model("test-model");
628
629        assert_eq!(builder.config().embeddings.dimension, 384);
630        assert_eq!(
631            builder.config().embeddings.model,
632            Some("test-model".to_string())
633        );
634    }
635
636    #[test]
637    fn test_builder_with_ollama() {
638        let builder = GraphRAGBuilder::new()
639            .with_ollama_enabled(true)
640            .with_ollama_host("custom-host")
641            .with_ollama_port(8080)
642            .with_chat_model("custom-model");
643
644        assert!(builder.config().ollama.enabled);
645        assert_eq!(builder.config().ollama.host, "custom-host");
646        assert_eq!(builder.config().ollama.port, 8080);
647        assert_eq!(builder.config().ollama.chat_model, "custom-model");
648    }
649
650    #[test]
651    fn test_builder_with_retrieval() {
652        let builder = GraphRAGBuilder::new()
653            .with_top_k(20)
654            .with_similarity_threshold(0.8);
655
656        assert_eq!(builder.config().top_k_results, Some(20));
657        assert_eq!(builder.config().retrieval.top_k, 20);
658        assert_eq!(builder.config().similarity_threshold, Some(0.8));
659        assert_eq!(builder.config().graph.similarity_threshold, 0.8);
660    }
661
662    #[test]
663    fn test_builder_with_parallel() {
664        let builder = GraphRAGBuilder::new()
665            .with_parallel_processing(false)
666            .with_num_threads(8);
667
668        assert!(!builder.config().parallel.enabled);
669        assert_eq!(builder.config().parallel.num_threads, 8);
670    }
671
672    #[test]
673    fn test_builder_with_auto_save() {
674        let builder = GraphRAGBuilder::new()
675            .with_auto_save(true, 600)
676            .with_auto_save_workspace("test");
677
678        assert!(builder.config().auto_save.enabled);
679        assert_eq!(builder.config().auto_save.interval_seconds, 600);
680        assert_eq!(
681            builder.config().auto_save.workspace_name,
682            Some("test".to_string())
683        );
684    }
685
686    #[test]
687    fn test_builder_local_defaults() {
688        let builder = GraphRAGBuilder::new().with_local_defaults();
689
690        assert!(builder.config().ollama.enabled);
691        assert_eq!(builder.config().ollama.host, "localhost");
692        assert_eq!(builder.config().ollama.port, 11434);
693        assert_eq!(builder.config().embeddings.backend, "candle");
694    }
695
696    #[test]
697    fn test_builder_fluent_api() {
698        let builder = GraphRAGBuilder::new()
699            .with_output_dir("./test")
700            .with_chunk_size(256)
701            .with_chunk_overlap(32)
702            .with_top_k(15)
703            .with_approach("hybrid");
704
705        assert_eq!(builder.config().output_dir, "./test");
706        assert_eq!(builder.config().chunk_size, 256);
707        assert_eq!(builder.config().chunk_overlap, 32);
708        assert_eq!(builder.config().top_k_results, Some(15));
709        assert_eq!(builder.config().approach, "hybrid");
710    }
711
712    // ============================================================================
713    // TypedBuilder Tests - Compile-time validation
714    // ============================================================================
715
716    #[test]
717    fn test_typed_builder_state_transitions() {
718        // Start unconfigured
719        let builder = TypedBuilder::new();
720
721        // Add output dir - transitions to HasOutput
722        let builder = builder.with_output_dir("./test_output");
723        assert_eq!(builder.config().output_dir, "./test_output");
724
725        // Add Ollama - transitions to HasLlm
726        let builder = builder.with_ollama();
727        assert!(builder.config().ollama.enabled);
728        assert_eq!(builder.config().ollama.host, "localhost");
729        assert_eq!(builder.config().ollama.port, 11434);
730    }
731
732    #[test]
733    fn test_typed_builder_with_hash_embeddings() {
734        let builder = TypedBuilder::new()
735            .with_output_dir("./test")
736            .with_hash_embeddings();
737
738        assert!(!builder.config().ollama.enabled);
739        assert_eq!(builder.config().embeddings.backend, "hash");
740        assert_eq!(builder.config().approach, "algorithmic");
741    }
742
743    #[test]
744    fn test_typed_builder_with_ollama_custom() {
745        let builder = TypedBuilder::new()
746            .with_output_dir("./test")
747            .with_ollama_custom("my-server", 8080, "mistral:latest");
748
749        assert!(builder.config().ollama.enabled);
750        assert_eq!(builder.config().ollama.host, "my-server");
751        assert_eq!(builder.config().ollama.port, 8080);
752        assert_eq!(builder.config().ollama.chat_model, "mistral:latest");
753    }
754
755    #[test]
756    fn test_typed_builder_with_candle() {
757        let builder = TypedBuilder::new()
758            .with_output_dir("./test")
759            .with_candle_embeddings();
760
761        assert_eq!(builder.config().embeddings.backend, "candle");
762    }
763
764    #[test]
765    fn test_typed_builder_optional_methods() {
766        let builder = TypedBuilder::new()
767            .with_chunk_size(512)
768            .with_chunk_overlap(64)
769            .with_top_k(20)
770            .with_similarity_threshold(0.75)
771            .with_approach("hybrid")
772            .with_parallel(true)
773            .with_gleaning(3);
774
775        assert_eq!(builder.config().chunk_size, 512);
776        assert_eq!(builder.config().chunk_overlap, 64);
777        assert_eq!(builder.config().top_k_results, Some(20));
778        assert_eq!(builder.config().similarity_threshold, Some(0.75));
779        assert_eq!(builder.config().approach, "hybrid");
780        assert!(builder.config().parallel.enabled);
781        assert!(builder.config().entities.use_gleaning);
782        assert_eq!(builder.config().entities.max_gleaning_rounds, 3);
783    }
784
785    #[test]
786    fn test_typed_builder_order_independence() {
787        // Test that optional methods can be called in any order before required ones
788        let builder1 = TypedBuilder::new()
789            .with_chunk_size(512)
790            .with_output_dir("./test1")
791            .with_ollama();
792
793        let builder2 = TypedBuilder::new()
794            .with_output_dir("./test2")
795            .with_chunk_size(512)
796            .with_ollama();
797
798        assert_eq!(builder1.config().chunk_size, builder2.config().chunk_size);
799    }
800
801    #[test]
802    fn test_typed_builder_llm_before_output() {
803        // Can configure LLM before output directory
804        let builder = TypedBuilder::new()
805            .with_ollama()  // LLM first
806            .with_output_dir("./test"); // Output second
807
808        assert!(builder.config().ollama.enabled);
809        assert_eq!(builder.config().output_dir, "./test");
810    }
811}