diff --git a/backend/app/api/rag_debug.py b/backend/app/api/rag_debug.py new file mode 100644 index 0000000..75a81ce --- /dev/null +++ b/backend/app/api/rag_debug.py @@ -0,0 +1,97 @@ +""" +RAG Debug API endpoints for testing and debugging +""" + +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Dict, Any, Optional +import logging + +from app.core.security import get_current_user +from app.core.config import settings +from app.modules.rag.main import RAGModule +from app.models.user import User + +logger = logging.getLogger(__name__) + +# Create router +router = APIRouter() + +@router.get("/collections") +async def list_collections( + current_user: User = Depends(get_current_user) +): + """List all available RAG collections""" + try: + from app.services.qdrant_stats_service import qdrant_stats_service + + # Get collections from Qdrant (same as main RAG API) + stats_data = await qdrant_stats_service.get_collections_stats() + collections = stats_data.get("collections", []) + + # Extract collection names + collection_names = [col["name"] for col in collections] + + return { + "collections": collection_names, + "count": len(collection_names) + } + + except Exception as e: + logger.error(f"List collections error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/search") +async def debug_search( + query: str = Query(..., description="Search query"), + max_results: int = Query(10, ge=1, le=50, description="Maximum number of results"), + score_threshold: float = Query(0.3, ge=0.0, le=1.0, description="Minimum score threshold"), + collection_name: Optional[str] = Query(None, description="Collection name to search"), + config: Optional[Dict[str, Any]] = None, + current_user: User = Depends(get_current_user) +): + """Debug search endpoint with detailed information""" + try: + # Get configuration + app_config = settings + + # Initialize RAG module + rag_module = RAGModule(app_config) + + # Get available collections if none specified + if not collection_name: + collections = await rag_module.list_collections() + if collections: + collection_name = collections[0] # Use first collection + else: + return { + "results": [], + "debug_info": { + "error": "No collections available", + "collections_found": 0 + }, + "search_time_ms": 0 + } + + # Perform search + results = await rag_module.search( + query=query, + max_results=max_results, + score_threshold=score_threshold, + collection_name=collection_name, + config=config or {} + ) + + return results + + except Exception as e: + logger.error(f"Debug search error: {e}") + return { + "results": [], + "debug_info": { + "error": str(e), + "query": query, + "collection_name": collection_name + }, + "search_time_ms": 0 + } \ No newline at end of file diff --git a/frontend/src/app/api/rag/debug/collections/route.ts b/frontend/src/app/api/rag/debug/collections/route.ts new file mode 100644 index 0000000..acfbd08 --- /dev/null +++ b/frontend/src/app/api/rag/debug/collections/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { tokenManager } from '@/lib/token-manager'; + +export async function GET(request: NextRequest) { + try { + // Get authentication token from Authorization header or tokenManager + const authHeader = request.headers.get('authorization'); + let token; + + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + } else { + token = await tokenManager.getAccessToken(); + } + + if (!token) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ); + } + + // Backend URL + const backendUrl = process.env.INTERNAL_API_URL || `http://enclava-backend:${process.env.BACKEND_INTERNAL_PORT || '8000'}`; + + // Build the proxy URL + const proxyUrl = `${backendUrl}/api-internal/v1/rag/debug/collections`; + + // Proxy the request to the backend with authentication + const response = await fetch(proxyUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Backend list collections error:', response.status, errorText); + return NextResponse.json( + { error: `Backend request failed: ${response.status}` }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('RAG collections proxy error:', error); + return NextResponse.json( + { error: 'Failed to proxy collections request' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/frontend/src/app/api/rag/debug/search/route.ts b/frontend/src/app/api/rag/debug/search/route.ts new file mode 100644 index 0000000..e06bcd9 --- /dev/null +++ b/frontend/src/app/api/rag/debug/search/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { tokenManager } from '@/lib/token-manager'; + +export async function POST(request: NextRequest) { + try { + // Get the search parameters from the query string + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get('query') || ''; + const max_results = searchParams.get('max_results') || '10'; + const score_threshold = searchParams.get('score_threshold') || '0.3'; + const collection_name = searchParams.get('collection_name'); + + // Get the config from the request body + const body = await request.json(); + + // Get authentication token from Authorization header or tokenManager + const authHeader = request.headers.get('authorization'); + let token; + + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + } else { + token = await tokenManager.getAccessToken(); + } + + if (!token) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ); + } + + // Backend URL + const backendUrl = process.env.INTERNAL_API_URL || `http://enclava-backend:${process.env.BACKEND_INTERNAL_PORT || '8000'}`; + + // Build the proxy URL with query parameters + const proxyUrl = `${backendUrl}/api-internal/v1/rag/debug/search?query=${encodeURIComponent(query)}&max_results=${max_results}&score_threshold=${score_threshold}${collection_name ? `&collection_name=${encodeURIComponent(collection_name)}` : ''}`; + + // Proxy the request to the backend with authentication + const response = await fetch(proxyUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Backend RAG search error:', response.status, errorText); + return NextResponse.json( + { error: `Backend request failed: ${response.status}` }, + { status: response.status } + ); + } + + const data = await response.json(); + return NextResponse.json(data); + } catch (error) { + console.error('RAG debug search proxy error:', error); + return NextResponse.json( + { error: 'Failed to proxy RAG search request' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/frontend/src/app/rag-demo/page.tsx b/frontend/src/app/rag-demo/page.tsx new file mode 100644 index 0000000..b07c7ba --- /dev/null +++ b/frontend/src/app/rag-demo/page.tsx @@ -0,0 +1,569 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useAuth } from '@/contexts/AuthContext'; +import { tokenManager } from '@/lib/token-manager'; + +interface SearchResult { + document: { + id: string; + content: string; + metadata: Record; + }; + score: number; + debug_info?: Record; +} + +interface DebugInfo { + query_embedding?: number[]; + embedding_dimension?: number; + score_stats?: { + min: number; + max: number; + avg: number; + stddev: number; + }; + collection_stats?: { + total_documents: number; + total_chunks: number; + languages: string[]; + }; +} + +export default function RAGDemoPage() { + const { user, loading } = useAuth(); + const [query, setQuery] = useState('are sd card backups encrypted?'); + const [results, setResults] = useState([]); + const [debugInfo, setDebugInfo] = useState({}); + const [searchTime, setSearchTime] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + + // Configuration state + const [config, setConfig] = useState({ + max_results: 10, + score_threshold: 0.3, + collection_name: '', + chunk_size: 300, + chunk_overlap: 50, + enable_hybrid: false, + vector_weight: 0.7, + bm25_weight: 0.3, + use_query_prefix: true, + use_passage_prefix: true, + show_timing: true, + show_embeddings: false, + }); + + // Available collections + const [collections, setCollections] = useState([]); + const [collectionsLoading, setCollectionsLoading] = useState(false); + + const presets = { + default: { + max_results: 10, + score_threshold: 0.3, + chunk_size: 300, + chunk_overlap: 50, + enable_hybrid: false, + vector_weight: 0.7, + bm25_weight: 0.3, + }, + high_precision: { + max_results: 5, + score_threshold: 0.5, + chunk_size: 200, + chunk_overlap: 30, + enable_hybrid: true, + vector_weight: 0.8, + bm25_weight: 0.2, + }, + high_recall: { + max_results: 20, + score_threshold: 0.1, + chunk_size: 400, + chunk_overlap: 100, + enable_hybrid: true, + vector_weight: 0.6, + bm25_weight: 0.4, + }, + hybrid: { + max_results: 10, + score_threshold: 0.2, + chunk_size: 300, + chunk_overlap: 50, + enable_hybrid: true, + vector_weight: 0.5, + bm25_weight: 0.5, + }, + }; + + useEffect(() => { + // Check if we have tokens in localStorage but not in tokenManager + const syncTokens = async () => { + const rawTokens = localStorage.getItem('auth_tokens'); + if (rawTokens && !tokenManager.isAuthenticated()) { + try { + const tokens = JSON.parse(rawTokens); + // Sync tokens to tokenManager + tokenManager.setTokens( + tokens.access_token, + tokens.refresh_token, + Math.floor((tokens.access_expires_at - Date.now()) / 1000) + ); + console.log('RAG Demo: Tokens synced from localStorage to tokenManager'); + } catch (e) { + console.error('RAG Demo: Failed to sync tokens:', e); + } + } + loadCollections(); + }; + + syncTokens(); + }, [user]); + + const loadCollections = async () => { + setCollectionsLoading(true); + try { + console.log('RAG Demo: Loading collections...'); + console.log('RAG Demo: User authenticated:', !!user); + console.log('RAG Demo: TokenManager authenticated:', tokenManager.isAuthenticated()); + + const token = await tokenManager.getAccessToken(); + console.log('RAG Demo: Token retrieved:', token ? 'Yes' : 'No'); + console.log('RAG Demo: Token expiry:', tokenManager.getTokenExpiry()); + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + console.log('RAG Demo: Authorization header set'); + } else { + console.warn('RAG Demo: No token available'); + } + + const response = await fetch('/api/rag/debug/collections', { headers }); + console.log('RAG Demo: Collections response status:', response.status); + if (response.ok) { + const data = await response.json(); + console.log('RAG Demo: Collections loaded:', data.collections); + setCollections(data.collections || []); + // Auto-select first collection if none selected + if (data.collections && data.collections.length > 0 && !config.collection_name) { + setConfig(prev => ({ ...prev, collection_name: data.collections[0] })); + } + } else { + const errorText = await response.text(); + console.error('RAG Demo: Collections failed:', response.status, errorText); + } + } catch (err) { + console.error('RAG Demo: Failed to load collections:', err); + } finally { + setCollectionsLoading(false); + } + }; + + const loadPreset = (presetName: keyof typeof presets) => { + setConfig(prev => ({ + ...prev, + ...presets[presetName], + })); + }; + + const performSearch = async () => { + if (!query.trim()) return; + if (!config.collection_name) { + setError('Please select a collection'); + return; + } + + setIsLoading(true); + setError(''); + setResults([]); + + try { + const token = await tokenManager.getAccessToken(); + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch('/api/rag/debug/search', { + method: 'POST', + headers, + body: JSON.stringify({ + query, + max_results: config.max_results, + score_threshold: config.score_threshold, + collection_name: config.collection_name, + config, + }), + }); + + if (!response.ok) { + throw new Error(`Search failed: ${response.statusText}`); + } + + const data = await response.json(); + setResults(data.results || []); + setDebugInfo(data.debug_info || {}); + setSearchTime(data.search_time_ms || 0); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setIsLoading(false); + } + }; + + const updateConfig = (key: string, value: any) => { + setConfig(prev => ({ ...prev, [key]: value })); + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + if (!user) { + return ( +
+
+

RAG Demo

+

Please log in to access the RAG demo interface.

+
+
+ ); + } + + return ( +
+

🔍 RAG Search Demo

+

Test and tune your RAG system with real-time search and debugging

+ +
+ {/* Search Results - Main Content */} +
+ {/* Preset Buttons */} +
+ {Object.entries(presets).map(([name, _]) => ( + + ))} +
+ + {/* Search Box */} +
+
+ setQuery(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && performSearch()} + placeholder="Enter your search query..." + className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+ + {error && ( +
+ Error: {error} +
+ )} + + {/* Results Summary */} + {results.length > 0 && ( +
+

+ Found {results.length} results in {searchTime.toFixed(0)}ms + {config.enable_hybrid && ( + • Hybrid Search Enabled + )} +

+
+ )} +
+ + {/* Search Results */} +
+ {results.map((result, index) => ( +
+
+

Result {index + 1}

+ = 0.5 ? 'bg-green-100 text-green-800' : + result.score >= 0.3 ? 'bg-yellow-100 text-yellow-800' : + 'bg-red-100 text-red-800' + }`}> + Score: {result.score.toFixed(4)} + +
+ +
+ {result.document.content} +
+ + {/* Metadata */} +
+ {result.document.metadata.content_type && ( + Type: {result.document.metadata.content_type} + )} + {result.document.metadata.language && ( + Language: {result.document.metadata.language} + )} + {result.document.metadata.filename && ( + File: {result.document.metadata.filename} + )} + {result.document.metadata.chunk_index !== undefined && ( + + Chunk: {result.document.metadata.chunk_index + 1}/{result.document.metadata.chunk_count || '?'} + + )} +
+ + {/* Debug Details */} + {config.show_timing && result.debug_info && ( +
+

Debug Information:

+ {result.debug_info.vector_score !== undefined && ( +

Vector Score: {result.debug_info.vector_score.toFixed(4)}

+ )} + {result.debug_info.bm25_score !== undefined && ( +

BM25 Score: {result.debug_info.bm25_score.toFixed(4)}

+ )} + {result.document.metadata.question && ( +
+

Question: {result.document.metadata.question}

+
+ )} +
+ )} +
+ ))} +
+ + {/* Debug Section */} + {debugInfo && Object.keys(debugInfo).length > 0 && ( +
+

Debug Information

+ + {debugInfo.score_stats && ( +
+

Score Statistics:

+
+
Min: {debugInfo.score_stats.min?.toFixed(4)}
+
Max: {debugInfo.score_stats.max?.toFixed(4)}
+
Avg: {debugInfo.score_stats.avg?.toFixed(4)}
+
StdDev: {debugInfo.score_stats.stddev?.toFixed(4)}
+
+
+ )} + + {debugInfo.collection_stats && ( +
+

Collection Stats:

+
+

Total Documents: {debugInfo.collection_stats.total_documents}

+

Total Chunks: {debugInfo.collection_stats.total_chunks}

+

Languages: {debugInfo.collection_stats.languages?.join(', ')}

+
+
+ )} + + {debugInfo.query_embedding && config.show_embeddings && ( +
+

Query Embedding (first 10 dims):

+

+ [{debugInfo.query_embedding.slice(0, 10).map(x => x.toFixed(6)).join(', ')}...] +

+
+ )} +
+ )} +
+ + {/* Configuration Panel */} +
+
+

⚙️ Configuration

+ +
+ {/* Search Settings */} +
+

Search Settings

+
+
+ + updateConfig('max_results', parseInt(e.target.value))} + className="w-full" + /> +
+
+ + updateConfig('score_threshold', parseFloat(e.target.value))} + className="w-full" + /> +
+
+ + {collectionsLoading ? ( + + ) : ( + + )} +
+
+
+ + {/* Chunking Settings */} +
+

Chunking Settings

+
+
+ + updateConfig('chunk_size', parseInt(e.target.value))} + className="w-full" + /> +
+
+ + updateConfig('chunk_overlap', parseInt(e.target.value))} + className="w-full" + /> +
+
+
+ + {/* Hybrid Search */} +
+

Hybrid Search

+
+ + {config.enable_hybrid && ( + <> +
+ + updateConfig('vector_weight', parseFloat(e.target.value))} + className="w-full" + /> +
+
+ + updateConfig('bm25_weight', parseFloat(e.target.value))} + className="w-full" + /> +
+ + )} +
+
+ + {/* Debug Options */} +
+

Debug Options

+
+ + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file