"""Phase 5 GraphRAG API: 엔티티 중복 제거, 부분그래프 추출, 패턴 매칭, 그래프 분석. 기능: - 엔티티 중복 감지 및 병합 (벡터 + 텍스트 유사도) - 의미 기반 부분그래프 추출 (쿼리 임베딩) - 패턴 매칭 (경로, 순환, SCC) - 그래프 분석 (중심성, 커뮤니티) """ import logging from typing import Dict, Any, Optional, List from fastapi import FastAPI, APIRouter, HTTPException, Depends, Query from fastapi.responses import JSONResponse from ont_platform.core.graph import ( EntityResolver, RDFToPropertyGraphConverter, SubgraphRetriever, PatternMatcher, GraphAnalytics, Neo4jAdapter, ) logger = logging.getLogger(__name__) # FastAPI 앱 app = FastAPI( title="Ontology Platform - Phase 5 GraphRAG", description="엔티티 중복 제거, 부분그래프 추출, 패턴 매칭, 그래프 분석", version="0.5.0", ) # 라우터 graph_router = APIRouter(prefix="/api/v1/graph", tags=["graph"]) # 전역 인스턴스 (싱글톤) _neo4j_adapter: Optional[Neo4jAdapter] = None def get_neo4j_adapter() -> Neo4jAdapter: """Get or initialize Neo4j adapter.""" global _neo4j_adapter if _neo4j_adapter is None: _neo4j_adapter = Neo4jAdapter() return _neo4j_adapter adapter = get_neo4j_adapter() entity_resolver = EntityResolver() rdf_converter = RDFToPropertyGraphConverter() subgraph_retriever = SubgraphRetriever(adapter=adapter) pattern_matcher = PatternMatcher(adapter=adapter) graph_analytics = GraphAnalytics(adapter=adapter) # ============================================================================ # 엔티티 중복 제거 엔드포인트 # ============================================================================ @graph_router.post("/resolve") async def resolve_duplicates( request: Dict[str, Any], ) -> Dict[str, Any]: """엔티티 중복 감지 및 병합. Args: request: JSON body with "entities" field containing list of entities (id, label, type) Returns: { "clusters": [중복 클러스터], "report": 리포트, "total_duplicates": 감지된 중복 수 } """ entities = request.get("entities", []) try: # 임베더 초기화 (필요시) if not entity_resolver.embedder: await entity_resolver.initialize_embedder() # 중복 감지 clusters = await entity_resolver.detect_duplicates(entities) # 리포트 생성 report = entity_resolver.get_resolution_report(clusters) return { "status": "success", "clusters": [ { "cluster_id": c.cluster_id, "canonical_id": c.canonical_id, "duplicates": c.duplicates, "confidence": c.confidence, "reason": c.reason, } for c in clusters ], "report": report, "total_duplicates": report["total_duplicates"], } except Exception as e: logger.error(f"Failed to resolve duplicates: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # 부분그래프 추출 엔드포인트 # ============================================================================ @graph_router.post("/subgraph/semantic") async def retrieve_semantic_context( query: str = Query(...), top_k: int = Query(10, ge=1, le=100), min_similarity: float = Query(0.6, ge=0.0, le=1.0), hops: int = Query(1, ge=0, le=3), ) -> Dict[str, Any]: """의미 기반 부분그래프 추출 (쿼리 임베딩 유사도). Args: query: 검색 쿼리 텍스트 top_k: 상위 K개 매칭 엔티티 min_similarity: 최소 유사도 임계값 (0-1) hops: 확장할 홉 수 (0-3) Returns: 의미 기반 부분그래프 """ try: # 임베더 초기화 (필요시) if not subgraph_retriever.embedder: from sentence_transformers import SentenceTransformer subgraph_retriever.embedder = SentenceTransformer("all-MiniLM-L6-v2") result = await subgraph_retriever.retrieve_by_semantic_query( query=query, top_k=top_k, min_similarity=min_similarity, hops=hops, ) return { "status": "success", "query": query, "query_dimension": result.get("query_embedding_dimension", 0), "matched_count": result.get("matched_count", 0), "matched_entities": result.get("matched_entities", []), "nodes": result.get("nodes", []), "edges": result.get("edges", []), } except Exception as e: logger.error(f"Failed to retrieve semantic context: {e}") raise HTTPException(status_code=500, detail=str(e)) @graph_router.post("/subgraph") async def extract_subgraph( entity_id: int = Query(...), hops: int = Query(2, ge=1, le=3), min_confidence: float = Query(0.0, ge=0.0, le=1.0), ) -> Dict[str, Any]: """N-hop 부분그래프 추출. Args: entity_id: 중심 엔티티 ID hops: 홉 수 (1-3) min_confidence: 최소 신뢰도 Returns: 부분그래프 정보 """ try: result = await subgraph_retriever.retrieve_neighborhood( entity_id=entity_id, hops=hops, min_confidence=min_confidence, ) return { "status": "success", "entity_id": entity_id, "hops": hops, "subgraph": result, } except Exception as e: logger.error(f"Failed to extract subgraph: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # 패턴 매칭 엔드포인트 # ============================================================================ @graph_router.post("/patterns/paths") async def find_paths( start_id: int = Query(...), end_id: int = Query(...), max_length: int = Query(5, ge=2, le=10), ) -> Dict[str, Any]: """두 엔티티 사이의 경로 찾기. Args: start_id: 시작 엔티티 ID end_id: 종료 엔티티 ID max_length: 최대 경로 길이 Returns: 경로 리스트 """ try: paths = await pattern_matcher.find_paths( start_entity_id=start_id, end_entity_id=end_id, max_length=max_length, ) return { "status": "success", "start_id": start_id, "end_id": end_id, "paths_found": len(paths), "paths": [ { "path": p.path if hasattr(p, "path") else [], "length": p.length if hasattr(p, "length") else 0, "confidence": p.confidence if hasattr(p, "confidence") else 0, } for p in paths ] if paths else [], } except Exception as e: logger.error(f"Failed to find paths: {e}") raise HTTPException(status_code=500, detail=str(e)) @graph_router.post("/patterns/cycles") async def find_cycles() -> Dict[str, Any]: """순환 경로 감지. Returns: 순환 리스트 """ try: cycles = await pattern_matcher.find_cycles() return { "status": "success", "cycles_found": len(cycles), "cycles": cycles, } except Exception as e: logger.error(f"Failed to find cycles: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # 그래프 분석 엔드포인트 # ============================================================================ @graph_router.post("/analytics/centrality") async def analyze_centrality( centrality_type: str = Query("pagerank", pattern="^(degree|betweenness|closeness|pagerank)$"), top_n: int = Query(10, ge=1, le=100), ) -> Dict[str, Any]: """엔티티 중심성 분석. Args: centrality_type: 중심성 타입 top_n: 상위 N개 반환 Returns: 중심성 결과 """ try: results = await graph_analytics.calculate_centrality( centrality_type=centrality_type, ) # 상위 N개만 반환 top_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)[ :top_n ] return { "status": "success", "centrality_type": centrality_type, "total_entities": len(results), "top_n": top_n, "results": top_results, } except Exception as e: logger.error(f"Failed to analyze centrality: {e}") raise HTTPException(status_code=500, detail=str(e)) @graph_router.post("/analytics/communities") async def detect_communities( algorithm: str = Query("louvain", pattern="^(louvain|leiden)$"), ) -> Dict[str, Any]: """커뮤니티 감지. Args: algorithm: 알고리즘 (louvain, leiden) Returns: 커뮤니티 결과 """ try: communities = await graph_analytics.detect_communities(algorithm=algorithm) return { "status": "success", "algorithm": algorithm, "communities_found": len(communities), "communities": [ { "community_id": c.community_id if hasattr(c, "community_id") else "", "size": c.size if hasattr(c, "size") else 0, "density": c.density if hasattr(c, "density") else 0, } for c in communities ], } except Exception as e: logger.error(f"Failed to detect communities: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # 헬스 체크 # ============================================================================ @app.get("/health") async def health_check() -> Dict[str, Any]: """헬스 체크.""" return { "status": "healthy", "version": "0.5.0", "phase": "5 (GraphRAG)", "components": { "entity_resolver": "ok", "rdf_converter": "ok", "subgraph_retriever": "ok", "pattern_matcher": "ok", "graph_analytics": "ok", }, } @app.get("/info") async def get_platform_info() -> Dict[str, Any]: """플랫폼 정보.""" return { "platform": "Ontology System Construction Platform", "phase": "5 (GraphRAG)", "version": "0.5.0", "features": { "entity_resolution": True, "subgraph_retrieval": True, "pattern_matching": True, "graph_analytics": True, }, } # ============================================================================ # 라우터 등록 # ============================================================================ app.include_router(graph_router) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8003)