1pub mod community;
23pub mod extractor;
24pub mod graph_store;
25pub mod matcher;
26pub mod query;
27
28pub use graph_store::{Community, Entity, GraphStore, Relation};
29pub use matcher::{EmbeddingMatcher, EntityMatcher, KeywordMatcher};
30pub use query::{GraphRAGResult, QueryMode};
31
32use lc_core::language_models::BaseChatModel;
33use lc_vector_stores::Document;
34use tokio::sync::RwLock;
35
36#[derive(Debug, thiserror::Error)]
38#[non_exhaustive]
39pub enum GraphRAGError {
40 #[error("LLM error: {0}")]
42 LLMError(String),
43
44 #[error("Extraction error: {0}")]
46 ExtractionError(String),
47
48 #[error("Query error: {0}")]
50 QueryError(String),
51
52 #[error("Community error: {0}")]
54 CommunityError(String),
55}
56
57pub struct GraphRAGConfig {
59 pub max_entities_per_doc: usize,
61 pub max_relations_per_doc: usize,
63 pub community_size_tiers: usize,
68 pub max_context_tokens: Option<usize>,
71 pub entity_matcher: Option<Box<dyn EntityMatcher>>,
74}
75
76impl Default for GraphRAGConfig {
77 fn default() -> Self {
78 Self {
79 max_entities_per_doc: 10,
80 max_relations_per_doc: 10,
81 community_size_tiers: 3,
82 max_context_tokens: None,
83 entity_matcher: None,
84 }
85 }
86}
87
88impl GraphRAGConfig {
89 pub fn new() -> Self {
91 Self::default()
92 }
93
94 pub fn with_max_entities_per_doc(mut self, n: usize) -> Self {
96 self.max_entities_per_doc = n;
97 self
98 }
99
100 pub fn with_max_relations_per_doc(mut self, n: usize) -> Self {
102 self.max_relations_per_doc = n;
103 self
104 }
105
106 pub fn with_community_size_tiers(mut self, n: usize) -> Self {
108 self.community_size_tiers = n;
109 self
110 }
111
112 pub fn with_max_context_tokens(mut self, tokens: usize) -> Self {
118 self.max_context_tokens = Some(tokens);
119 self
120 }
121
122 pub fn with_entity_matcher(mut self, matcher: Box<dyn EntityMatcher>) -> Self {
127 self.entity_matcher = Some(matcher);
128 self
129 }
130}
131
132pub struct GraphRAG<M: BaseChatModel> {
137 llm: M,
138 store: RwLock<GraphStore>,
139 config: GraphRAGConfig,
140}
141
142impl<M: BaseChatModel> GraphRAG<M> {
143 pub fn new(llm: M) -> Self {
145 Self {
146 llm,
147 store: RwLock::new(GraphStore::new()),
148 config: GraphRAGConfig::default(),
149 }
150 }
151
152 pub fn with_config(mut self, config: GraphRAGConfig) -> Self {
154 self.config = config;
155 self
156 }
157
158 pub async fn add_documents(&self, docs: &[Document]) -> Result<(), GraphRAGError> {
161 for doc in docs {
162 let extraction = extractor::extract(
163 &self.llm,
164 &doc.content,
165 self.config.max_entities_per_doc,
166 self.config.max_relations_per_doc,
167 )
168 .await?;
169
170 let doc_id = doc.id.clone();
171 let mut store = self.store.write().await;
172
173 let mut name_to_id: std::collections::HashMap<String, String> = store
175 .all_entities()
176 .values()
177 .map(|e| (e.name.to_lowercase(), e.id.clone()))
178 .collect();
179
180 for ext_ent in &extraction.entities {
182 let key = ext_ent.name.to_lowercase();
183 if let Some(_existing_id) = name_to_id.get(&key) {
184 log::info!("GraphRAG: skipping duplicate entity '{}'", ext_ent.name);
186 continue;
187 }
188
189 let id = format!("e_{}", uuid::Uuid::new_v4().as_simple());
190 name_to_id.insert(key, id.clone());
191
192 store.add_entity(Entity {
193 id,
194 name: ext_ent.name.clone(),
195 entity_type: ext_ent.entity_type.clone(),
196 description: ext_ent.description.clone(),
197 });
198 }
199
200 for ext_rel in &extraction.relations {
202 let source_key = ext_rel.source.to_lowercase();
203 let target_key = ext_rel.target.to_lowercase();
204
205 let source_id = match name_to_id.get(&source_key) {
206 Some(id) => id.clone(),
207 None => {
208 log::info!(
209 "GraphRAG: skipping relation with unknown source entity '{}'",
210 ext_rel.source
211 );
212 continue;
213 }
214 };
215 let target_id = match name_to_id.get(&target_key) {
216 Some(id) => id.clone(),
217 None => {
218 log::info!(
219 "GraphRAG: skipping relation with unknown target entity '{}'",
220 ext_rel.target
221 );
222 continue;
223 }
224 };
225
226 store.add_relation(Relation {
227 source: source_id,
228 target: target_id,
229 relation_type: ext_rel.relation_type.clone(),
230 description: ext_rel.description.clone(),
231 doc_id: doc_id.clone(),
232 });
233 }
234 }
235
236 Ok(())
237 }
238
239 pub async fn build_communities(&self) -> Result<(), GraphRAGError> {
241 let communities = {
242 let store = self.store.read().await;
243 community::detect_communities(&store, self.config.community_size_tiers)
244 };
245
246 let mut summaries = Vec::with_capacity(communities.len());
248 for comm in &communities {
249 let store_clone = {
250 let store = self.store.read().await;
251 store.clone()
252 };
253 let summary = community::summarize_community(&self.llm, &store_clone, comm).await?;
254 summaries.push(summary);
255 }
256
257 let mut store = self.store.write().await;
259 store.set_communities(communities);
260 store.set_community_summaries(summaries);
261
262 Ok(())
263 }
264
265 pub async fn query(&self, q: &str, mode: QueryMode) -> Result<GraphRAGResult, GraphRAGError> {
267 let store = {
268 let guard = self.store.read().await;
269 guard.clone()
270 };
271
272 let max_tokens = self.config.max_context_tokens;
273
274 match mode {
275 QueryMode::Global => query::global_query(&self.llm, &store, q, max_tokens).await,
276 QueryMode::Local => {
277 let matcher = self.config.entity_matcher.as_deref();
278 query::local_query(&self.llm, &store, q, max_tokens, matcher).await
279 }
280 QueryMode::Hybrid => {
281 let matcher = self.config.entity_matcher.as_deref();
282 query::hybrid_query(&self.llm, &store, q, max_tokens, matcher).await
283 }
284 }
285 }
286
287 pub async fn entity_count(&self) -> usize {
289 let store = self.store.read().await;
290 store.entity_count()
291 }
292
293 pub async fn relation_count(&self) -> usize {
295 let store = self.store.read().await;
296 store.relation_count()
297 }
298
299 pub async fn community_count(&self) -> usize {
301 let store = self.store.read().await;
302 store.communities().len()
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_graph_rag_config_default() {
312 let config = GraphRAGConfig::default();
313 assert_eq!(config.max_entities_per_doc, 10);
314 assert_eq!(config.max_relations_per_doc, 10);
315 assert_eq!(config.community_size_tiers, 3);
316 assert!(config.max_context_tokens.is_none());
317 }
318
319 #[test]
320 fn test_graph_rag_config_builder() {
321 let config = GraphRAGConfig::new()
322 .with_max_entities_per_doc(5)
323 .with_max_relations_per_doc(8)
324 .with_community_size_tiers(2);
325
326 assert_eq!(config.max_entities_per_doc, 5);
327 assert_eq!(config.max_relations_per_doc, 8);
328 assert_eq!(config.community_size_tiers, 2);
329 }
330
331 #[test]
332 fn test_graph_error_display() {
333 let err = GraphRAGError::LLMError("timeout".into());
334 assert!(err.to_string().contains("timeout"));
335
336 let err = GraphRAGError::ExtractionError("bad json".into());
337 assert!(err.to_string().contains("bad json"));
338
339 let err = GraphRAGError::QueryError("no entities".into());
340 assert!(err.to_string().contains("no entities"));
341 }
342}