/** * Legal Corpus API Proxy * * Proxies requests to klausur-service for RAG operations. * This allows the client-side RAG page to call the API without CORS issues. */ import { NextRequest, NextResponse } from 'next/server' const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086' const QDRANT_URL = process.env.QDRANT_URL || 'http://qdrant:6333' export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const action = searchParams.get('action') try { let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus` switch (action) { case 'status': { // Query Qdrant directly for collection stats const qdrantRes = await fetch(`${QDRANT_URL}/collections/bp_legal_corpus`, { cache: 'no-store', }) if (!qdrantRes.ok) { return NextResponse.json({ error: 'Qdrant not available' }, { status: 503 }) } const qdrantData = await qdrantRes.json() const result = qdrantData.result || {} return NextResponse.json({ collection: 'bp_legal_corpus', totalPoints: result.points_count || 0, vectorSize: result.config?.params?.vectors?.size || 0, status: result.status || 'unknown', regulations: {}, }) } case 'search': const query = searchParams.get('query') const topK = searchParams.get('top_k') || '5' const regulations = searchParams.get('regulations') url += `/search?query=${encodeURIComponent(query || '')}&top_k=${topK}` if (regulations) { url += `®ulations=${encodeURIComponent(regulations)}` } break case 'ingestion-status': url += '/ingestion-status' break case 'regulations': url += '/regulations' break case 'custom-documents': url += '/custom-documents' break case 'pipeline-checkpoints': url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/checkpoints` break case 'pipeline-status': url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/status` break case 'traceability': { const chunkId = searchParams.get('chunk_id') const regulation = searchParams.get('regulation') url += `/traceability?chunk_id=${encodeURIComponent(chunkId || '')}®ulation=${encodeURIComponent(regulation || '')}` break } case 'scroll': { const collection = searchParams.get('collection') || 'bp_compliance_gesetze' const limit = parseInt(searchParams.get('limit') || '20', 10) const offsetParam = searchParams.get('offset') const filterKey = searchParams.get('filter_key') const filterValue = searchParams.get('filter_value') const textSearch = searchParams.get('text_search') const scrollBody: Record = { limit: Math.min(limit, 100), with_payload: true, with_vector: false, } if (offsetParam) { scrollBody.offset = offsetParam } if (filterKey && filterValue) { scrollBody.filter = { must: [{ key: filterKey, match: { value: filterValue } }], } } const scrollRes = await fetch(`${QDRANT_URL}/collections/${encodeURIComponent(collection)}/points/scroll`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(scrollBody), cache: 'no-store', }) if (!scrollRes.ok) { return NextResponse.json({ error: 'Qdrant scroll failed' }, { status: scrollRes.status }) } const scrollData = await scrollRes.json() const points = (scrollData.result?.points || []).map((p: { id: string; payload?: Record }) => ({ id: p.id, ...p.payload, })) // Client-side text search filter let filtered = points if (textSearch && textSearch.trim()) { const term = textSearch.toLowerCase() filtered = points.filter((p: Record) => { const text = String(p.text || p.content || p.chunk_text || '') return text.toLowerCase().includes(term) }) } return NextResponse.json({ chunks: filtered, next_offset: scrollData.result?.next_page_offset || null, total_in_page: points.length, }) } case 'regulation-counts-batch': { const col = searchParams.get('collection') || 'bp_compliance_gesetze' // Accept qdrant_ids (actual regulation_id values in Qdrant payload) const qdrantIds = (searchParams.get('qdrant_ids') || '').split(',').filter(Boolean) const results: Record = {} for (let i = 0; i < qdrantIds.length; i += 10) { const batch = qdrantIds.slice(i, i + 10) await Promise.all(batch.map(async (qid) => { try { const res = await fetch(`${QDRANT_URL}/collections/${encodeURIComponent(col)}/points/count`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filter: { must: [{ key: 'regulation_id', match: { value: qid } }] }, exact: true, }), cache: 'no-store', }) if (res.ok) { const data = await res.json() results[qid] = data.result?.count || 0 } } catch { /* skip failed counts */ } })) } return NextResponse.json({ counts: results }) } case 'collection-count': { const col = searchParams.get('collection') || 'bp_compliance_gesetze' const countRes = await fetch(`${QDRANT_URL}/collections/${encodeURIComponent(col)}`, { cache: 'no-store', }) if (!countRes.ok) { return NextResponse.json({ count: 0 }) } const countData = await countRes.json() return NextResponse.json({ count: countData.result?.points_count || 0, }) } default: return NextResponse.json({ error: 'Unknown action' }, { status: 400 }) } const res = await fetch(url, { headers: { 'Content-Type': 'application/json', }, cache: 'no-store', }) const data = await res.json() return NextResponse.json(data, { status: res.status }) } catch (error) { console.error('Legal corpus proxy error:', error) return NextResponse.json( { error: 'Failed to connect to klausur-service' }, { status: 503 } ) } } export async function POST(request: NextRequest) { const { searchParams } = new URL(request.url) const action = searchParams.get('action') try { let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus` switch (action) { case 'ingest': { url += '/ingest' const body = await request.json() const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const data = await res.json() return NextResponse.json(data, { status: res.status }) } case 'add-link': { url += '/add-link' const body = await request.json() const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const data = await res.json() return NextResponse.json(data, { status: res.status }) } case 'upload': { url += '/upload' // Forward FormData directly const formData = await request.formData() const res = await fetch(url, { method: 'POST', body: formData, }) const data = await res.json() return NextResponse.json(data, { status: res.status }) } case 'start-pipeline': { url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/start` const body = await request.json() const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const data = await res.json() return NextResponse.json(data, { status: res.status }) } default: return NextResponse.json({ error: 'Unknown action' }, { status: 400 }) } } catch (error) { console.error('Legal corpus proxy error:', error) return NextResponse.json( { error: 'Failed to connect to klausur-service' }, { status: 503 } ) } } export async function DELETE(request: NextRequest) { const { searchParams } = new URL(request.url) const action = searchParams.get('action') const docId = searchParams.get('docId') try { if (action === 'delete-document' && docId) { const url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus/custom-documents/${docId}` const res = await fetch(url, { method: 'DELETE' }) const data = await res.json() return NextResponse.json(data, { status: res.status }) } return NextResponse.json({ error: 'Unknown action' }, { status: 400 }) } catch (error) { console.error('Legal corpus proxy error:', error) return NextResponse.json( { error: 'Failed to connect to klausur-service' }, { status: 503 } ) } }