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}