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)]
38pub enum GraphRAGError {
39 #[error("LLM error: {0}")]
40 LLMError(String),
41
42 #[error("Extraction error: {0}")]
43 ExtractionError(String),
44
45 #[error("Query error: {0}")]
46 QueryError(String),
47
48 #[error("Community error: {0}")]
49 CommunityError(String),
50}
51
52pub struct GraphRAGConfig {
54 pub max_entities_per_doc: usize,
55 pub max_relations_per_doc: usize,
56 pub community_max_levels: usize,
57 pub max_context_tokens: Option<usize>,
60 pub entity_matcher: Option<Box<dyn EntityMatcher>>,
63}
64
65impl Default for GraphRAGConfig {
66 fn default() -> Self {
67 Self {
68 max_entities_per_doc: 10,
69 max_relations_per_doc: 10,
70 community_max_levels: 3,
71 max_context_tokens: None,
72 entity_matcher: None,
73 }
74 }
75}
76
77impl GraphRAGConfig {
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn with_max_entities_per_doc(mut self, n: usize) -> Self {
83 self.max_entities_per_doc = n;
84 self
85 }
86
87 pub fn with_max_relations_per_doc(mut self, n: usize) -> Self {
88 self.max_relations_per_doc = n;
89 self
90 }
91
92 pub fn with_community_max_levels(mut self, n: usize) -> Self {
93 self.community_max_levels = n;
94 self
95 }
96
97 pub fn with_max_context_tokens(mut self, tokens: usize) -> Self {
103 self.max_context_tokens = Some(tokens);
104 self
105 }
106
107 pub fn with_entity_matcher(mut self, matcher: Box<dyn EntityMatcher>) -> Self {
112 self.entity_matcher = Some(matcher);
113 self
114 }
115}
116
117pub struct GraphRAG<M: BaseChatModel> {
122 llm: M,
123 store: RwLock<GraphStore>,
124 config: GraphRAGConfig,
125}
126
127impl<M: BaseChatModel> GraphRAG<M> {
128 pub fn new(llm: M) -> Self {
130 Self {
131 llm,
132 store: RwLock::new(GraphStore::new()),
133 config: GraphRAGConfig::default(),
134 }
135 }
136
137 pub fn with_config(mut self, config: GraphRAGConfig) -> Self {
139 self.config = config;
140 self
141 }
142
143 pub async fn add_documents(&self, docs: &[Document]) -> Result<(), GraphRAGError> {
146 for doc in docs {
147 let extraction = extractor::extract(
148 &self.llm,
149 &doc.content,
150 self.config.max_entities_per_doc,
151 self.config.max_relations_per_doc,
152 )
153 .await?;
154
155 let doc_id = doc.id.clone();
156 let mut store = self.store.write().await;
157
158 let mut name_to_id: std::collections::HashMap<String, String> = store
160 .all_entities()
161 .values()
162 .map(|e| (e.name.to_lowercase(), e.id.clone()))
163 .collect();
164
165 for ext_ent in &extraction.entities {
167 let key = ext_ent.name.to_lowercase();
168 if let Some(_existing_id) = name_to_id.get(&key) {
169 log::info!("GraphRAG: skipping duplicate entity '{}'", ext_ent.name);
171 continue;
172 }
173
174 let id = format!("e_{}", uuid::Uuid::new_v4().as_simple());
175 name_to_id.insert(key, id.clone());
176
177 store.add_entity(Entity {
178 id,
179 name: ext_ent.name.clone(),
180 entity_type: ext_ent.entity_type.clone(),
181 description: ext_ent.description.clone(),
182 });
183 }
184
185 for ext_rel in &extraction.relations {
187 let source_key = ext_rel.source.to_lowercase();
188 let target_key = ext_rel.target.to_lowercase();
189
190 let source_id = match name_to_id.get(&source_key) {
191 Some(id) => id.clone(),
192 None => {
193 log::info!(
194 "GraphRAG: skipping relation with unknown source entity '{}'",
195 ext_rel.source
196 );
197 continue;
198 }
199 };
200 let target_id = match name_to_id.get(&target_key) {
201 Some(id) => id.clone(),
202 None => {
203 log::info!(
204 "GraphRAG: skipping relation with unknown target entity '{}'",
205 ext_rel.target
206 );
207 continue;
208 }
209 };
210
211 store.add_relation(Relation {
212 source: source_id,
213 target: target_id,
214 relation_type: ext_rel.relation_type.clone(),
215 description: ext_rel.description.clone(),
216 doc_id: doc_id.clone(),
217 });
218 }
219 }
220
221 Ok(())
222 }
223
224 pub async fn build_communities(&self) -> Result<(), GraphRAGError> {
226 let communities = {
227 let store = self.store.read().await;
228 community::detect_communities(&store, self.config.community_max_levels)
229 };
230
231 let mut summaries = Vec::with_capacity(communities.len());
233 for comm in &communities {
234 let store_clone = {
235 let store = self.store.read().await;
236 store.clone()
237 };
238 let summary = community::summarize_community(&self.llm, &store_clone, comm).await?;
239 summaries.push(summary);
240 }
241
242 let mut store = self.store.write().await;
244 store.set_communities(communities);
245 store.set_community_summaries(summaries);
246
247 Ok(())
248 }
249
250 pub async fn query(&self, q: &str, mode: QueryMode) -> Result<GraphRAGResult, GraphRAGError> {
252 let store = {
253 let guard = self.store.read().await;
254 guard.clone()
255 };
256
257 let max_tokens = self.config.max_context_tokens;
258
259 match mode {
260 QueryMode::Global => query::global_query(&self.llm, &store, q, max_tokens).await,
261 QueryMode::Local => {
262 let matcher = self.config.entity_matcher.as_deref();
263 query::local_query(&self.llm, &store, q, max_tokens, matcher).await
264 }
265 QueryMode::Hybrid => {
266 let matcher = self.config.entity_matcher.as_deref();
267 query::hybrid_query(&self.llm, &store, q, max_tokens, matcher).await
268 }
269 }
270 }
271
272 pub async fn entity_count(&self) -> usize {
274 let store = self.store.read().await;
275 store.entity_count()
276 }
277
278 pub async fn relation_count(&self) -> usize {
280 let store = self.store.read().await;
281 store.relation_count()
282 }
283
284 pub async fn community_count(&self) -> usize {
286 let store = self.store.read().await;
287 store.communities().len()
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_graph_rag_config_default() {
297 let config = GraphRAGConfig::default();
298 assert_eq!(config.max_entities_per_doc, 10);
299 assert_eq!(config.max_relations_per_doc, 10);
300 assert_eq!(config.community_max_levels, 3);
301 assert!(config.max_context_tokens.is_none());
302 }
303
304 #[test]
305 fn test_graph_rag_config_builder() {
306 let config = GraphRAGConfig::new()
307 .with_max_entities_per_doc(5)
308 .with_max_relations_per_doc(8)
309 .with_community_max_levels(2);
310
311 assert_eq!(config.max_entities_per_doc, 5);
312 assert_eq!(config.max_relations_per_doc, 8);
313 assert_eq!(config.community_max_levels, 2);
314 }
315
316 #[test]
317 fn test_graph_error_display() {
318 let err = GraphRAGError::LLMError("timeout".into());
319 assert!(err.to_string().contains("timeout"));
320
321 let err = GraphRAGError::ExtractionError("bad json".into());
322 assert!(err.to_string().contains("bad json"));
323
324 let err = GraphRAGError::QueryError("no entities".into());
325 assert!(err.to_string().contains("no entities"));
326 }
327}