781 lines
21 KiB
Python
781 lines
21 KiB
Python
|
|
"""Phase 6 FastAPI application: Graph API + GraphQL + RAG Pipeline.
|
||
|
|
|
||
|
|
Features:
|
||
|
|
- REST API for graph operations (entity resolution, subgraph, patterns, analytics)
|
||
|
|
- GraphQL endpoint for flexible queries
|
||
|
|
- RAG pipeline integrating with LLM
|
||
|
|
"""
|
||
|
|
|
||
|
|
from fastapi import FastAPI, APIRouter, HTTPException, Query, Request
|
||
|
|
from fastapi.responses import JSONResponse
|
||
|
|
from typing import Optional, List, Dict, Any
|
||
|
|
import time
|
||
|
|
import asyncio
|
||
|
|
import logging
|
||
|
|
import json
|
||
|
|
|
||
|
|
from ont_platform.core.graph.neo4j_adapter import Neo4jAdapter, Neo4jConfig
|
||
|
|
from ont_platform.core.graph.entity_resolver import EntityResolver
|
||
|
|
from ont_platform.core.graph.subgraph_retriever import SubgraphRetriever
|
||
|
|
from ont_platform.core.graph.pattern_matcher import PatternMatcher
|
||
|
|
from ont_platform.core.graph.graph_analytics import GraphAnalytics
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
app = FastAPI(
|
||
|
|
title="Ontology Platform - Phase 6 GraphRAG",
|
||
|
|
description="Graph API + GraphQL + RAG Pipeline",
|
||
|
|
version="0.6.0",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Routers
|
||
|
|
graph_router = APIRouter(prefix="/api/v1/graph", tags=["graph"])
|
||
|
|
rag_router = APIRouter(prefix="/api/v1/rag", tags=["rag"])
|
||
|
|
|
||
|
|
# Global instances
|
||
|
|
_neo4j_adapter: Optional[Neo4jAdapter] = None
|
||
|
|
_entity_resolver: Optional[EntityResolver] = None
|
||
|
|
_subgraph_retriever: Optional[SubgraphRetriever] = None
|
||
|
|
_pattern_matcher: Optional[PatternMatcher] = None
|
||
|
|
_graph_analytics: Optional[GraphAnalytics] = None
|
||
|
|
|
||
|
|
|
||
|
|
async def get_neo4j_adapter() -> Neo4jAdapter:
|
||
|
|
"""Get or create Neo4j adapter instance."""
|
||
|
|
global _neo4j_adapter
|
||
|
|
if _neo4j_adapter is None:
|
||
|
|
config = Neo4jConfig(
|
||
|
|
uri="bolt://localhost:7687",
|
||
|
|
username="neo4j",
|
||
|
|
password="ontology123",
|
||
|
|
)
|
||
|
|
_neo4j_adapter = Neo4jAdapter(config)
|
||
|
|
if not await _neo4j_adapter.connect():
|
||
|
|
logger.warning("Neo4j not available")
|
||
|
|
else:
|
||
|
|
try:
|
||
|
|
await _neo4j_adapter.initialize_embedder()
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Failed to initialize embedder: {e}")
|
||
|
|
return _neo4j_adapter
|
||
|
|
|
||
|
|
|
||
|
|
async def get_components():
|
||
|
|
"""Initialize all graph components."""
|
||
|
|
global _entity_resolver, _subgraph_retriever, _pattern_matcher, _graph_analytics
|
||
|
|
|
||
|
|
adapter = await get_neo4j_adapter()
|
||
|
|
|
||
|
|
if _entity_resolver is None:
|
||
|
|
_entity_resolver = EntityResolver()
|
||
|
|
await _entity_resolver.initialize_embedder()
|
||
|
|
|
||
|
|
if _subgraph_retriever is None:
|
||
|
|
_subgraph_retriever = SubgraphRetriever(adapter)
|
||
|
|
|
||
|
|
if _pattern_matcher is None:
|
||
|
|
_pattern_matcher = PatternMatcher(adapter)
|
||
|
|
|
||
|
|
if _graph_analytics is None:
|
||
|
|
_graph_analytics = GraphAnalytics(adapter)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"adapter": adapter,
|
||
|
|
"resolver": _entity_resolver,
|
||
|
|
"retriever": _subgraph_retriever,
|
||
|
|
"matcher": _pattern_matcher,
|
||
|
|
"analytics": _graph_analytics,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Entity Resolution Endpoints
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.post("/resolve")
|
||
|
|
async def resolve_entities(
|
||
|
|
entities: List[Dict[str, Any]],
|
||
|
|
vector_threshold: float = Query(0.85),
|
||
|
|
text_threshold: float = Query(0.88),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Detect and resolve duplicate entities.
|
||
|
|
|
||
|
|
Request:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"entities": [
|
||
|
|
{"id": 1, "label": "Apple Inc.", "type": "Company"},
|
||
|
|
{"id": 2, "label": "Apple Inc", "type": "Company"}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Response:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"clusters": [
|
||
|
|
{
|
||
|
|
"cluster_id": "C_1_2",
|
||
|
|
"canonical_id": 1,
|
||
|
|
"duplicates": [2],
|
||
|
|
"confidence": 0.92,
|
||
|
|
"reason": "combined"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
resolver = components["resolver"]
|
||
|
|
|
||
|
|
resolver.vector_threshold = vector_threshold
|
||
|
|
resolver.text_threshold = text_threshold
|
||
|
|
|
||
|
|
clusters = await resolver.detect_duplicates(entities)
|
||
|
|
|
||
|
|
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
|
||
|
|
],
|
||
|
|
"total_clusters": len(clusters),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Entity resolution failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Subgraph Retrieval Endpoints
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.get("/subgraph/neighborhood/{entity_id}")
|
||
|
|
async def get_neighborhood(
|
||
|
|
entity_id: int,
|
||
|
|
hops: int = Query(2, ge=1, le=3),
|
||
|
|
limit: int = Query(500),
|
||
|
|
min_confidence: float = Query(0.0),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Extract N-hop neighborhood around an entity.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"center_entity": {...},
|
||
|
|
"nodes": [{id, label, type, confidence}, ...],
|
||
|
|
"edges": [{source_id, target_id, predicate, confidence}, ...],
|
||
|
|
"node_count": 125,
|
||
|
|
"edge_count": 287
|
||
|
|
}
|
||
|
|
```
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
retriever = components["retriever"]
|
||
|
|
|
||
|
|
result = await retriever.retrieve_neighborhood(
|
||
|
|
entity_id=entity_id,
|
||
|
|
hops=hops,
|
||
|
|
limit=limit,
|
||
|
|
min_confidence=min_confidence,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"data": result,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Subgraph retrieval failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.post("/subgraph/context")
|
||
|
|
async def get_context(
|
||
|
|
entity_ids: List[int],
|
||
|
|
context_hops: int = Query(2, ge=1, le=3),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Find common context between multiple entities.
|
||
|
|
|
||
|
|
Request:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"entity_ids": [1, 2, 3]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
retriever = components["retriever"]
|
||
|
|
|
||
|
|
result = await retriever.retrieve_context(
|
||
|
|
entity_ids=entity_ids,
|
||
|
|
context_hops=context_hops,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"data": result,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Context retrieval failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Pattern Matching Endpoints
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.post("/patterns/paths")
|
||
|
|
async def find_paths(
|
||
|
|
start_id: int,
|
||
|
|
end_id: int,
|
||
|
|
max_length: int = Query(5, ge=2, le=6),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Find all paths between two entities.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"paths": [
|
||
|
|
{"path": [1, 2, 3, 5], "length": 3, "confidence": 0.87},
|
||
|
|
{"path": [1, 4, 5], "length": 2, "confidence": 0.91}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
matcher = components["matcher"]
|
||
|
|
|
||
|
|
paths = await matcher.find_paths(
|
||
|
|
start_entity_id=start_id,
|
||
|
|
end_entity_id=end_id,
|
||
|
|
max_length=max_length,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"paths": paths,
|
||
|
|
"total_paths": len(paths),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Path finding failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.post("/patterns/cycles")
|
||
|
|
async def find_cycles(
|
||
|
|
min_length: int = Query(2, ge=2),
|
||
|
|
max_length: int = Query(5, ge=2, le=6),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Detect cycles in the knowledge graph.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
matcher = components["matcher"]
|
||
|
|
|
||
|
|
cycles = await matcher.find_cycles(
|
||
|
|
min_length=min_length,
|
||
|
|
max_length=max_length,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"cycles": cycles,
|
||
|
|
"total_cycles": len(cycles),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Cycle detection failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.post("/patterns/motifs")
|
||
|
|
async def find_motifs(
|
||
|
|
motif_type: str = Query("triangle"),
|
||
|
|
limit: int = Query(100),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Detect graph motifs (triangle, chain, star).
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
matcher = components["matcher"]
|
||
|
|
|
||
|
|
motifs = await matcher.find_motifs(
|
||
|
|
motif_type=motif_type,
|
||
|
|
limit=limit,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"motif_type": motif_type,
|
||
|
|
"motifs": motifs,
|
||
|
|
"total_motifs": len(motifs),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Motif detection failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Graph Analytics Endpoints
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.post("/analytics/centrality")
|
||
|
|
async def calculate_centrality(
|
||
|
|
centrality_type: str = Query("pagerank"),
|
||
|
|
top_n: int = Query(100),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Calculate entity centrality metrics.
|
||
|
|
|
||
|
|
Types: degree, pagerank, betweenness, closeness
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
analytics = components["analytics"]
|
||
|
|
|
||
|
|
entities = await analytics.calculate_centrality(
|
||
|
|
centrality_type=centrality_type,
|
||
|
|
top_n=top_n,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"centrality_type": centrality_type,
|
||
|
|
"entities": entities,
|
||
|
|
"total_entities": len(entities),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Centrality calculation failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.post("/analytics/communities")
|
||
|
|
async def detect_communities(
|
||
|
|
algorithm: str = Query("louvain"),
|
||
|
|
min_size: int = Query(2),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Detect communities in the graph.
|
||
|
|
|
||
|
|
Algorithms: louvain, label_propagation
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
analytics = components["analytics"]
|
||
|
|
|
||
|
|
communities = await analytics.detect_communities(
|
||
|
|
algorithm=algorithm,
|
||
|
|
)
|
||
|
|
|
||
|
|
filtered = [c for c in communities if c["size"] >= min_size]
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"algorithm": algorithm,
|
||
|
|
"communities": filtered,
|
||
|
|
"total_communities": len(filtered),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Community detection failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.get("/analytics/statistics")
|
||
|
|
async def get_graph_statistics():
|
||
|
|
"""
|
||
|
|
Get overall graph statistics.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"total_nodes": 1000,
|
||
|
|
"total_edges": 5000,
|
||
|
|
"density": 0.01,
|
||
|
|
"diameter": 7,
|
||
|
|
"is_connected": true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
analytics = components["analytics"]
|
||
|
|
|
||
|
|
stats = await analytics.get_graph_statistics()
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"statistics": stats,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Statistics calculation failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@graph_router.get("/analytics/influential")
|
||
|
|
async def get_influential_entities(
|
||
|
|
top_n: int = Query(20),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Get most influential entities (composite score).
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
analytics = components["analytics"]
|
||
|
|
|
||
|
|
entities = await analytics.find_influential_entities(top_n=top_n)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"entities": entities,
|
||
|
|
"total_entities": len(entities),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Influential entity detection failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# RAG Pipeline Endpoints
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
@rag_router.post("/context-extraction")
|
||
|
|
async def extract_rag_context(
|
||
|
|
query_text: str,
|
||
|
|
entity_id: Optional[int] = None,
|
||
|
|
hops: int = Query(2, ge=1, le=3),
|
||
|
|
max_entities: int = Query(100),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Extract RAG context from knowledge graph.
|
||
|
|
|
||
|
|
If entity_id provided: use neighborhood
|
||
|
|
If query_text provided: search and extract context
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
retriever = components["retriever"]
|
||
|
|
analytics = components["analytics"]
|
||
|
|
|
||
|
|
if entity_id:
|
||
|
|
# Extract from known entity
|
||
|
|
context = await retriever.retrieve_neighborhood(
|
||
|
|
entity_id=entity_id,
|
||
|
|
hops=hops,
|
||
|
|
limit=max_entities,
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
# Search for query in entities (simple text match)
|
||
|
|
adapter = components["adapter"]
|
||
|
|
results = await adapter.vector_search(query_text, limit=5)
|
||
|
|
|
||
|
|
if not results:
|
||
|
|
return {
|
||
|
|
"status": "no_results",
|
||
|
|
"message": f"No entities found for: {query_text}",
|
||
|
|
"context": None,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Use top result for context
|
||
|
|
top_entity = results[0]
|
||
|
|
context = await retriever.retrieve_neighborhood(
|
||
|
|
entity_id=top_entity["id"],
|
||
|
|
hops=hops,
|
||
|
|
limit=max_entities,
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"query": query_text or f"entity_{entity_id}",
|
||
|
|
"context": context,
|
||
|
|
"context_size": len(context.get("nodes", [])),
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Context extraction failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@rag_router.post("/query")
|
||
|
|
async def rag_query(
|
||
|
|
query: str,
|
||
|
|
context_hops: int = Query(2),
|
||
|
|
use_graph_context: bool = Query(True),
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Process a RAG query with graph context.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"query": "What is Apple?",
|
||
|
|
"context": {...},
|
||
|
|
"llm_prompt": "...",
|
||
|
|
"ready_for_llm": true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Note: For LLM inference, send the llm_prompt to your LLM service.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
components = await get_components()
|
||
|
|
retriever = components["retriever"]
|
||
|
|
adapter = components["adapter"]
|
||
|
|
|
||
|
|
# Step 1: Search for relevant entities
|
||
|
|
search_results = await adapter.vector_search(query, limit=3)
|
||
|
|
|
||
|
|
if not search_results:
|
||
|
|
return {
|
||
|
|
"status": "no_results",
|
||
|
|
"message": "No relevant entities found",
|
||
|
|
"query": query,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Step 2: Extract context from top results
|
||
|
|
context_data = []
|
||
|
|
for result in search_results:
|
||
|
|
context = await retriever.retrieve_neighborhood(
|
||
|
|
entity_id=result["id"],
|
||
|
|
hops=context_hops,
|
||
|
|
limit=50,
|
||
|
|
)
|
||
|
|
context_data.append(
|
||
|
|
{
|
||
|
|
"entity": result,
|
||
|
|
"subgraph": context,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Step 3: Build LLM prompt
|
||
|
|
llm_prompt = _build_rag_prompt(query, context_data)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "success",
|
||
|
|
"query": query,
|
||
|
|
"relevant_entities": [r["label"] for r in search_results],
|
||
|
|
"context_nodes": sum(
|
||
|
|
len(c["subgraph"].get("nodes", [])) for c in context_data
|
||
|
|
),
|
||
|
|
"llm_prompt": llm_prompt,
|
||
|
|
"ready_for_llm": True,
|
||
|
|
"context": context_data if use_graph_context else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"RAG query failed: {e}")
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
def _build_rag_prompt(query: str, context_data: List[Dict]) -> str:
|
||
|
|
"""
|
||
|
|
Build a structured prompt for LLM with graph context.
|
||
|
|
"""
|
||
|
|
prompt = f"""You are a helpful assistant with access to a knowledge graph.
|
||
|
|
|
||
|
|
KNOWLEDGE GRAPH CONTEXT:
|
||
|
|
"""
|
||
|
|
|
||
|
|
for i, ctx in enumerate(context_data, 1):
|
||
|
|
entity = ctx["entity"]
|
||
|
|
subgraph = ctx["subgraph"]
|
||
|
|
|
||
|
|
prompt += f"\n--- Source Entity {i}: {entity['label']} ---\n"
|
||
|
|
prompt += f"Type: {entity['type']}\n"
|
||
|
|
prompt += f"Confidence: {entity['similarity']:.3f}\n"
|
||
|
|
|
||
|
|
if subgraph.get("nodes"):
|
||
|
|
prompt += f"\nRelated Entities ({len(subgraph['nodes'])} total):\n"
|
||
|
|
for node in subgraph["nodes"][:10]: # Show top 10
|
||
|
|
prompt += f" - {node['label']} (type: {node['type']})\n"
|
||
|
|
|
||
|
|
if subgraph.get("edges"):
|
||
|
|
prompt += f"\nRelationships ({len(subgraph['edges'])} total):\n"
|
||
|
|
for edge in subgraph["edges"][:5]: # Show top 5
|
||
|
|
prompt += (
|
||
|
|
f" - {edge['source_id']} --{edge['predicate']}--> "
|
||
|
|
f"{edge['target_id']} (confidence: {edge['confidence']:.2f})\n"
|
||
|
|
)
|
||
|
|
|
||
|
|
prompt += f"\nUSER QUERY: {query}\n\n"
|
||
|
|
prompt += "Based on the knowledge graph context above, please answer the user's query comprehensively.\n"
|
||
|
|
prompt += "If information is found in the graph, cite it. If not found, say so clearly.\n"
|
||
|
|
|
||
|
|
return prompt
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# GraphQL Endpoint (Simple Implementation)
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
@app.post("/graphql")
|
||
|
|
async def graphql_query(request: Request):
|
||
|
|
"""
|
||
|
|
Simple GraphQL endpoint for flexible graph queries.
|
||
|
|
|
||
|
|
Example query:
|
||
|
|
```graphql
|
||
|
|
{
|
||
|
|
entity(id: 1) {
|
||
|
|
id
|
||
|
|
label
|
||
|
|
type
|
||
|
|
neighbors(hops: 2) {
|
||
|
|
id
|
||
|
|
label
|
||
|
|
distance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
body = await request.json()
|
||
|
|
query = body.get("query", "")
|
||
|
|
variables = body.get("variables", {})
|
||
|
|
|
||
|
|
# Simple GraphQL parser (in production, use graphene or similar)
|
||
|
|
result = await _process_graphql(query, variables)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"data": result,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"GraphQL query failed: {e}")
|
||
|
|
return {
|
||
|
|
"errors": [{"message": str(e)}],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def _process_graphql(query: str, variables: Dict) -> Dict:
|
||
|
|
"""
|
||
|
|
Process GraphQL query (simplified implementation).
|
||
|
|
|
||
|
|
Supports:
|
||
|
|
- entity(id): Get entity with neighbors
|
||
|
|
- entities: List all entities
|
||
|
|
- communities: List detected communities
|
||
|
|
"""
|
||
|
|
components = await get_components()
|
||
|
|
|
||
|
|
# Simple parsing (in production, use proper GraphQL parser)
|
||
|
|
if "entity(" in query:
|
||
|
|
# Extract entity ID from query
|
||
|
|
import re
|
||
|
|
|
||
|
|
match = re.search(r"entity\(id:\s*(\d+)", query)
|
||
|
|
if match:
|
||
|
|
entity_id = int(match.group(1))
|
||
|
|
retriever = components["retriever"]
|
||
|
|
|
||
|
|
context = await retriever.retrieve_neighborhood(entity_id=entity_id)
|
||
|
|
return {
|
||
|
|
"entity": {
|
||
|
|
"id": entity_id,
|
||
|
|
"data": context,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
elif "communities" in query:
|
||
|
|
analytics = components["analytics"]
|
||
|
|
communities = await analytics.detect_communities()
|
||
|
|
return {"communities": communities}
|
||
|
|
|
||
|
|
return {"error": "Query not supported"}
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Health Check & Info Endpoints
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
@app.get("/health")
|
||
|
|
async def health_check():
|
||
|
|
"""Health check endpoint."""
|
||
|
|
try:
|
||
|
|
adapter = await get_neo4j_adapter()
|
||
|
|
neo4j_status = "connected" if adapter._driver else "disconnected"
|
||
|
|
except Exception as e:
|
||
|
|
neo4j_status = f"error: {str(e)}"
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "ok",
|
||
|
|
"version": "0.6.0",
|
||
|
|
"neo4j": neo4j_status,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@app.get("/info")
|
||
|
|
async def info():
|
||
|
|
"""API information."""
|
||
|
|
return {
|
||
|
|
"name": "Ontology Platform - Phase 6",
|
||
|
|
"version": "0.6.0",
|
||
|
|
"phase": 6,
|
||
|
|
"features": [
|
||
|
|
"REST API for graph operations",
|
||
|
|
"GraphQL endpoint",
|
||
|
|
"RAG pipeline integration",
|
||
|
|
"Entity resolution",
|
||
|
|
"Subgraph retrieval",
|
||
|
|
"Pattern matching",
|
||
|
|
"Graph analytics",
|
||
|
|
],
|
||
|
|
"endpoints": {
|
||
|
|
"graph": "/api/v1/graph",
|
||
|
|
"rag": "/api/v1/rag",
|
||
|
|
"graphql": "/graphql",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# Register routers
|
||
|
|
app.include_router(graph_router)
|
||
|
|
app.include_router(rag_router)
|
||
|
|
|
||
|
|
|
||
|
|
@app.on_event("shutdown")
|
||
|
|
async def shutdown_event():
|
||
|
|
"""Cleanup on shutdown."""
|
||
|
|
global _neo4j_adapter
|
||
|
|
if _neo4j_adapter:
|
||
|
|
await _neo4j_adapter.close()
|
||
|
|
logger.info("Neo4j connection closed")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
import uvicorn
|
||
|
|
|
||
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|