feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint - usePresenceHeartbeat hook with activity tracking - Auto away after 5 min inactivity - Offline on page close/visibility change - Integrated in MatrixChatRoom component
This commit is contained in:
277
services/node2-rag-router/rag_router.py
Normal file
277
services/node2-rag-router/rag_router.py
Normal file
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Local RAG Router for microDAO Node-2
|
||||
Routes queries to appropriate memory stores:
|
||||
- local.memory.qdrant - Fast RAG
|
||||
- local.memory.milvus - Heavy indexing
|
||||
- local.graph.neo4j - Graph queries
|
||||
- global.memory - Node-1 (optional)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any
|
||||
from enum import Enum
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MemoryTarget(str, Enum):
|
||||
"""Memory store targets"""
|
||||
QDRANT = "local.memory.qdrant"
|
||||
MILVUS = "local.memory.milvus"
|
||||
NEO4J = "local.graph.neo4j"
|
||||
GLOBAL = "global.memory"
|
||||
|
||||
class QueryType(str, Enum):
|
||||
"""Query types"""
|
||||
VECTOR_SEARCH = "vector_search"
|
||||
GRAPH_QUERY = "graph_query"
|
||||
HYBRID = "hybrid"
|
||||
METADATA = "metadata"
|
||||
|
||||
class RAGQuery(BaseModel):
|
||||
"""RAG query model"""
|
||||
query: str
|
||||
query_type: QueryType = QueryType.VECTOR_SEARCH
|
||||
limit: int = 10
|
||||
filters: Optional[Dict[str, Any]] = None
|
||||
target: Optional[MemoryTarget] = None
|
||||
use_global: bool = False
|
||||
|
||||
class RAGRouter:
|
||||
"""Local RAG Router for microDAO Node-2"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
qdrant_url: str = "http://localhost:6333",
|
||||
milvus_host: str = "localhost",
|
||||
milvus_port: int = 19530,
|
||||
neo4j_uri: str = "bolt://localhost:7687",
|
||||
neo4j_user: str = "neo4j",
|
||||
neo4j_password: str = "microdao-node2",
|
||||
global_memory_url: Optional[str] = None
|
||||
):
|
||||
self.qdrant_url = qdrant_url
|
||||
self.milvus_host = milvus_host
|
||||
self.milvus_port = milvus_port
|
||||
self.neo4j_uri = neo4j_uri
|
||||
self.neo4j_user = neo4j_user
|
||||
self.neo4j_password = neo4j_password
|
||||
self.global_memory_url = global_memory_url
|
||||
|
||||
self.http_client = httpx.AsyncClient(timeout=30.0)
|
||||
|
||||
def _determine_target(self, query: RAGQuery) -> MemoryTarget:
|
||||
"""Determine target memory store based on query"""
|
||||
# If target explicitly specified
|
||||
if query.target:
|
||||
return query.target
|
||||
|
||||
# Route based on query type
|
||||
if query.query_type == QueryType.GRAPH_QUERY:
|
||||
return MemoryTarget.NEO4J
|
||||
|
||||
if query.query_type == QueryType.VECTOR_SEARCH:
|
||||
# Use Qdrant for fast queries, Milvus for heavy indexing
|
||||
if query.limit <= 50 and not query.filters:
|
||||
return MemoryTarget.QDRANT
|
||||
else:
|
||||
return MemoryTarget.MILVUS
|
||||
|
||||
if query.query_type == QueryType.HYBRID:
|
||||
# Hybrid queries can use both
|
||||
return MemoryTarget.QDRANT # Default to Qdrant for speed
|
||||
|
||||
# Default to Qdrant
|
||||
return MemoryTarget.QDRANT
|
||||
|
||||
async def _query_qdrant(self, query: RAGQuery) -> List[Dict[str, Any]]:
|
||||
"""Query Qdrant vector database"""
|
||||
try:
|
||||
# First, get collection info
|
||||
collections_response = await self.http_client.get(
|
||||
f"{self.qdrant_url}/collections"
|
||||
)
|
||||
collections = collections_response.json().get("result", {}).get("collections", [])
|
||||
|
||||
if not collections:
|
||||
logger.warning("No collections found in Qdrant")
|
||||
return []
|
||||
|
||||
# Use first collection (or implement collection selection logic)
|
||||
collection_name = collections[0].get("name", "default")
|
||||
|
||||
# For now, return empty - actual vector search requires embeddings
|
||||
# This is a placeholder for the actual implementation
|
||||
logger.info(f"Querying Qdrant collection: {collection_name}")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying Qdrant: {e}")
|
||||
return []
|
||||
|
||||
async def _query_milvus(self, query: RAGQuery) -> List[Dict[str, Any]]:
|
||||
"""Query Milvus vector database"""
|
||||
try:
|
||||
# Milvus query implementation
|
||||
# This requires pymilvus client
|
||||
logger.info(f"Querying Milvus (not yet implemented)")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying Milvus: {e}")
|
||||
return []
|
||||
|
||||
async def _query_neo4j(self, query: RAGQuery) -> List[Dict[str, Any]]:
|
||||
"""Query Neo4j graph database"""
|
||||
try:
|
||||
# Neo4j query implementation
|
||||
# This requires neo4j driver
|
||||
logger.info(f"Querying Neo4j (not yet implemented)")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying Neo4j: {e}")
|
||||
return []
|
||||
|
||||
async def _query_global(self, query: RAGQuery) -> List[Dict[str, Any]]:
|
||||
"""Query global memory on Node-1"""
|
||||
if not self.global_memory_url:
|
||||
logger.warning("Global memory URL not configured")
|
||||
return []
|
||||
|
||||
try:
|
||||
response = await self.http_client.post(
|
||||
f"{self.global_memory_url}/query",
|
||||
json=query.dict()
|
||||
)
|
||||
return response.json().get("results", [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying global memory: {e}")
|
||||
return []
|
||||
|
||||
async def route_query(self, query: RAGQuery) -> Dict[str, Any]:
|
||||
"""Route query to appropriate memory store"""
|
||||
target = self._determine_target(query)
|
||||
|
||||
logger.info(f"Routing query to: {target}")
|
||||
|
||||
results = []
|
||||
|
||||
# Query primary target
|
||||
if target == MemoryTarget.QDRANT:
|
||||
results = await self._query_qdrant(query)
|
||||
elif target == MemoryTarget.MILVUS:
|
||||
results = await self._query_milvus(query)
|
||||
elif target == MemoryTarget.NEO4J:
|
||||
results = await self._query_neo4j(query)
|
||||
|
||||
# Optionally query global memory
|
||||
if query.use_global and self.global_memory_url:
|
||||
global_results = await self._query_global(query)
|
||||
results.extend(global_results)
|
||||
|
||||
return {
|
||||
"target": target.value,
|
||||
"results": results,
|
||||
"count": len(results)
|
||||
}
|
||||
|
||||
async def health_check(self) -> Dict[str, bool]:
|
||||
"""Check health of all memory stores"""
|
||||
health = {
|
||||
"qdrant": False,
|
||||
"milvus": False,
|
||||
"neo4j": False,
|
||||
"global": False
|
||||
}
|
||||
|
||||
# Check Qdrant
|
||||
try:
|
||||
response = await self.http_client.get(f"{self.qdrant_url}/health")
|
||||
health["qdrant"] = response.status_code == 200
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check Milvus (placeholder)
|
||||
health["milvus"] = True # TODO: Implement actual health check
|
||||
|
||||
# Check Neo4j (placeholder)
|
||||
health["neo4j"] = True # TODO: Implement actual health check
|
||||
|
||||
# Check Global (placeholder)
|
||||
if self.global_memory_url:
|
||||
try:
|
||||
response = await self.http_client.get(f"{self.global_memory_url}/health")
|
||||
health["global"] = response.status_code == 200
|
||||
except:
|
||||
pass
|
||||
|
||||
return health
|
||||
|
||||
async def close(self):
|
||||
"""Close HTTP client"""
|
||||
await self.http_client.aclose()
|
||||
|
||||
# FastAPI integration
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
app = FastAPI(title="microDAO Node-2 RAG Router")
|
||||
|
||||
# Global router instance
|
||||
router: Optional[RAGRouter] = None
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
"""Initialize router on startup"""
|
||||
global router
|
||||
router = RAGRouter(
|
||||
global_memory_url=None # Configure from environment
|
||||
)
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
"""Close router on shutdown"""
|
||||
global router
|
||||
if router:
|
||||
await router.close()
|
||||
|
||||
@app.post("/query")
|
||||
async def query_endpoint(query: RAGQuery):
|
||||
"""Query endpoint"""
|
||||
if not router:
|
||||
raise HTTPException(status_code=503, detail="Router not initialized")
|
||||
|
||||
try:
|
||||
result = await router.route_query(query)
|
||||
return JSONResponse(content=result)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in query endpoint: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/health")
|
||||
async def health_endpoint():
|
||||
"""Health check endpoint"""
|
||||
if not router:
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"status": "unhealthy", "reason": "Router not initialized"}
|
||||
)
|
||||
|
||||
health = await router.health_check()
|
||||
all_healthy = all(health.values())
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200 if all_healthy else 503,
|
||||
content={
|
||||
"status": "healthy" if all_healthy else "degraded",
|
||||
"stores": health
|
||||
}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=9401)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user