55
66import logging
77from datetime import datetime , timezone
8- from typing import cast
8+ from typing import Any , cast
99from uuid import uuid4
1010
1111import numpy as np
1212import pandas as pd
13+ from graphrag_storage .tables .table import Table
1314
1415from graphrag .config .models .graph_rag_config import GraphRagConfig
1516from graphrag .data_model .data_reader import DataReader
@@ -28,88 +29,122 @@ async def run_workflow(
2829 """All the steps to transform final communities."""
2930 logger .info ("Workflow started: create_communities" )
3031 reader = DataReader (context .output_table_provider )
31- entities = await reader .entities ()
3232 relationships = await reader .relationships ()
33+
3334 max_cluster_size = config .cluster_graph .max_cluster_size
3435 use_lcc = config .cluster_graph .use_lcc
3536 seed = config .cluster_graph .seed
3637
37- output = create_communities (
38- entities ,
39- relationships ,
40- max_cluster_size = max_cluster_size ,
41- use_lcc = use_lcc ,
42- seed = seed ,
43- )
44-
45- await context .output_table_provider .write_dataframe ("communities" , output )
38+ async with (
39+ context .output_table_provider .open ("entities" ) as entities_table ,
40+ context .output_table_provider .open ("communities" ) as communities_table ,
41+ ):
42+ sample_rows = await create_communities (
43+ communities_table ,
44+ entities_table ,
45+ relationships ,
46+ max_cluster_size = max_cluster_size ,
47+ use_lcc = use_lcc ,
48+ seed = seed ,
49+ )
4650
4751 logger .info ("Workflow completed: create_communities" )
48- return WorkflowFunctionOutput (result = output )
52+ return WorkflowFunctionOutput (result = sample_rows )
4953
5054
51- def create_communities (
52- entities : pd .DataFrame ,
55+ async def create_communities (
56+ communities_table : Table ,
57+ entities_table : Table ,
5358 relationships : pd .DataFrame ,
5459 max_cluster_size : int ,
5560 use_lcc : bool ,
5661 seed : int | None = None ,
57- ) -> pd .DataFrame :
58- """All the steps to transform final communities."""
62+ ) -> list [dict [str , Any ]]:
63+ """Build communities from clustered relationships and stream rows to the table.
64+
65+ Args
66+ ----
67+ communities_table: Table
68+ Output table to write community rows to.
69+ entities_table: Table
70+ Table containing entity rows.
71+ relationships: pd.DataFrame
72+ Relationships DataFrame with source, target, weight,
73+ text_unit_ids columns.
74+ max_cluster_size: int
75+ Maximum cluster size for hierarchical Leiden.
76+ use_lcc: bool
77+ Whether to restrict to the largest connected component.
78+ seed: int | None
79+ Random seed for deterministic clustering.
80+
81+ Returns
82+ -------
83+ list[dict[str, Any]]
84+ Sample of up to 5 community rows for logging.
85+ """
5986 clusters = cluster_graph (
6087 relationships ,
6188 max_cluster_size ,
6289 use_lcc ,
6390 seed = seed ,
6491 )
6592
93+ title_to_entity_id : dict [str , str ] = {}
94+ async for row in entities_table :
95+ title_to_entity_id [row ["title" ]] = row ["id" ]
96+
6697 communities = pd .DataFrame (
6798 clusters , columns = pd .Index (["level" , "community" , "parent" , "title" ])
6899 ).explode ("title" )
69100 communities ["community" ] = communities ["community" ].astype (int )
70101
71102 # aggregate entity ids for each community
72- entity_ids = communities .merge (entities , on = "title" , how = "inner" )
103+ entity_map = communities [["community" , "title" ]].copy ()
104+ entity_map ["entity_id" ] = entity_map ["title" ].map (title_to_entity_id )
73105 entity_ids = (
74- entity_ids .groupby ("community" ).agg (entity_ids = ("id" , list )).reset_index ()
106+ entity_map
107+ .dropna (subset = ["entity_id" ])
108+ .groupby ("community" )
109+ .agg (entity_ids = ("entity_id" , list ))
110+ .reset_index ()
75111 )
76112
77- # aggregate relationships ids for each community
78- # these are limited to only those where the source and target are in the same community
79- max_level = communities ["level" ].max ()
80- all_grouped = pd .DataFrame (
81- columns = ["community" , "level" , "relationship_ids" , "text_unit_ids" ] # type: ignore
82- )
83- for level in range (max_level + 1 ):
84- communities_at_level = communities .loc [communities ["level" ] == level ]
85- sources = relationships .merge (
86- communities_at_level , left_on = "source" , right_on = "title" , how = "inner"
113+ # aggregate relationship ids per community, limited to
114+ # intra-community edges (source and target in the same community).
115+ # Process one hierarchy level at a time to keep intermediate
116+ # DataFrames small, then concat the grouped results once at the end.
117+ level_results = []
118+ for level in communities ["level" ].unique ():
119+ level_comms = communities [communities ["level" ] == level ]
120+ with_source = relationships .merge (
121+ level_comms , left_on = "source" , right_on = "title" , how = "inner"
87122 )
88- targets = sources .merge (
89- communities_at_level , left_on = "target" , right_on = "title" , how = "inner"
123+ with_both = with_source .merge (
124+ level_comms , left_on = "target" , right_on = "title" , how = "inner"
90125 )
91- matched = targets .loc [targets ["community_x" ] == targets ["community_y" ]]
92- text_units = matched .explode ("text_unit_ids" )
126+ intra = with_both [with_both ["community_x" ] == with_both ["community_y" ]]
127+ if intra .empty :
128+ continue
93129 grouped = (
94- text_units
95- .groupby (["community_x" , "level_x" , "parent_x" ])
96- .agg (relationship_ids = ("id" , list ), text_unit_ids = ("text_unit_ids" , list ))
130+ intra
131+ .explode ("text_unit_ids" )
132+ .groupby (["community_x" , "parent_x" ])
133+ .agg (
134+ relationship_ids = ("id" , list ),
135+ text_unit_ids = ("text_unit_ids" , list ),
136+ )
97137 .reset_index ()
98138 )
99- grouped .rename (
100- columns = {
101- "community_x" : "community" ,
102- "level_x" : "level" ,
103- "parent_x" : "parent" ,
104- },
105- inplace = True ,
106- )
107- all_grouped = pd .concat ([
108- all_grouped ,
109- grouped .loc [
110- :, ["community" , "level" , "parent" , "relationship_ids" , "text_unit_ids" ]
111- ],
112- ])
139+ grouped ["level" ] = level
140+ level_results .append (grouped )
141+
142+ all_grouped = pd .concat (level_results , ignore_index = True ).rename (
143+ columns = {
144+ "community_x" : "community" ,
145+ "parent_x" : "parent" ,
146+ }
147+ )
113148
114149 # deduplicate the lists
115150 all_grouped ["relationship_ids" ] = all_grouped ["relationship_ids" ].apply (
@@ -146,7 +181,27 @@ def create_communities(
146181 final_communities ["period" ] = datetime .now (timezone .utc ).date ().isoformat ()
147182 final_communities ["size" ] = final_communities .loc [:, "entity_ids" ].apply (len )
148183
149- return final_communities .loc [
150- :,
151- COMMUNITIES_FINAL_COLUMNS ,
152- ]
184+ output = final_communities .loc [:, COMMUNITIES_FINAL_COLUMNS ]
185+ rows = output .to_dict ("records" )
186+ sample_rows : list [dict [str , Any ]] = []
187+ for row in rows :
188+ row = _sanitize_row (row )
189+ await communities_table .write (row )
190+ if len (sample_rows ) < 5 :
191+ sample_rows .append (row )
192+ return sample_rows
193+
194+
195+ def _sanitize_row (row : dict [str , Any ]) -> dict [str , Any ]:
196+ """Convert numpy types to native Python types for table serialization."""
197+ sanitized = {}
198+ for key , value in row .items ():
199+ if isinstance (value , np .ndarray ):
200+ sanitized [key ] = value .tolist ()
201+ elif isinstance (value , np .integer ):
202+ sanitized [key ] = int (value )
203+ elif isinstance (value , np .floating ):
204+ sanitized [key ] = float (value )
205+ else :
206+ sanitized [key ] = value
207+ return sanitized
0 commit comments