abinazebinoy commited on
Commit
5ac9ba3
Β·
1 Parent(s): 4b4af4b

feat(phase-33): add 4 graph analytics endpoints to api/routes/graph.py

Browse files

GET /graph/analytics/{entity_id} -- betweenness + pagerank + communities
for an entity local sub-graph
GET /graph/centrality/betweenness -- top 20 brokers in full graph
GET /graph/centrality/pagerank -- top 20 influential entities in full graph
GET /graph/communities -- detect institutional clusters

All 4 endpoints use the pre-built GraphAnalytics class (ai/graph_analytics.py)
which uses NetworkX under the hood. Graceful ImportError if networkx not
installed -- returns {status: error} instead of 500.

Files changed (1) hide show
  1. api/routes/graph.py +146 -0
api/routes/graph.py CHANGED
@@ -123,3 +123,149 @@ def politician_contracts_pattern(
123
  return {"pattern": "politician -> company -> contract",
124
  "total": len(rows), "results": rows,
125
  "generated_at": datetime.now().isoformat()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  return {"pattern": "politician -> company -> contract",
124
  "total": len(rows), "results": rows,
125
  "generated_at": datetime.now().isoformat()}
126
+
127
+
128
+ # ── Phase 33: Graph Intelligence endpoints ───────────────────────────────────
129
+
130
+ @router.get("/graph/analytics/{entity_id}")
131
+ def entity_graph_analytics(
132
+ entity_id: str,
133
+ depth: int = Query(3, ge=1, le=4),
134
+ driver=Depends(get_db),
135
+ ):
136
+ """
137
+ Run full graph analytics for one entity:
138
+ betweenness centrality, PageRank, and community detection
139
+ on the entity local sub-graph (up to depth hops out).
140
+ """
141
+ logger.info(f"[GraphAnalytics] entity={entity_id[:8]} depth={depth}")
142
+ try:
143
+ from ai.graph_analytics import GraphAnalytics
144
+ ga = GraphAnalytics(driver=driver)
145
+ nodes, edges = ga._fetch_graph_from_neo4j(entity_id=entity_id, depth=depth)
146
+ if not nodes:
147
+ return {
148
+ "entity_id": entity_id,
149
+ "status": "no_data",
150
+ "note": "Entity not found or graph has no connections at this depth",
151
+ "analyzed_at": datetime.now().isoformat(),
152
+ }
153
+ betweenness = ga.compute_betweenness_centrality(nodes, edges)
154
+ pagerank = ga.compute_pagerank(nodes, edges)
155
+ communities = ga.detect_communities(nodes, edges)
156
+ return {
157
+ "entity_id": entity_id,
158
+ "depth": depth,
159
+ "node_count": len(nodes),
160
+ "edge_count": len(edges),
161
+ "betweenness": betweenness[:10],
162
+ "pagerank": pagerank[:10],
163
+ "communities": communities[:5],
164
+ "analyzed_at": datetime.now().isoformat(),
165
+ }
166
+ except ImportError:
167
+ return {"status": "error", "detail": "networkx not installed"}
168
+ except Exception as e:
169
+ logger.error(f"[GraphAnalytics] entity={entity_id[:8]}: {type(e).__name__}")
170
+ from fastapi import HTTPException
171
+ raise HTTPException(status_code=500, detail="Analytics failed")
172
+
173
+
174
+ @router.get("/graph/centrality/betweenness")
175
+ def global_betweenness(
176
+ limit: int = Query(20, ge=5, le=100),
177
+ driver=Depends(get_db),
178
+ ):
179
+ """
180
+ Top entities ranked by betweenness centrality across the entire graph.
181
+ High betweenness = acts as a bridge between institutional networks.
182
+ Expensive on large graphs -- cached by the caller for 10 minutes.
183
+ """
184
+ logger.info("[GraphAnalytics] global betweenness requested")
185
+ try:
186
+ from ai.graph_analytics import GraphAnalytics
187
+ ga = GraphAnalytics(driver=driver)
188
+ nodes, edges = ga._fetch_graph_from_neo4j()
189
+ if not nodes:
190
+ return {"status": "no_data", "results": []}
191
+ results = ga.compute_betweenness_centrality(nodes, edges)
192
+ return {
193
+ "metric": "betweenness_centrality",
194
+ "node_count": len(nodes),
195
+ "edge_count": len(edges),
196
+ "top": results[:limit],
197
+ "analyzed_at": datetime.now().isoformat(),
198
+ }
199
+ except ImportError:
200
+ return {"status": "error", "detail": "networkx not installed"}
201
+ except Exception as e:
202
+ logger.error(f"[GraphAnalytics] betweenness error: {type(e).__name__}")
203
+ from fastapi import HTTPException
204
+ raise HTTPException(status_code=500, detail="Betweenness centrality failed")
205
+
206
+
207
+ @router.get("/graph/centrality/pagerank")
208
+ def global_pagerank(
209
+ limit: int = Query(20, ge=5, le=100),
210
+ driver=Depends(get_db),
211
+ ):
212
+ """
213
+ Top entities ranked by PageRank across the entire graph.
214
+ High PageRank = many high-influence entities point to this node.
215
+ """
216
+ logger.info("[GraphAnalytics] global pagerank requested")
217
+ try:
218
+ from ai.graph_analytics import GraphAnalytics
219
+ ga = GraphAnalytics(driver=driver)
220
+ nodes, edges = ga._fetch_graph_from_neo4j()
221
+ if not nodes:
222
+ return {"status": "no_data", "results": []}
223
+ results = ga.compute_pagerank(nodes, edges)
224
+ return {
225
+ "metric": "pagerank",
226
+ "node_count": len(nodes),
227
+ "edge_count": len(edges),
228
+ "top": results[:limit],
229
+ "analyzed_at": datetime.now().isoformat(),
230
+ }
231
+ except ImportError:
232
+ return {"status": "error", "detail": "networkx not installed"}
233
+ except Exception as e:
234
+ logger.error(f"[GraphAnalytics] pagerank error: {type(e).__name__}")
235
+ from fastapi import HTTPException
236
+ raise HTTPException(status_code=500, detail="PageRank failed")
237
+
238
+
239
+ @router.get("/graph/communities")
240
+ def global_communities(
241
+ min_size: int = Query(3, ge=2, le=50),
242
+ driver=Depends(get_db),
243
+ ):
244
+ """
245
+ Detect institutional clusters (communities) in the full graph.
246
+ Returns communities with >= min_size members.
247
+ Large communities often indicate procurement clusters, shell company
248
+ networks, or party-linked directorships warranting investigation.
249
+ """
250
+ logger.info("[GraphAnalytics] community detection requested")
251
+ try:
252
+ from ai.graph_analytics import GraphAnalytics
253
+ ga = GraphAnalytics(driver=driver)
254
+ nodes, edges = ga._fetch_graph_from_neo4j()
255
+ if not nodes:
256
+ return {"status": "no_data", "communities": []}
257
+ communities = ga.detect_communities(nodes, edges)
258
+ filtered = [c for c in communities if c["size"] >= min_size]
259
+ return {
260
+ "total_communities": len(communities),
261
+ "shown": len(filtered),
262
+ "min_size": min_size,
263
+ "communities": filtered,
264
+ "analyzed_at": datetime.now().isoformat(),
265
+ }
266
+ except ImportError:
267
+ return {"status": "error", "detail": "networkx not installed"}
268
+ except Exception as e:
269
+ logger.error(f"[GraphAnalytics] communities error: {type(e).__name__}")
270
+ from fastapi import HTTPException
271
+ raise HTTPException(status_code=500, detail="Community detection failed")