context_forge/analysis/
mod.rs1use std::collections::HashMap;
8
9#[cfg(feature = "parallel")]
10use rayon::prelude::*;
11
12pub mod classification;
14pub mod extraction;
16pub mod frequency;
18pub mod injection;
20pub mod lexicon;
22pub mod ngrams;
24pub mod prefilter;
26pub mod recurrence;
28pub mod scoring;
30pub mod tokenizer;
32
33pub use classification::{
35 classify_passages, ClassificationConfig, ClassifiedPassage, ImportanceCategory, PassageContext,
36};
37pub use extraction::{extract_passages, ExtractedPassage, ExtractionConfig, ExtractionEntry};
38pub use frequency::{term_counts, term_counts_with_ngrams};
39pub use injection::{adjust_weights, scale_budget, InjectionConfig};
40pub use lexicon::Lexicons;
41pub use ngrams::{bigrams, extract, trigrams};
42pub use prefilter::{strip_execution_artifacts, FilterToggle, PrefilterConfig};
43pub use recurrence::{compute_recurrence, RecurrenceConfig, RecurrenceResult};
44pub use scoring::{pack_segments, score_passages, ImportanceSegment, ScoringConfig};
45pub use tokenizer::{Tokenizer, TokenizerConfig};
46
47#[allow(
53 clippy::implicit_hasher,
54 reason = "HashMap with the default hasher is the natural return type here; generalizing \
55 over S: BuildHasher would add generic noise with no caller benefit"
56)]
57#[must_use]
58pub fn build_session_term_maps(
59 session_contents: &[Vec<&str>],
60 tokenizer: &Tokenizer,
61 prefilter_config: &PrefilterConfig,
62) -> Vec<HashMap<String, usize>> {
63 let session_term_map = |contents: &Vec<&str>| {
64 let mut combined_tokens: Vec<String> = Vec::new();
65 for content in contents {
66 let clean = strip_execution_artifacts(content, prefilter_config);
67 combined_tokens.extend(tokenizer.tokenize(&clean));
68 }
69 term_counts_with_ngrams(&combined_tokens)
70 };
71
72 #[cfg(feature = "parallel")]
73 {
74 session_contents.par_iter().map(session_term_map).collect()
75 }
76 #[cfg(not(feature = "parallel"))]
77 {
78 session_contents.iter().map(session_term_map).collect()
79 }
80}
81
82#[cfg(feature = "parallel")]
101pub fn with_thread_cap<R: Send>(
102 thread_cap: Option<usize>,
103 f: impl FnOnce() -> R + Send,
104) -> crate::Result<R> {
105 match thread_cap {
106 Some(threads) => {
107 let pool = rayon::ThreadPoolBuilder::new()
108 .num_threads(threads)
109 .build()
110 .map_err(|err| crate::Error::InvalidEntry(format!("rayon pool: {err}")))?;
111 Ok(pool.install(f))
112 }
113 None => Ok(f()),
114 }
115}
116
117#[cfg(all(test, feature = "parallel"))]
118mod parallel_tests {
119 use super::with_thread_cap;
120
121 #[test]
122 fn with_thread_cap_some_installs_scoped_pool() {
123 let threads = with_thread_cap(Some(2), rayon::current_num_threads).unwrap();
124 assert_eq!(
125 threads, 2,
126 "closure should run inside a 2-thread scoped pool"
127 );
128 }
129
130 #[test]
131 fn with_thread_cap_none_calls_directly() {
132 let value = with_thread_cap(None, || 21 * 2).unwrap();
133 assert_eq!(value, 42);
134 }
135
136 #[test]
137 fn with_thread_cap_zero_means_automatic() {
138 let threads = with_thread_cap(Some(0), rayon::current_num_threads).unwrap();
140 assert!(threads >= 1);
141 }
142
143 #[test]
144 fn with_thread_cap_returns_closure_result_through_pool() {
145 let sum: usize = with_thread_cap(Some(2), || (1..=100).sum()).unwrap();
146 assert_eq!(sum, 5050);
147 }
148}