@@ -1768,6 +1768,20 @@ def _build_community_points(
17681768 return points
17691769
17701770
1771+ def _community_graph_source_warnings (graph_source : str ) -> list [str ]:
1772+ """Convert graph source into human-readable warnings."""
1773+ warnings : list [str ] = []
1774+ if graph_source == "connectivity_recovery" :
1775+ warnings .append (
1776+ "Recovered community graph from architecture/UI/flow links because symbol graph was sparse."
1777+ )
1778+ if graph_source == "knowledge_fallback" :
1779+ warnings .append (
1780+ "No symbol dependency graph found. Using knowledge graph communities as fallback."
1781+ )
1782+ return warnings
1783+
1784+
17711785async def _build_structural_community_points (
17721786 db : Any ,
17731787 * ,
@@ -1778,16 +1792,8 @@ async def _build_structural_community_points(
17781792 page : int ,
17791793 limit : int ,
17801794) -> tuple [list [dict [str , Any ]], list [str ]]:
1781- warnings : list [str ] = []
17821795 graph_nodes , graph_edges , graph_source = await _load_community_graph (db , collection_id )
1783- if graph_source == "connectivity_recovery" :
1784- warnings .append (
1785- "Recovered community graph from architecture/UI/flow links because symbol graph was sparse."
1786- )
1787- if graph_source == "knowledge_fallback" :
1788- warnings .append (
1789- "No symbol dependency graph found. Using knowledge graph communities as fallback."
1790- )
1796+ warnings = _community_graph_source_warnings (graph_source )
17911797
17921798 if include_node_kinds :
17931799 graph_nodes = [node for node in graph_nodes if node .kind in include_node_kinds ]
@@ -3999,6 +4005,55 @@ def _graphrag_status(
39994005 return {"status" : "ready" , "reason" : "ok" }
40004006
40014007
4008+ def _build_graphrag_node_query (
4009+ collection_uuid : uuid .UUID ,
4010+ * ,
4011+ include_node_kinds : set [KnowledgeNodeKind ] | None ,
4012+ exclude_node_kinds : set [KnowledgeNodeKind ] | None ,
4013+ community_id : str | None ,
4014+ communities : dict [str , dict [str , Any ]],
4015+ ) -> Any | None :
4016+ """Build the KnowledgeNode query for graphrag. Returns None if the community is empty."""
4017+ node_query = select (KnowledgeNode ).where (KnowledgeNode .collection_id == collection_uuid )
4018+ if include_node_kinds :
4019+ node_query = node_query .where (KnowledgeNode .kind .in_ (include_node_kinds ))
4020+ if exclude_node_kinds :
4021+ node_query = node_query .where (~ KnowledgeNode .kind .in_ (exclude_node_kinds ))
4022+ if community_id :
4023+ member_ids = communities [community_id ]["member_node_ids" ]
4024+ if not member_ids :
4025+ return None
4026+ node_query = node_query .where (KnowledgeNode .id .in_ (member_ids ))
4027+ return node_query
4028+
4029+
4030+ def _empty_graphrag_response (
4031+ collection_uuid : uuid .UUID ,
4032+ scenario : Any ,
4033+ community_mode : str ,
4034+ community_id : str | None ,
4035+ page : int ,
4036+ limit : int ,
4037+ ) -> dict [str , Any ]:
4038+ """Return an empty graphrag response for an empty community."""
4039+ return {
4040+ "collection_id" : str (collection_uuid ),
4041+ "scenario" : _serialize_scenario (scenario ),
4042+ "projection" : "graphrag" ,
4043+ "entity_level" : "knowledge_node" ,
4044+ "community_mode" : community_mode ,
4045+ "community_id" : community_id ,
4046+ "status" : {"status" : "unavailable" , "reason" : "no_knowledge_graph" },
4047+ "graph" : {
4048+ "nodes" : [],
4049+ "edges" : [],
4050+ "page" : page ,
4051+ "limit" : limit ,
4052+ "total_nodes" : 0 ,
4053+ },
4054+ }
4055+
4056+
40024057@router .get (
40034058 "/collections/{collection_id}/views/graphrag" ,
40044059 responses = {
@@ -4050,32 +4105,22 @@ async def graphrag_view(
40504105 if resolved_community_id and resolved_community_id not in communities :
40514106 raise HTTPException (status_code = 404 , detail = "Community not found" )
40524107
4053- node_query = select (KnowledgeNode ).where (KnowledgeNode .collection_id == collection_uuid )
4054- if include_node_kinds :
4055- node_query = node_query .where (KnowledgeNode .kind .in_ (include_node_kinds ))
4056- if exclude_node_kinds :
4057- node_query = node_query .where (~ KnowledgeNode .kind .in_ (exclude_node_kinds ))
4058- if resolved_community_id :
4059- member_ids = communities [resolved_community_id ]["member_node_ids" ]
4060- if not member_ids :
4061- status = {"status" : "unavailable" , "reason" : "no_knowledge_graph" }
4062- return {
4063- "collection_id" : str (collection_uuid ),
4064- "scenario" : _serialize_scenario (scenario ),
4065- "projection" : "graphrag" ,
4066- "entity_level" : "knowledge_node" ,
4067- "community_mode" : resolved_community_mode ,
4068- "community_id" : resolved_community_id ,
4069- "status" : status ,
4070- "graph" : {
4071- "nodes" : [],
4072- "edges" : [],
4073- "page" : page ,
4074- "limit" : limit ,
4075- "total_nodes" : 0 ,
4076- },
4077- }
4078- node_query = node_query .where (KnowledgeNode .id .in_ (member_ids ))
4108+ node_query = _build_graphrag_node_query (
4109+ collection_uuid ,
4110+ include_node_kinds = include_node_kinds ,
4111+ exclude_node_kinds = exclude_node_kinds ,
4112+ community_id = resolved_community_id ,
4113+ communities = communities ,
4114+ )
4115+ if node_query is None :
4116+ return _empty_graphrag_response (
4117+ collection_uuid ,
4118+ scenario ,
4119+ resolved_community_mode ,
4120+ resolved_community_id ,
4121+ page ,
4122+ limit ,
4123+ )
40794124
40804125 total_nodes = (
40814126 await db .execute (select (func .count ()).select_from (node_query .subquery ()))
@@ -5470,6 +5515,28 @@ async def _export_mermaid_c4_format(
54705515 }
54715516
54725517
5518+ async def _export_graph_format (
5519+ db : Any ,
5520+ scenario : Any ,
5521+ projection : GraphProjection ,
5522+ entity_level : str | None ,
5523+ * ,
5524+ symbol_exporter : Any ,
5525+ graph_exporter : Any ,
5526+ ) -> str :
5527+ """Export using the symbol exporter for CODE_SYMBOL, otherwise build graph and use graph exporter."""
5528+ if projection == GraphProjection .CODE_SYMBOL :
5529+ return await symbol_exporter (db , scenario .id ) # type: ignore[no-any-return]
5530+ graph = await get_full_scenario_graph (
5531+ session = db ,
5532+ scenario_id = scenario .id ,
5533+ layer = None ,
5534+ projection = projection ,
5535+ entity_level = entity_level ,
5536+ )
5537+ return graph_exporter (scenario .id , graph ) # type: ignore[no-any-return]
5538+
5539+
54735540async def _generate_export_content (
54745541 db : Any ,
54755542 scenario : Any ,
@@ -5478,17 +5545,14 @@ async def _generate_export_content(
54785545) -> tuple [dict | None , str , Any , str , GraphProjection ]:
54795546 """Dispatch to the correct export format and return (early_return, content, kind, name, projection)."""
54805547 if body .format == "lpg_jsonl" :
5481- if projection == GraphProjection .CODE_SYMBOL :
5482- content = await export_lpg_jsonl (db , scenario .id )
5483- else :
5484- graph = await get_full_scenario_graph (
5485- session = db ,
5486- scenario_id = scenario .id ,
5487- layer = None ,
5488- projection = projection ,
5489- entity_level = body .entity_level ,
5490- )
5491- content = export_lpg_jsonl_from_graph (scenario .id , graph )
5548+ content = await _export_graph_format (
5549+ db ,
5550+ scenario ,
5551+ projection ,
5552+ body .entity_level ,
5553+ symbol_exporter = export_lpg_jsonl ,
5554+ graph_exporter = export_lpg_jsonl_from_graph ,
5555+ )
54925556 return (
54935557 None ,
54945558 content ,
@@ -5507,30 +5571,24 @@ async def _generate_export_content(
55075571 )
55085572 return None , content , KnowledgeArtifactKind .CC_JSON , f"{ scenario .name } .cc.json" , projection
55095573 if body .format == "cx2" :
5510- if projection == GraphProjection .CODE_SYMBOL :
5511- content = await export_cx2 (db , scenario .id )
5512- else :
5513- graph = await get_full_scenario_graph (
5514- session = db ,
5515- scenario_id = scenario .id ,
5516- layer = None ,
5517- projection = projection ,
5518- entity_level = body .entity_level ,
5519- )
5520- content = export_cx2_from_graph (scenario .id , graph )
5574+ content = await _export_graph_format (
5575+ db ,
5576+ scenario ,
5577+ projection ,
5578+ body .entity_level ,
5579+ symbol_exporter = export_cx2 ,
5580+ graph_exporter = export_cx2_from_graph ,
5581+ )
55215582 return None , content , KnowledgeArtifactKind .CX2 , f"{ scenario .name } .cx2.json" , projection
55225583 if body .format == "jgf" :
5523- if projection == GraphProjection .CODE_SYMBOL :
5524- content = await export_jgf (db , scenario .id )
5525- else :
5526- graph = await get_full_scenario_graph (
5527- session = db ,
5528- scenario_id = scenario .id ,
5529- layer = None ,
5530- projection = projection ,
5531- entity_level = body .entity_level ,
5532- )
5533- content = export_jgf_from_graph (scenario .id , graph )
5584+ content = await _export_graph_format (
5585+ db ,
5586+ scenario ,
5587+ projection ,
5588+ body .entity_level ,
5589+ symbol_exporter = export_jgf ,
5590+ graph_exporter = export_jgf_from_graph ,
5591+ )
55345592 return None , content , KnowledgeArtifactKind .JGF , f"{ scenario .name } .jgf.json" , projection
55355593 if body .format == "twin_manifest" :
55365594 content = await export_twin_manifest (db , scenario .id )
0 commit comments