Files
gt-ai-os-community/apps/tenant-backend/app/api/v1/rag_visualization.py
HackWeasel 310491a557 GT AI OS Community v2.0.33 - Add NVIDIA NIM and Nemotron agents
- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2
- Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2
  - Made more general-purpose (flexible targets, expanded tools)
- Added nemotron-mini-agent.csv for fast local inference via Ollama
- Added nemotron-agent.csv for advanced reasoning via Ollama
- Added wiki page: Projects for NVIDIA NIMs and Nemotron
2025-12-12 17:47:14 -05:00

535 lines
20 KiB
Python

"""
RAG Network Visualization API for GT 2.0
Provides force-directed graph data and semantic relationships
"""
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db_session
from app.core.security import get_current_user
from app.services.rag_service import RAGService
import random
import math
import json
import uuid
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/rag/visualization", tags=["rag-visualization"])
class NetworkNode:
"""Represents a node in the knowledge network"""
def __init__(self, id: str, label: str, type: str, metadata: Dict[str, Any]):
self.id = id
self.label = label
self.type = type # document, chunk, concept, query
self.metadata = metadata
self.x = random.uniform(-100, 100)
self.y = random.uniform(-100, 100)
self.size = self._calculate_size(type, metadata)
self.color = self._get_color_by_type(type)
self.importance = metadata.get('importance', 0.5)
def _calculate_size(self, type: str, metadata: Dict[str, Any]) -> int:
"""Calculate node size based on type and metadata"""
base_sizes = {
"document": 20,
"chunk": 10,
"concept": 15,
"query": 25
}
base_size = base_sizes.get(type, 10)
# Adjust based on importance/usage
if 'usage_count' in metadata:
usage_multiplier = min(2.0, 1.0 + metadata['usage_count'] / 10.0)
base_size = int(base_size * usage_multiplier)
if 'relevance_score' in metadata:
relevance_multiplier = 0.5 + metadata['relevance_score'] * 0.5
base_size = int(base_size * relevance_multiplier)
return max(5, min(50, base_size))
def _get_color_by_type(self, type: str) -> str:
"""Get color based on node type"""
colors = {
"document": "#00d084", # GT Green
"chunk": "#677489", # GT Gray
"concept": "#4a5568", # Darker gray
"query": "#ff6b6b", # Red for queries
"dataset": "#4ade80", # Light green
"user": "#3b82f6" # Blue
}
return colors.get(type, "#9aa5b1")
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization"""
return {
"id": self.id,
"label": self.label,
"type": self.type,
"x": float(self.x),
"y": float(self.y),
"size": self.size,
"color": self.color,
"importance": self.importance,
"metadata": self.metadata
}
class NetworkEdge:
"""Represents an edge in the knowledge network"""
def __init__(self, source: str, target: str, weight: float, edge_type: str = "semantic"):
self.source = source
self.target = target
self.weight = max(0.0, min(1.0, weight)) # Clamp to 0-1
self.type = edge_type
self.color = self._get_edge_color(weight)
self.width = self._get_edge_width(weight)
self.animated = weight > 0.7
def _get_edge_color(self, weight: float) -> str:
"""Get edge color based on weight"""
if weight > 0.8:
return "#00d084" # Strong connection - GT green
elif weight > 0.6:
return "#4ade80" # Medium-strong - light green
elif weight > 0.4:
return "#9aa5b1" # Medium - gray
else:
return "#d1d9e0" # Weak - light gray
def _get_edge_width(self, weight: float) -> float:
"""Get edge width based on weight"""
return 1.0 + (weight * 4.0) # 1-5px width
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization"""
return {
"source": self.source,
"target": self.target,
"weight": self.weight,
"type": self.type,
"color": self.color,
"width": self.width,
"animated": self.animated
}
@router.get("/network/{dataset_id}")
async def get_knowledge_network(
dataset_id: str,
max_nodes: int = Query(default=100, le=500),
min_similarity: float = Query(default=0.3, ge=0, le=1),
include_concepts: bool = Query(default=True),
layout_algorithm: str = Query(default="force", description="force, circular, hierarchical"),
db: AsyncSession = Depends(get_db_session),
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""
Get force-directed graph data for a RAG dataset
Returns nodes (documents/chunks/concepts) and edges (semantic relationships)
"""
try:
# rag_service = RAGService(db)
user_id = current_user["sub"]
tenant_id = current_user.get("tenant_id", "default")
# TODO: Verify dataset ownership and access permissions
# dataset = await rag_service._get_user_dataset(user_id, dataset_id)
# if not dataset:
# raise HTTPException(status_code=404, detail="Dataset not found")
# For now, generate mock data that represents the expected structure
nodes = []
edges = []
# Generate mock document nodes
doc_count = min(max_nodes // 3, 20)
for i in range(doc_count):
doc_node = NetworkNode(
id=f"doc_{i}",
label=f"Document {i+1}",
type="document",
metadata={
"filename": f"document_{i+1}.pdf",
"size_bytes": random.randint(1000, 100000),
"chunk_count": random.randint(5, 50),
"upload_date": datetime.utcnow().isoformat(),
"usage_count": random.randint(0, 100),
"relevance_score": random.uniform(0.3, 1.0)
}
)
nodes.append(doc_node.to_dict())
# Generate chunks for this document
chunk_count = min(5, (max_nodes - len(nodes)) // (doc_count - i))
for j in range(chunk_count):
chunk_node = NetworkNode(
id=f"chunk_{i}_{j}",
label=f"Chunk {j+1}",
type="chunk",
metadata={
"document_id": f"doc_{i}",
"chunk_index": j,
"token_count": random.randint(50, 500),
"semantic_density": random.uniform(0.2, 0.9)
}
)
nodes.append(chunk_node.to_dict())
# Connect chunk to document
edge = NetworkEdge(
source=f"doc_{i}",
target=f"chunk_{i}_{j}",
weight=1.0,
edge_type="contains"
)
edges.append(edge.to_dict())
# Generate concept nodes if requested
if include_concepts:
concept_count = min(10, max_nodes - len(nodes))
concepts = ["AI", "Machine Learning", "Neural Networks", "Data Science",
"Python", "JavaScript", "API", "Database", "Security", "Cloud"]
for i in range(concept_count):
if i < len(concepts):
concept_node = NetworkNode(
id=f"concept_{i}",
label=concepts[i],
type="concept",
metadata={
"frequency": random.randint(1, 50),
"co_occurrence_score": random.uniform(0.1, 0.8),
"domain": "technology"
}
)
nodes.append(concept_node.to_dict())
# Generate semantic relationships between chunks
chunk_nodes = [n for n in nodes if n["type"] == "chunk"]
relationship_count = 0
max_relationships = min(len(chunk_nodes) * 2, 100)
for i, node1 in enumerate(chunk_nodes):
if relationship_count >= max_relationships:
break
# Connect to a few other chunks based on semantic similarity
connection_count = random.randint(1, 4)
for _ in range(connection_count):
if relationship_count >= max_relationships:
break
# Select random target (avoid self-connection)
target_idx = random.randint(0, len(chunk_nodes) - 1)
if target_idx == i:
continue
node2 = chunk_nodes[target_idx]
# Generate similarity score (higher for nodes with related content)
similarity = random.uniform(0.2, 0.9)
# Apply minimum similarity filter
if similarity >= min_similarity:
edge = NetworkEdge(
source=node1["id"],
target=node2["id"],
weight=similarity,
edge_type="semantic_similarity"
)
edges.append(edge.to_dict())
relationship_count += 1
# Connect concepts to relevant chunks
concept_nodes = [n for n in nodes if n["type"] == "concept"]
for concept in concept_nodes:
# Connect to 2-5 relevant chunks
connection_count = random.randint(2, 6)
connected_chunks = random.sample(
range(len(chunk_nodes)),
k=min(connection_count, len(chunk_nodes))
)
for chunk_idx in connected_chunks:
chunk = chunk_nodes[chunk_idx]
relevance = random.uniform(0.4, 0.9)
edge = NetworkEdge(
source=concept["id"],
target=chunk["id"],
weight=relevance,
edge_type="concept_relevance"
)
edges.append(edge.to_dict())
# Apply layout algorithm positioning
if layout_algorithm == "circular":
_apply_circular_layout(nodes)
elif layout_algorithm == "hierarchical":
_apply_hierarchical_layout(nodes)
# force layout uses random positioning (already applied)
return {
"dataset_id": dataset_id,
"nodes": nodes,
"edges": edges,
"metadata": {
"dataset_name": f"Dataset {dataset_id}",
"total_nodes": len(nodes),
"total_edges": len(edges),
"node_types": {
node_type: len([n for n in nodes if n["type"] == node_type])
for node_type in set(n["type"] for n in nodes)
},
"edge_types": {
edge_type: len([e for e in edges if e["type"] == edge_type])
for edge_type in set(e["type"] for e in edges)
},
"min_similarity": min_similarity,
"layout_algorithm": layout_algorithm,
"generated_at": datetime.utcnow().isoformat()
}
}
except Exception as e:
logger.error(f"Error generating knowledge network: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/search/visual")
async def visual_search(
query: str,
dataset_ids: Optional[List[str]] = Query(default=None),
top_k: int = Query(default=10, le=50),
include_network: bool = Query(default=True),
db: AsyncSession = Depends(get_db_session),
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""
Perform semantic search with visualization metadata
Returns search results with connection paths and confidence scores
"""
try:
# rag_service = RAGService(db)
user_id = current_user["sub"]
tenant_id = current_user.get("tenant_id", "default")
# TODO: Perform actual search using RAG service
# results = await rag_service.search_documents(
# user_id=user_id,
# tenant_id=tenant_id,
# query=query,
# dataset_ids=dataset_ids,
# top_k=top_k
# )
# Generate mock search results for now
results = []
for i in range(top_k):
similarity = random.uniform(0.5, 0.95) # Mock similarity scores
results.append({
"id": f"result_{i}",
"document_id": f"doc_{random.randint(0, 9)}",
"chunk_id": f"chunk_{i}",
"content": f"Mock search result {i+1} for query: {query}",
"metadata": {
"filename": f"document_{i}.pdf",
"page_number": random.randint(1, 100),
"chunk_index": i
},
"similarity": similarity,
"dataset_id": dataset_ids[0] if dataset_ids else "default"
})
# Enhance results with visualization data
visual_results = []
for i, result in enumerate(results):
visual_result = {
**result,
"visual_metadata": {
"position": i + 1,
"relevance_score": result["similarity"],
"confidence_level": _calculate_confidence(result["similarity"]),
"connection_strength": result["similarity"],
"highlight_color": _get_relevance_color(result["similarity"]),
"path_to_query": _generate_path(query, result),
"semantic_distance": 1.0 - result["similarity"],
"cluster_id": f"cluster_{random.randint(0, 2)}"
}
}
visual_results.append(visual_result)
response = {
"query": query,
"results": visual_results,
"total_results": len(visual_results),
"search_metadata": {
"execution_time_ms": random.randint(50, 200),
"datasets_searched": dataset_ids or ["all"],
"semantic_method": "embedding_similarity",
"reranking_applied": True
}
}
# Add network visualization if requested
if include_network:
network_data = _generate_search_network(query, visual_results)
response["network_visualization"] = network_data
return response
except Exception as e:
logger.error(f"Error in visual search: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/datasets/{dataset_id}/stats")
async def get_dataset_stats(
dataset_id: str,
db: AsyncSession = Depends(get_db_session),
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""Get statistical information about a dataset for visualization"""
try:
# TODO: Implement actual dataset statistics
return {
"dataset_id": dataset_id,
"document_count": random.randint(10, 100),
"chunk_count": random.randint(100, 1000),
"total_tokens": random.randint(10000, 100000),
"concept_count": random.randint(20, 200),
"average_document_similarity": random.uniform(0.3, 0.8),
"semantic_clusters": random.randint(3, 10),
"most_connected_documents": [
{"id": f"doc_{i}", "connections": random.randint(5, 50)}
for i in range(5)
],
"topic_distribution": {
f"topic_{i}": random.uniform(0.05, 0.3)
for i in range(5)
},
"last_updated": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error(f"Error getting dataset stats: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# Helper functions
def _calculate_confidence(similarity: float) -> str:
"""Calculate confidence level from similarity score"""
if similarity > 0.9:
return "very_high"
elif similarity > 0.75:
return "high"
elif similarity > 0.6:
return "medium"
elif similarity > 0.4:
return "low"
else:
return "very_low"
def _get_relevance_color(similarity: float) -> str:
"""Get color based on relevance score"""
if similarity > 0.8:
return "#00d084" # GT Green
elif similarity > 0.6:
return "#4ade80" # Light green
elif similarity > 0.4:
return "#fbbf24" # Yellow
else:
return "#ef4444" # Red
def _generate_path(query: str, result: Dict[str, Any]) -> List[str]:
"""Generate conceptual path from query to result"""
return [
"query",
f"dataset_{result.get('dataset_id', 'unknown')}",
f"document_{result.get('document_id', 'unknown')}",
f"chunk_{result.get('chunk_id', 'unknown')}"
]
def _generate_search_network(query: str, results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Generate network visualization for search results"""
nodes = []
edges = []
# Add query node
query_node = NetworkNode(
id="query",
label=query[:50] + "..." if len(query) > 50 else query,
type="query",
metadata={"original_query": query}
)
nodes.append(query_node.to_dict())
# Add result nodes and connections
for i, result in enumerate(results):
result_node = NetworkNode(
id=f"result_{i}",
label=result.get("content", "")[:30] + "...",
type="chunk",
metadata={
"similarity": result["similarity"],
"position": i + 1,
"document_id": result.get("document_id")
}
)
nodes.append(result_node.to_dict())
# Connect query to result
edge = NetworkEdge(
source="query",
target=f"result_{i}",
weight=result["similarity"],
edge_type="search_result"
)
edges.append(edge.to_dict())
return {
"nodes": nodes,
"edges": edges,
"center_node_id": "query",
"layout": "radial"
}
def _apply_circular_layout(nodes: List[Dict[str, Any]]) -> None:
"""Apply circular layout to nodes"""
if not nodes:
return
radius = 150
angle_step = 2 * math.pi / len(nodes)
for i, node in enumerate(nodes):
angle = i * angle_step
node["x"] = radius * math.cos(angle)
node["y"] = radius * math.sin(angle)
def _apply_hierarchical_layout(nodes: List[Dict[str, Any]]) -> None:
"""Apply hierarchical layout to nodes"""
if not nodes:
return
# Group by type
node_types = {}
for node in nodes:
node_type = node["type"]
if node_type not in node_types:
node_types[node_type] = []
node_types[node_type].append(node)
# Position by type levels
y_positions = {"document": -100, "concept": 0, "chunk": 100, "query": -150}
for node_type, type_nodes in node_types.items():
y_pos = y_positions.get(node_type, 0)
x_step = 300 / max(1, len(type_nodes) - 1) if len(type_nodes) > 1 else 0
for i, node in enumerate(type_nodes):
node["y"] = y_pos
node["x"] = -150 + (i * x_step) if len(type_nodes) > 1 else 0