refactor to openrag and knowledge filters

This commit is contained in:
estevez.sebastian@gmail.com 2025-08-13 16:31:35 -04:00
parent 33ea4148c4
commit 735929c99f
25 changed files with 390 additions and 2629 deletions

View file

@ -1,6 +1,6 @@
### gendb ### OpenRAG
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/phact/gendb) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/phact/openrag)
docker compose build docker compose build

View file

@ -5,7 +5,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: os container_name: os
depends_on: depends_on:
- gendb-backend - openrag-backend
environment: environment:
- discovery.type=single-node - discovery.type=single-node
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_PASSWORD} - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_PASSWORD}
@ -37,12 +37,12 @@ services:
ports: ports:
- "5601:5601" - "5601:5601"
gendb-backend: openrag-backend:
image: phact/gendb-backend:latest image: phact/openrag-backend:latest
#build: #build:
#context: . #context: .
#dockerfile: Dockerfile.backend #dockerfile: Dockerfile.backend
container_name: gendb-backend container_name: openrag-backend
depends_on: depends_on:
- langflow - langflow
environment: environment:
@ -67,16 +67,16 @@ services:
gpus: all gpus: all
platform: linux/amd64 platform: linux/amd64
gendb-frontend: openrag-frontend:
image: phact/gendb-frontend:latest image: phact/openrag-frontend:latest
#build: #build:
#context: . #context: .
#dockerfile: Dockerfile.frontend #dockerfile: Dockerfile.frontend
container_name: gendb-frontend container_name: openrag-frontend
depends_on: depends_on:
- gendb-backend - openrag-backend
environment: environment:
- GENDB_BACKEND_HOST=gendb-backend - OPENRAG_BACKEND_HOST=openrag-backend
ports: ports:
- "3000:3000" - "3000:3000"
volumes: volumes:
@ -94,6 +94,6 @@ services:
- LANGFLOW_LOAD_FLOWS_PATH=/app/flows - LANGFLOW_LOAD_FLOWS_PATH=/app/flows
- LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY} - LANGFLOW_SECRET_KEY=${LANGFLOW_SECRET_KEY}
- JWT="dummy" - JWT="dummy"
- GENDB-QUERY-FILTER="{}" - OPENRAG-QUERY-FILTER="{}"
- LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT - LANGFLOW_VARIABLES_TO_GET_FROM_ENVIRONMENT=JWT,OPENRAG-QUERY-FILTER
- LANGFLOW_LOG_LEVEL=DEBUG - LANGFLOW_LOG_LEVEL=DEBUG

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,7 @@ export function NavigationLayout({ children }: NavigationLayoutProps) {
<div className="container flex h-14 max-w-screen-2xl items-center"> <div className="container flex h-14 max-w-screen-2xl items-center">
<div className="mr-4 hidden md:flex"> <div className="mr-4 hidden md:flex">
<h1 className="text-lg font-semibold tracking-tight"> <h1 className="text-lg font-semibold tracking-tight">
GenDB OpenRAG
</h1> </h1>
</div> </div>
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end"> <div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">

View file

@ -28,10 +28,10 @@ export function Navigation() {
active: pathname === "/chat", active: pathname === "/chat",
}, },
{ {
label: "Contexts", label: "Knowledge Filters",
icon: BookOpenCheck, icon: BookOpenCheck,
href: "/contexts", href: "/knowledge-filters",
active: pathname.startsWith("/contexts"), active: pathname.startsWith("/knowledge-filters"),
}, },
{ {
label: "Connectors", label: "Connectors",

View file

@ -39,7 +39,7 @@ async function proxyRequest(
request: NextRequest, request: NextRequest,
params: { path: string[] } params: { path: string[] }
) { ) {
const backendHost = process.env.GENDB_BACKEND_HOST || 'localhost'; const backendHost = process.env.OPENRAG_BACKEND_HOST || 'localhost';
const path = params.path.join('/'); const path = params.path.join('/');
const searchParams = request.nextUrl.searchParams.toString(); const searchParams = request.nextUrl.searchParams.toString();
const backendUrl = `http://${backendHost}:8000/${path}${searchParams ? `?${searchParams}` : ''}`; const backendUrl = `http://${backendHost}:8000/${path}${searchParams ? `?${searchParams}` : ''}`;

View file

@ -135,7 +135,7 @@ function AuthCallbackContent() {
return isAppAuth ? "Signing you in..." : "Connecting..." return isAppAuth ? "Signing you in..." : "Connecting..."
} }
if (status === "success") { if (status === "success") {
return isAppAuth ? "Welcome to GenDB!" : "Connection Successful!" return isAppAuth ? "Welcome to OpenRAG!" : "Connection Successful!"
} }
if (status === "error") { if (status === "error") {
return isAppAuth ? "Sign In Failed" : "Connection Failed" return isAppAuth ? "Sign In Failed" : "Connection Failed"

View file

@ -89,17 +89,17 @@ function ChatPage() {
const [scoreThreshold, setScoreThreshold] = useState(0) const [scoreThreshold, setScoreThreshold] = useState(0)
const [loadedContextName, setLoadedContextName] = useState<string | null>(null) const [loadedContextName, setLoadedContextName] = useState<string | null>(null)
// Load context if contextId is provided in URL // Load knowledge filter if filterId is provided in URL
useEffect(() => { useEffect(() => {
const contextId = searchParams.get('contextId') const filterId = searchParams.get('filterId')
if (contextId) { if (filterId) {
loadContext(contextId) loadKnowledgeFilter(filterId)
} }
}, [searchParams]) }, [searchParams])
const loadContext = async (contextId: string) => { const loadKnowledgeFilter = async (filterId: string) => {
try { try {
const response = await fetch(`/api/contexts/${contextId}`, { const response = await fetch(`/api/knowledge-filter/${filterId}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -108,19 +108,19 @@ function ChatPage() {
const result = await response.json() const result = await response.json()
if (response.ok && result.success) { if (response.ok && result.success) {
const context = result.context const filter = result.filter
const parsedQueryData = JSON.parse(context.query_data) const parsedQueryData = JSON.parse(filter.query_data)
// Load the context data into state // Load the context data into state
setSelectedFilters(parsedQueryData.filters) setSelectedFilters(parsedQueryData.filters)
setResultLimit(parsedQueryData.limit) setResultLimit(parsedQueryData.limit)
setScoreThreshold(parsedQueryData.scoreThreshold) setScoreThreshold(parsedQueryData.scoreThreshold)
setLoadedContextName(context.name) setLoadedContextName(filter.name)
} else { } else {
console.error("Failed to load context:", result.error) console.error("Failed to load knowledge filter:", result.error)
} }
} catch (error) { } catch (error) {
console.error("Error loading context:", error) console.error("Error loading knowledge filter:", error)
} }
} }
@ -283,10 +283,14 @@ function ChatPage() {
const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow" const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"
try { try {
const hasFilters = selectedFilters.data_sources.length > 0 ||
selectedFilters.document_types.length > 0 ||
selectedFilters.owners.length > 0
const requestBody: RequestBody = { const requestBody: RequestBody = {
prompt: userMessage.content, prompt: userMessage.content,
stream: true, stream: true,
filters: selectedFilters, ...(hasFilters && { filters: selectedFilters }),
limit: resultLimit, limit: resultLimit,
scoreThreshold: scoreThreshold scoreThreshold: scoreThreshold
} }
@ -744,9 +748,13 @@ function ChatPage() {
try { try {
const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow" const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"
const hasFilters = selectedFilters.data_sources.length > 0 ||
selectedFilters.document_types.length > 0 ||
selectedFilters.owners.length > 0
const requestBody: RequestBody = { const requestBody: RequestBody = {
prompt: userMessage.content, prompt: userMessage.content,
filters: selectedFilters, ...(hasFilters && { filters: selectedFilters }),
limit: resultLimit, limit: resultLimit,
scoreThreshold: scoreThreshold scoreThreshold: scoreThreshold
} }

View file

@ -9,7 +9,7 @@ import { Label } from "@/components/ui/label"
import { Search, Loader2, BookOpenCheck, Settings, Calendar, MessageCircle } from "lucide-react" import { Search, Loader2, BookOpenCheck, Settings, Calendar, MessageCircle } from "lucide-react"
import { ProtectedRoute } from "@/components/protected-route" import { ProtectedRoute } from "@/components/protected-route"
interface Context { interface KnowledgeFilter {
id: string id: string
name: string name: string
description: string description: string
@ -30,18 +30,18 @@ interface ParsedQueryData {
scoreThreshold: number scoreThreshold: number
} }
function ContextsPage() { function KnowledgeFiltersPage() {
const router = useRouter() const router = useRouter()
const [contexts, setContexts] = useState<Context[]>([]) const [filters, setFilters] = useState<KnowledgeFilter[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const [selectedContext, setSelectedContext] = useState<Context | null>(null) const [selectedFilter, setSelectedFilter] = useState<KnowledgeFilter | null>(null)
const [parsedQueryData, setParsedQueryData] = useState<ParsedQueryData | null>(null) const [parsedQueryData, setParsedQueryData] = useState<ParsedQueryData | null>(null)
const loadContexts = async (query = "") => { const loadFilters = async (query = "") => {
setLoading(true) setLoading(true)
try { try {
const response = await fetch("/api/contexts/search", { const response = await fetch("/api/knowledge-filter/search", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -54,32 +54,32 @@ function ContextsPage() {
const result = await response.json() const result = await response.json()
if (response.ok && result.success) { if (response.ok && result.success) {
setContexts(result.contexts) setFilters(result.filters)
} else { } else {
console.error("Failed to load contexts:", result.error) console.error("Failed to load knowledge filters:", result.error)
setContexts([]) setFilters([])
} }
} catch (error) { } catch (error) {
console.error("Error loading contexts:", error) console.error("Error loading knowledge filters:", error)
setContexts([]) setFilters([])
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
useEffect(() => { useEffect(() => {
loadContexts() loadFilters()
}, []) }, [])
const handleSearch = async (e: React.FormEvent) => { const handleSearch = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
await loadContexts(searchQuery) await loadFilters(searchQuery)
} }
const handleContextClick = (context: Context) => { const handleFilterClick = (filter: KnowledgeFilter) => {
setSelectedContext(context) setSelectedFilter(filter)
try { try {
const parsed = JSON.parse(context.query_data) as ParsedQueryData const parsed = JSON.parse(filter.query_data) as ParsedQueryData
setParsedQueryData(parsed) setParsedQueryData(parsed)
} catch (error) { } catch (error) {
console.error("Error parsing query data:", error) console.error("Error parsing query data:", error)
@ -97,16 +97,16 @@ function ContextsPage() {
}) })
} }
const handleSearchWithContext = () => { const handleSearchWithFilter = () => {
if (!selectedContext) return if (!selectedFilter) return
router.push(`/?contextId=${selectedContext.id}`) router.push(`/?filterId=${selectedFilter.id}`)
} }
const handleChatWithContext = () => { const handleChatWithFilter = () => {
if (!selectedContext) return if (!selectedFilter) return
router.push(`/chat?contextId=${selectedContext.id}`) router.push(`/chat?filterId=${selectedFilter.id}`)
} }
return ( return (
@ -115,14 +115,14 @@ function ContextsPage() {
<div className="space-y-4"> <div className="space-y-4">
<div className="mb-4"> <div className="mb-4">
<h1 className="text-4xl font-bold tracking-tight text-white"> <h1 className="text-4xl font-bold tracking-tight text-white">
Contexts Knowledge Filters
</h1> </h1>
</div> </div>
<p className="text-xl text-muted-foreground"> <p className="text-xl text-muted-foreground">
Manage your saved search contexts Manage your saved knowledge filters
</p> </p>
<p className="text-sm text-muted-foreground max-w-2xl"> <p className="text-sm text-muted-foreground max-w-2xl">
View and manage your saved search queries, filters, and configurations for quick access to your most important searches. View and manage your saved search configurations that help you focus on specific subsets of your knowledge base.
</p> </p>
</div> </div>
@ -131,10 +131,10 @@ function ContextsPage() {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<BookOpenCheck className="h-5 w-5" /> <BookOpenCheck className="h-5 w-5" />
Search Contexts Search Knowledge Filters
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Search through your saved search contexts by name or description Search through your saved knowledge filters by name or description
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
@ -142,7 +142,7 @@ function ContextsPage() {
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
type="text" type="text"
placeholder="Search contexts..." placeholder="Search knowledge filters..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="h-12 bg-background/50 border-border/50 focus:border-blue-400/50 focus:ring-blue-400/20 flex-1" className="h-12 bg-background/50 border-border/50 focus:border-blue-400/50 focus:ring-blue-400/20 flex-1"
@ -170,7 +170,7 @@ function ContextsPage() {
<div className="flex gap-6"> <div className="flex gap-6">
{/* Context List */} {/* Context List */}
<div className="flex-1 space-y-4"> <div className="flex-1 space-y-4">
{contexts.length === 0 ? ( {filters.length === 0 ? (
<Card className="bg-muted/20 border-dashed border-muted-foreground/30"> <Card className="bg-muted/20 border-dashed border-muted-foreground/30">
<CardContent className="pt-8 pb-8"> <CardContent className="pt-8 pb-8">
<div className="text-center space-y-3"> <div className="text-center space-y-3">
@ -178,40 +178,40 @@ function ContextsPage() {
<BookOpenCheck className="h-8 w-8 text-muted-foreground/50" /> <BookOpenCheck className="h-8 w-8 text-muted-foreground/50" />
</div> </div>
<p className="text-lg font-medium text-muted-foreground"> <p className="text-lg font-medium text-muted-foreground">
No contexts found No knowledge filters found
</p> </p>
<p className="text-sm text-muted-foreground/70 max-w-md mx-auto"> <p className="text-sm text-muted-foreground/70 max-w-md mx-auto">
Create your first context by saving a search configuration from the search page. Create your first knowledge filter by saving a search configuration from the search page.
</p> </p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{contexts.map((context) => ( {filters.map((filter) => (
<Card <Card
key={context.id} key={filter.id}
className={`bg-card/50 backdrop-blur-sm border-border/50 hover:bg-card/70 transition-all duration-200 hover:shadow-lg hover:shadow-blue-500/10 cursor-pointer ${ className={`bg-card/50 backdrop-blur-sm border-border/50 hover:bg-card/70 transition-all duration-200 hover:shadow-lg hover:shadow-blue-500/10 cursor-pointer ${
selectedContext?.id === context.id ? 'ring-2 ring-blue-500/50 bg-card/70' : '' selectedFilter?.id === filter.id ? 'ring-2 ring-blue-500/50 bg-card/70' : ''
}`} }`}
onClick={() => handleContextClick(context)} onClick={() => handleFilterClick(filter)}
> >
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<h3 className="font-semibold text-lg">{context.name}</h3> <h3 className="font-semibold text-lg">{filter.name}</h3>
{context.description && ( {filter.description && (
<p className="text-sm text-muted-foreground">{context.description}</p> <p className="text-sm text-muted-foreground">{filter.description}</p>
)} )}
<div className="flex items-center gap-4 text-xs text-muted-foreground"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
<span>Created {formatDate(context.created_at)}</span> <span>Created {formatDate(filter.created_at)}</span>
</div> </div>
{context.updated_at !== context.created_at && ( {filter.updated_at !== filter.created_at && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
<span>Updated {formatDate(context.updated_at)}</span> <span>Updated {formatDate(filter.updated_at)}</span>
</div> </div>
)} )}
</div> </div>
@ -225,12 +225,12 @@ function ContextsPage() {
</div> </div>
{/* Context Detail Panel */} {/* Context Detail Panel */}
{selectedContext && parsedQueryData && ( {selectedFilter && parsedQueryData && (
<div className="w-64 space-y-6 flex-shrink-0"> <div className="w-64 space-y-6 flex-shrink-0">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold flex items-center gap-2"> <h2 className="text-lg font-semibold flex items-center gap-2">
<Settings className="h-5 w-5" /> <Settings className="h-5 w-5" />
Context Details Knowledge Filter Details
</h2> </h2>
</div> </div>
@ -311,21 +311,21 @@ function ContextsPage() {
{/* Action Buttons */} {/* Action Buttons */}
<div className="space-y-2 pt-4 border-t border-border/50"> <div className="space-y-2 pt-4 border-t border-border/50">
<Button <Button
onClick={handleSearchWithContext} onClick={handleSearchWithFilter}
className="w-full flex items-center gap-2" className="w-full flex items-center gap-2"
variant="default" variant="default"
> >
<Search className="h-4 w-4" /> <Search className="h-4 w-4" />
Search with Context Search with Filter
</Button> </Button>
<Button <Button
onClick={handleChatWithContext} onClick={handleChatWithFilter}
className="w-full flex items-center gap-2" className="w-full flex items-center gap-2"
variant="outline" variant="outline"
> >
<MessageCircle className="h-4 w-4" /> <MessageCircle className="h-4 w-4" />
Chat with Context Chat with Filter
</Button> </Button>
</div> </div>
</div> </div>
@ -338,10 +338,10 @@ function ContextsPage() {
) )
} }
export default function ProtectedContextsPage() { export default function ProtectedKnowledgeFiltersPage() {
return ( return (
<ProtectedRoute> <ProtectedRoute>
<ContextsPage /> <KnowledgeFiltersPage />
</ProtectedRoute> </ProtectedRoute>
) )
} }

View file

@ -18,8 +18,8 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "GenDB", title: "OpenRAG",
description: "Document search and management system", description: "Open source RAG (Retrieval Augmented Generation) system",
}; };
export default function RootLayout({ export default function RootLayout({

View file

@ -43,7 +43,7 @@ function LoginPageContent() {
<Lock className="h-8 w-8 text-primary" /> <Lock className="h-8 w-8 text-primary" />
</div> </div>
<div> <div>
<CardTitle className="text-2xl">Welcome to GenDB</CardTitle> <CardTitle className="text-2xl">Welcome to OpenRAG</CardTitle>
<CardDescription className="mt-2"> <CardDescription className="mt-2">
Sign in to access your documents and AI chat Sign in to access your documents and AI chat
</CardDescription> </CardDescription>

View file

@ -88,48 +88,7 @@ function SearchPage() {
const [savingContext, setSavingContext] = useState(false) const [savingContext, setSavingContext] = useState(false)
const [loadedContextName, setLoadedContextName] = useState<string | null>(null) const [loadedContextName, setLoadedContextName] = useState<string | null>(null)
const loadContext = useCallback(async (contextId: string) => { const handleSearch = useCallback(async (e?: React.FormEvent) => {
try {
const response = await fetch(`/api/contexts/${contextId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
const result = await response.json()
if (response.ok && result.success) {
const context = result.context
const parsedQueryData = JSON.parse(context.query_data)
// Load the context data into state
setQuery(parsedQueryData.query)
setSelectedFilters(parsedQueryData.filters)
setResultLimit(parsedQueryData.limit)
setScoreThreshold(parsedQueryData.scoreThreshold)
setLoadedContextName(context.name)
// Automatically perform the search
setTimeout(() => {
handleSearch()
}, 100)
} else {
console.error("Failed to load context:", result.error)
}
} catch (err) {
console.error("Error loading context:", err)
}
}, [])
// Load context if contextId is provided in URL
useEffect(() => {
const contextId = searchParams.get('contextId')
if (contextId) {
loadContext(contextId)
}
}, [searchParams, loadContext])
const handleSearch = async (e?: React.FormEvent) => {
if (e) e.preventDefault() if (e) e.preventDefault()
if (!query.trim()) return if (!query.trim()) return
@ -202,7 +161,48 @@ function SearchPage() {
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }, [query, resultLimit, scoreThreshold, searchPerformed, selectedFilters])
const loadKnowledgeFilter = useCallback(async (filterId: string) => {
try {
const response = await fetch(`/api/knowledge-filter/${filterId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
const result = await response.json()
if (response.ok && result.success) {
const filter = result.filter
const parsedQueryData = JSON.parse(filter.query_data)
// Load the context data into state
setQuery(parsedQueryData.query)
setSelectedFilters(parsedQueryData.filters)
setResultLimit(parsedQueryData.limit)
setScoreThreshold(parsedQueryData.scoreThreshold)
setLoadedContextName(filter.name)
// Automatically perform the search
setTimeout(() => {
handleSearch()
}, 100)
} else {
console.error("Failed to load knowledge filter:", result.error)
}
} catch (err) {
console.error("Error loading knowledge filter:", err)
}
}, [handleSearch])
// Load knowledge filter if filterId is provided in URL
useEffect(() => {
const filterId = searchParams.get('filterId')
if (filterId) {
loadKnowledgeFilter(filterId)
}
}, [searchParams, loadKnowledgeFilter])
const handleFilterChange = async (facetType: keyof SelectedFilters, value: string, checked: boolean) => { const handleFilterChange = async (facetType: keyof SelectedFilters, value: string, checked: boolean) => {
const newFilters = { const newFilters = {
@ -277,16 +277,16 @@ function SearchPage() {
selectedFilters.owners.length selectedFilters.owners.length
} }
const handleSaveContext = async () => { const handleSaveKnowledgeFilter = async () => {
const contextId = searchParams.get('contextId') const filterId = searchParams.get('filterId')
// If no contextId present and no title, we need the modal // If no filterId present and no title, we need the modal
if (!contextId && !contextTitle.trim()) return if (!filterId && !contextTitle.trim()) return
setSavingContext(true) setSavingContext(true)
try { try {
const contextData = { const filterData = {
query, query,
filters: selectedFilters, filters: selectedFilters,
limit: resultLimit, limit: resultLimit,
@ -295,20 +295,20 @@ function SearchPage() {
let response; let response;
if (contextId) { if (filterId) {
// Update existing context (upsert) // Update existing knowledge filter (upsert)
response = await fetch(`/api/contexts/${contextId}`, { response = await fetch(`/api/knowledge-filter/${filterId}`, {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
queryData: JSON.stringify(contextData) queryData: JSON.stringify(filterData)
}), }),
}) })
} else { } else {
// Create new context // Create new knowledge filter
response = await fetch("/api/contexts", { response = await fetch("/api/knowledge-filter", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -316,7 +316,7 @@ function SearchPage() {
body: JSON.stringify({ body: JSON.stringify({
name: contextTitle, name: contextTitle,
description: contextDescription, description: contextDescription,
queryData: JSON.stringify(contextData) queryData: JSON.stringify(filterData)
}), }),
}) })
} }
@ -324,18 +324,18 @@ function SearchPage() {
const result = await response.json() const result = await response.json()
if (response.ok && result.success) { if (response.ok && result.success) {
if (!contextId) { if (!filterId) {
// Reset modal state only if we were creating a new context // Reset modal state only if we were creating a new knowledge filter
setShowSaveModal(false) setShowSaveModal(false)
setContextTitle("") setContextTitle("")
setContextDescription("") setContextDescription("")
} }
toast.success(contextId ? "Context updated successfully" : "Context saved successfully") toast.success(filterId ? "Knowledge filter updated successfully" : "Knowledge filter saved successfully")
} else { } else {
toast.error(contextId ? "Failed to update context" : "Failed to save context") toast.error(filterId ? "Failed to update knowledge filter" : "Failed to save knowledge filter")
} }
} catch { } catch {
toast.error(contextId ? "Error updating context" : "Error saving context") toast.error(filterId ? "Error updating knowledge filter" : "Error saving knowledge filter")
} finally { } finally {
setSavingContext(false) setSavingContext(false)
} }
@ -843,13 +843,13 @@ function SearchPage() {
</div> </div>
</div> </div>
{/* Save Context Button */} {/* Save Knowledge Filter Button */}
<div className="pt-4 border-t border-border/50"> <div className="pt-4 border-t border-border/50">
<Button <Button
onClick={() => { onClick={() => {
const contextId = searchParams.get('contextId') const filterId = searchParams.get('filterId')
if (contextId) { if (filterId) {
handleSaveContext() handleSaveKnowledgeFilter()
} else { } else {
setShowSaveModal(true) setShowSaveModal(true)
} }
@ -861,12 +861,12 @@ function SearchPage() {
{savingContext ? ( {savingContext ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
{searchParams.get('contextId') ? 'Updating...' : 'Saving...'} {searchParams.get('filterId') ? 'Updating...' : 'Saving...'}
</> </>
) : ( ) : (
<> <>
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
{searchParams.get('contextId') ? 'Update Context' : 'Save Context'} {searchParams.get('filterId') ? 'Update Knowledge Filter' : 'Save Knowledge Filter'}
</> </>
)} )}
</Button> </Button>
@ -938,7 +938,7 @@ function SearchPage() {
Cancel Cancel
</Button> </Button>
<Button <Button
onClick={handleSaveContext} onClick={handleSaveKnowledgeFilter}
disabled={!contextTitle.trim() || savingContext} disabled={!contextTitle.trim() || savingContext}
className="flex items-center gap-2" className="flex items-center gap-2"
> >

View file

@ -39,7 +39,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
<div className="flex h-14 items-center px-4"> <div className="flex h-14 items-center px-4">
<div className="flex items-center"> <div className="flex items-center">
<h1 className="text-lg font-semibold tracking-tight text-white"> <h1 className="text-lg font-semibold tracking-tight text-white">
GenDB OpenRAG
</h1> </h1>
</div> </div>
<div className="flex flex-1 items-center justify-end space-x-2"> <div className="flex flex-1 items-center justify-end space-x-2">

View file

@ -1,5 +1,5 @@
[project] [project]
name = "gendb" name = "openrag"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"

View file

@ -12,7 +12,7 @@ config:
type: openid type: openid
challenge: false challenge: false
config: config:
openid_connect_url: "http://gendb-backend:8000/.well-known/openid-configuration" openid_connect_url: "http://openrag-backend:8000/.well-known/openid-configuration"
subject_key: "sub" subject_key: "sub"
jwt_header: "Authorization" # expects Bearer token jwt_header: "Authorization" # expects Bearer token
roles_key: "roles" roles_key: "roles"

View file

@ -2,13 +2,13 @@ _meta:
type: "roles" type: "roles"
config_version: 2 config_version: 2
gendb_user_role: openrag_user_role:
description: "DLS: user can read/write docs they own or are allowed on" description: "DLS: user can read/write docs they own or are allowed on"
cluster_permissions: cluster_permissions:
- "indices:data/write/bulk" - "indices:data/write/bulk"
- "indices:data/write/index" - "indices:data/write/index"
index_permissions: index_permissions:
- index_patterns: ["documents", "documents*", "search_contexts", "search_contexts*"] - index_patterns: ["documents", "documents*", "knowledge_filters", "knowledge_filters*"]
allowed_actions: allowed_actions:
- crud - crud
- create_index - create_index

View file

@ -2,11 +2,11 @@ _meta:
type: "rolesmapping" type: "rolesmapping"
config_version: 2 config_version: 2
gendb_user_role: openrag_user_role:
users: [] users: []
hosts: [] hosts: []
backend_roles: backend_roles:
- "gendb_user" - "openrag_user"
all_access: all_access:
users: users:

View file

@ -1,114 +0,0 @@
from starlette.requests import Request
from starlette.responses import JSONResponse
import uuid
from datetime import datetime
async def create_context(request: Request, contexts_service, session_manager):
"""Create a new search context"""
payload = await request.json()
name = payload.get("name")
if not name:
return JSONResponse({"error": "Context name is required"}, status_code=400)
description = payload.get("description", "")
query_data = payload.get("queryData")
if not query_data:
return JSONResponse({"error": "Query data is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
# Create context document
context_id = str(uuid.uuid4())
context_doc = {
"id": context_id,
"name": name,
"description": description,
"query_data": query_data, # Store the full search query JSON
"owner": user.user_id,
"allowed_users": payload.get("allowedUsers", []), # ACL field for future use
"allowed_groups": payload.get("allowedGroups", []), # ACL field for future use
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat()
}
result = await contexts_service.create_context(context_doc, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)
async def search_contexts(request: Request, contexts_service, session_manager):
"""Search for contexts by name, description, or query content"""
payload = await request.json()
query = payload.get("query", "")
limit = payload.get("limit", 20)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
result = await contexts_service.search_contexts(query, user_id=user.user_id, jwt_token=jwt_token, limit=limit)
return JSONResponse(result)
async def get_context(request: Request, contexts_service, session_manager):
"""Get a specific context by ID"""
context_id = request.path_params.get("context_id")
if not context_id:
return JSONResponse({"error": "Context ID is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
result = await contexts_service.get_context(context_id, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)
async def update_context(request: Request, contexts_service, session_manager):
"""Update an existing context by delete + recreate (due to DLS limitations)"""
context_id = request.path_params.get("context_id")
if not context_id:
return JSONResponse({"error": "Context ID is required"}, status_code=400)
payload = await request.json()
user = request.state.user
jwt_token = request.cookies.get("auth_token")
# First, get the existing context
existing_result = await contexts_service.get_context(context_id, user_id=user.user_id, jwt_token=jwt_token)
if not existing_result.get("success"):
return JSONResponse({"error": "Context not found or access denied"}, status_code=404)
existing_context = existing_result["context"]
# Delete the existing context
delete_result = await contexts_service.delete_context(context_id, user_id=user.user_id, jwt_token=jwt_token)
if not delete_result.get("success"):
return JSONResponse({"error": "Failed to delete existing context"}, status_code=500)
# Create updated context document with same ID
updated_context = {
"id": context_id,
"name": payload.get("name", existing_context["name"]),
"description": payload.get("description", existing_context["description"]),
"query_data": payload.get("queryData", existing_context["query_data"]),
"owner": existing_context["owner"],
"allowed_users": payload.get("allowedUsers", existing_context.get("allowed_users", [])),
"allowed_groups": payload.get("allowedGroups", existing_context.get("allowed_groups", [])),
"created_at": existing_context["created_at"], # Preserve original creation time
"updated_at": datetime.utcnow().isoformat()
}
# Recreate the context
result = await contexts_service.create_context(updated_context, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)
async def delete_context(request: Request, contexts_service, session_manager):
"""Delete a context"""
context_id = request.path_params.get("context_id")
if not context_id:
return JSONResponse({"error": "Context ID is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
result = await contexts_service.delete_context(context_id, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)

114
src/api/knowledge_filter.py Normal file
View file

@ -0,0 +1,114 @@
from starlette.requests import Request
from starlette.responses import JSONResponse
import uuid
from datetime import datetime
async def create_knowledge_filter(request: Request, knowledge_filter_service, session_manager):
"""Create a new knowledge filter"""
payload = await request.json()
name = payload.get("name")
if not name:
return JSONResponse({"error": "Knowledge filter name is required"}, status_code=400)
description = payload.get("description", "")
query_data = payload.get("queryData")
if not query_data:
return JSONResponse({"error": "Query data is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
# Create knowledge filter document
filter_id = str(uuid.uuid4())
filter_doc = {
"id": filter_id,
"name": name,
"description": description,
"query_data": query_data, # Store the full search query JSON
"owner": user.user_id,
"allowed_users": payload.get("allowedUsers", []), # ACL field for future use
"allowed_groups": payload.get("allowedGroups", []), # ACL field for future use
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat()
}
result = await knowledge_filter_service.create_knowledge_filter(filter_doc, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)
async def search_knowledge_filters(request: Request, knowledge_filter_service, session_manager):
"""Search for knowledge filters by name, description, or query content"""
payload = await request.json()
query = payload.get("query", "")
limit = payload.get("limit", 20)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
result = await knowledge_filter_service.search_knowledge_filters(query, user_id=user.user_id, jwt_token=jwt_token, limit=limit)
return JSONResponse(result)
async def get_knowledge_filter(request: Request, knowledge_filter_service, session_manager):
"""Get a specific knowledge filter by ID"""
filter_id = request.path_params.get("filter_id")
if not filter_id:
return JSONResponse({"error": "Knowledge filter ID is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
result = await knowledge_filter_service.get_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)
async def update_knowledge_filter(request: Request, knowledge_filter_service, session_manager):
"""Update an existing knowledge filter by delete + recreate (due to DLS limitations)"""
filter_id = request.path_params.get("filter_id")
if not filter_id:
return JSONResponse({"error": "Knowledge filter ID is required"}, status_code=400)
payload = await request.json()
user = request.state.user
jwt_token = request.cookies.get("auth_token")
# First, get the existing knowledge filter
existing_result = await knowledge_filter_service.get_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
if not existing_result.get("success"):
return JSONResponse({"error": "Knowledge filter not found or access denied"}, status_code=404)
existing_filter = existing_result["filter"]
# Delete the existing knowledge filter
delete_result = await knowledge_filter_service.delete_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
if not delete_result.get("success"):
return JSONResponse({"error": "Failed to delete existing knowledge filter"}, status_code=500)
# Create updated knowledge filter document with same ID
updated_filter = {
"id": filter_id,
"name": payload.get("name", existing_filter["name"]),
"description": payload.get("description", existing_filter["description"]),
"query_data": payload.get("queryData", existing_filter["query_data"]),
"owner": existing_filter["owner"],
"allowed_users": payload.get("allowedUsers", existing_filter.get("allowed_users", [])),
"allowed_groups": payload.get("allowedGroups", existing_filter.get("allowed_groups", [])),
"created_at": existing_filter["created_at"], # Preserve original creation time
"updated_at": datetime.utcnow().isoformat()
}
# Recreate the knowledge filter
result = await knowledge_filter_service.create_knowledge_filter(updated_filter, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)
async def delete_knowledge_filter(request: Request, knowledge_filter_service, session_manager):
"""Delete a knowledge filter"""
filter_id = request.path_params.get("filter_id")
if not filter_id:
return JSONResponse({"error": "Knowledge filter ID is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
result = await knowledge_filter_service.delete_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
return JSONResponse(result)

View file

@ -51,7 +51,7 @@ async def jwks_endpoint(request: Request, session_manager):
"kty": "RSA", "kty": "RSA",
"use": "sig", "use": "sig",
"alg": "RS256", "alg": "RS256",
"kid": "gendb-key-1", "kid": "openrag-key-1",
"n": int_to_base64url(public_numbers.n), "n": int_to_base64url(public_numbers.n),
"e": int_to_base64url(public_numbers.e) "e": int_to_base64url(public_numbers.e)
} }

View file

@ -23,7 +23,7 @@ from services.search_service import SearchService
from services.task_service import TaskService from services.task_service import TaskService
from services.auth_service import AuthService from services.auth_service import AuthService
from services.chat_service import ChatService from services.chat_service import ChatService
from services.contexts_service import ContextsService from services.knowledge_filter_service import KnowledgeFilterService
# Existing services # Existing services
from connectors.service import ConnectorService from connectors.service import ConnectorService
@ -31,7 +31,7 @@ from session_manager import SessionManager
from auth_middleware import require_auth, optional_auth from auth_middleware import require_auth, optional_auth
# API endpoints # API endpoints
from api import upload, search, chat, auth, connectors, tasks, oidc, contexts from api import upload, search, chat, auth, connectors, tasks, oidc, knowledge_filter
print("CUDA available:", torch.cuda.is_available()) print("CUDA available:", torch.cuda.is_available())
print("CUDA version PyTorch was built with:", torch.version.cuda) print("CUDA version PyTorch was built with:", torch.version.cuda)
@ -64,9 +64,9 @@ async def init_index():
else: else:
print(f"Index '{INDEX_NAME}' already exists, skipping creation.") print(f"Index '{INDEX_NAME}' already exists, skipping creation.")
# Create contexts index # Create knowledge filters index
contexts_index_name = "search_contexts" knowledge_filter_index_name = "knowledge_filters"
contexts_index_body = { knowledge_filter_index_body = {
"mappings": { "mappings": {
"properties": { "properties": {
"id": {"type": "keyword"}, "id": {"type": "keyword"},
@ -82,11 +82,11 @@ async def init_index():
} }
} }
if not await clients.opensearch.indices.exists(index=contexts_index_name): if not await clients.opensearch.indices.exists(index=knowledge_filter_index_name):
await clients.opensearch.indices.create(index=contexts_index_name, body=contexts_index_body) await clients.opensearch.indices.create(index=knowledge_filter_index_name, body=knowledge_filter_index_body)
print(f"Created index '{contexts_index_name}'") print(f"Created index '{knowledge_filter_index_name}'")
else: else:
print(f"Index '{contexts_index_name}' already exists, skipping creation.") print(f"Index '{knowledge_filter_index_name}' already exists, skipping creation.")
async def init_index_when_ready(): async def init_index_when_ready():
"""Initialize OpenSearch index when it becomes available""" """Initialize OpenSearch index when it becomes available"""
@ -111,7 +111,7 @@ def initialize_services():
search_service = SearchService(session_manager) search_service = SearchService(session_manager)
task_service = TaskService(document_service, process_pool) task_service = TaskService(document_service, process_pool)
chat_service = ChatService() chat_service = ChatService()
contexts_service = ContextsService(session_manager) knowledge_filter_service = KnowledgeFilterService(session_manager)
# Set process pool for document service # Set process pool for document service
document_service.process_pool = process_pool document_service.process_pool = process_pool
@ -136,7 +136,7 @@ def initialize_services():
'chat_service': chat_service, 'chat_service': chat_service,
'auth_service': auth_service, 'auth_service': auth_service,
'connector_service': connector_service, 'connector_service': connector_service,
'contexts_service': contexts_service, 'knowledge_filter_service': knowledge_filter_service,
'session_manager': session_manager 'session_manager': session_manager
} }
@ -198,39 +198,39 @@ def create_app():
session_manager=services['session_manager']) session_manager=services['session_manager'])
), methods=["POST"]), ), methods=["POST"]),
# Contexts endpoints # Knowledge Filter endpoints
Route("/contexts", Route("/knowledge-filter",
require_auth(services['session_manager'])( require_auth(services['session_manager'])(
partial(contexts.create_context, partial(knowledge_filter.create_knowledge_filter,
contexts_service=services['contexts_service'], knowledge_filter_service=services['knowledge_filter_service'],
session_manager=services['session_manager']) session_manager=services['session_manager'])
), methods=["POST"]), ), methods=["POST"]),
Route("/contexts/search", Route("/knowledge-filter/search",
require_auth(services['session_manager'])( require_auth(services['session_manager'])(
partial(contexts.search_contexts, partial(knowledge_filter.search_knowledge_filters,
contexts_service=services['contexts_service'], knowledge_filter_service=services['knowledge_filter_service'],
session_manager=services['session_manager']) session_manager=services['session_manager'])
), methods=["POST"]), ), methods=["POST"]),
Route("/contexts/{context_id}", Route("/knowledge-filter/{filter_id}",
require_auth(services['session_manager'])( require_auth(services['session_manager'])(
partial(contexts.get_context, partial(knowledge_filter.get_knowledge_filter,
contexts_service=services['contexts_service'], knowledge_filter_service=services['knowledge_filter_service'],
session_manager=services['session_manager']) session_manager=services['session_manager'])
), methods=["GET"]), ), methods=["GET"]),
Route("/contexts/{context_id}", Route("/knowledge-filter/{filter_id}",
require_auth(services['session_manager'])( require_auth(services['session_manager'])(
partial(contexts.update_context, partial(knowledge_filter.update_knowledge_filter,
contexts_service=services['contexts_service'], knowledge_filter_service=services['knowledge_filter_service'],
session_manager=services['session_manager']) session_manager=services['session_manager'])
), methods=["PUT"]), ), methods=["PUT"]),
Route("/contexts/{context_id}", Route("/knowledge-filter/{filter_id}",
require_auth(services['session_manager'])( require_auth(services['session_manager'])(
partial(contexts.delete_context, partial(knowledge_filter.delete_knowledge_filter,
contexts_service=services['contexts_service'], knowledge_filter_service=services['knowledge_filter_service'],
session_manager=services['session_manager']) session_manager=services['session_manager'])
), methods=["DELETE"]), ), methods=["DELETE"]),

View file

@ -77,8 +77,8 @@ class ChatService:
# Pass the complete filter expression as a single header to Langflow (only if we have something to send) # Pass the complete filter expression as a single header to Langflow (only if we have something to send)
if filter_expression: if filter_expression:
print(f"Sending GenDB query filter to Langflow: {json.dumps(filter_expression, indent=2)}") print(f"Sending OpenRAG query filter to Langflow: {json.dumps(filter_expression, indent=2)}")
extra_headers['X-LANGFLOW-GLOBAL-VAR-GENDB-QUERY-FILTER'] = json.dumps(filter_expression) extra_headers['X-LANGFLOW-GLOBAL-VAR-OPENRAG-QUERY-FILTER'] = json.dumps(filter_expression)
if stream: if stream:
return async_langflow_stream(clients.langflow_client, FLOW_ID, prompt, extra_headers=extra_headers, previous_response_id=previous_response_id) return async_langflow_stream(clients.langflow_client, FLOW_ID, prompt, extra_headers=extra_headers, previous_response_id=previous_response_id)

View file

@ -1,34 +1,34 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
CONTEXTS_INDEX_NAME = "search_contexts" KNOWLEDGE_FILTERS_INDEX_NAME = "knowledge_filters"
class ContextsService: class KnowledgeFilterService:
def __init__(self, session_manager=None): def __init__(self, session_manager=None):
self.session_manager = session_manager self.session_manager = session_manager
async def create_context(self, context_doc: Dict[str, Any], user_id: str = None, jwt_token: str = None) -> Dict[str, Any]: async def create_knowledge_filter(self, filter_doc: Dict[str, Any], user_id: str = None, jwt_token: str = None) -> Dict[str, Any]:
"""Create a new search context""" """Create a new knowledge filter"""
try: try:
# Get user's OpenSearch client with JWT for OIDC auth # Get user's OpenSearch client with JWT for OIDC auth
opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token) opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token)
# Index the context document # Index the knowledge filter document
result = await opensearch_client.index( result = await opensearch_client.index(
index=CONTEXTS_INDEX_NAME, index=KNOWLEDGE_FILTERS_INDEX_NAME,
id=context_doc["id"], id=filter_doc["id"],
body=context_doc body=filter_doc
) )
if result.get("result") == "created": if result.get("result") == "created":
return {"success": True, "id": context_doc["id"], "context": context_doc} return {"success": True, "id": filter_doc["id"], "filter": filter_doc}
else: else:
return {"success": False, "error": "Failed to create context"} return {"success": False, "error": "Failed to create knowledge filter"}
except Exception as e: except Exception as e:
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}
async def search_contexts(self, query: str, user_id: str = None, jwt_token: str = None, limit: int = 20) -> Dict[str, Any]: async def search_knowledge_filters(self, query: str, user_id: str = None, jwt_token: str = None, limit: int = 20) -> Dict[str, Any]:
"""Search for contexts by name, description, or query content""" """Search for knowledge filters by name, description, or query content"""
try: try:
# Get user's OpenSearch client with JWT for OIDC auth # Get user's OpenSearch client with JWT for OIDC auth
opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token) opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token)
@ -52,7 +52,7 @@ class ContextsService:
"size": limit "size": limit
} }
else: else:
# No query - return all contexts sorted by most recent # No query - return all knowledge filters sorted by most recent
search_body = { search_body = {
"query": {"match_all": {}}, "query": {"match_all": {}},
"sort": [{"updated_at": {"order": "desc"}}], "sort": [{"updated_at": {"order": "desc"}}],
@ -60,72 +60,72 @@ class ContextsService:
"size": limit "size": limit
} }
result = await opensearch_client.search(index=CONTEXTS_INDEX_NAME, body=search_body) result = await opensearch_client.search(index=KNOWLEDGE_FILTERS_INDEX_NAME, body=search_body)
# Transform results # Transform results
contexts = [] filters = []
for hit in result["hits"]["hits"]: for hit in result["hits"]["hits"]:
context = hit["_source"] knowledge_filter = hit["_source"]
context["score"] = hit.get("_score") knowledge_filter["score"] = hit.get("_score")
contexts.append(context) filters.append(knowledge_filter)
return {"success": True, "contexts": contexts} return {"success": True, "filters": filters}
except Exception as e: except Exception as e:
return {"success": False, "error": str(e), "contexts": []} return {"success": False, "error": str(e), "filters": []}
async def get_context(self, context_id: str, user_id: str = None, jwt_token: str = None) -> Dict[str, Any]: async def get_knowledge_filter(self, filter_id: str, user_id: str = None, jwt_token: str = None) -> Dict[str, Any]:
"""Get a specific context by ID""" """Get a specific knowledge filter by ID"""
try: try:
# Get user's OpenSearch client with JWT for OIDC auth # Get user's OpenSearch client with JWT for OIDC auth
opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token) opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token)
result = await opensearch_client.get(index=CONTEXTS_INDEX_NAME, id=context_id) result = await opensearch_client.get(index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_id)
if result.get("found"): if result.get("found"):
context = result["_source"] knowledge_filter = result["_source"]
return {"success": True, "context": context} return {"success": True, "filter": knowledge_filter}
else: else:
return {"success": False, "error": "Context not found"} return {"success": False, "error": "Knowledge filter not found"}
except Exception as e: except Exception as e:
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}
async def update_context(self, context_id: str, updates: Dict[str, Any], user_id: str = None, jwt_token: str = None) -> Dict[str, Any]: async def update_knowledge_filter(self, filter_id: str, updates: Dict[str, Any], user_id: str = None, jwt_token: str = None) -> Dict[str, Any]:
"""Update an existing context""" """Update an existing knowledge filter"""
try: try:
# Get user's OpenSearch client with JWT for OIDC auth # Get user's OpenSearch client with JWT for OIDC auth
opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token) opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token)
# Update the document # Update the document
result = await opensearch_client.update( result = await opensearch_client.update(
index=CONTEXTS_INDEX_NAME, index=KNOWLEDGE_FILTERS_INDEX_NAME,
id=context_id, id=filter_id,
body={"doc": updates} body={"doc": updates}
) )
if result.get("result") in ["updated", "noop"]: if result.get("result") in ["updated", "noop"]:
# Get the updated document # Get the updated document
updated_doc = await opensearch_client.get(index=CONTEXTS_INDEX_NAME, id=context_id) updated_doc = await opensearch_client.get(index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_id)
return {"success": True, "context": updated_doc["_source"]} return {"success": True, "filter": updated_doc["_source"]}
else: else:
return {"success": False, "error": "Failed to update context"} return {"success": False, "error": "Failed to update knowledge filter"}
except Exception as e: except Exception as e:
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}
async def delete_context(self, context_id: str, user_id: str = None, jwt_token: str = None) -> Dict[str, Any]: async def delete_knowledge_filter(self, filter_id: str, user_id: str = None, jwt_token: str = None) -> Dict[str, Any]:
"""Delete a context""" """Delete a knowledge filter"""
try: try:
# Get user's OpenSearch client with JWT for OIDC auth # Get user's OpenSearch client with JWT for OIDC auth
opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token) opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token)
result = await opensearch_client.delete(index=CONTEXTS_INDEX_NAME, id=context_id) result = await opensearch_client.delete(index=KNOWLEDGE_FILTERS_INDEX_NAME, id=filter_id)
if result.get("result") == "deleted": if result.get("result") == "deleted":
return {"success": True, "message": "Context deleted successfully"} return {"success": True, "message": "Knowledge filter deleted successfully"}
else: else:
return {"success": False, "error": "Failed to delete context"} return {"success": False, "error": "Failed to delete knowledge filter"}
except Exception as e: except Exception as e:
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}

View file

@ -109,7 +109,7 @@ class SessionManager:
# OIDC standard claims # OIDC standard claims
"iss": issuer, # Issuer from request "iss": issuer, # Issuer from request
"sub": user_id, # Subject (user ID) "sub": user_id, # Subject (user ID)
"aud": ["opensearch", "gendb"], # Audience "aud": ["opensearch", "openrag"], # Audience
"exp": now + timedelta(days=7), # Expiration "exp": now + timedelta(days=7), # Expiration
"iat": now, # Issued at "iat": now, # Issued at
"auth_time": int(now.timestamp()), # Authentication time "auth_time": int(now.timestamp()), # Authentication time
@ -120,7 +120,7 @@ class SessionManager:
"name": user.name, "name": user.name,
"preferred_username": user.email, "preferred_username": user.email,
"email_verified": True, "email_verified": True,
"roles": ["gendb_user"] # Backend role for OpenSearch "roles": ["openrag_user"] # Backend role for OpenSearch
} }
token = jwt.encode(token_payload, self.private_key, algorithm="RS256") token = jwt.encode(token_payload, self.private_key, algorithm="RS256")
@ -133,7 +133,7 @@ class SessionManager:
token, token,
self.public_key, self.public_key,
algorithms=["RS256"], algorithms=["RS256"],
audience=["opensearch", "gendb"] audience=["opensearch", "openrag"]
) )
return payload return payload
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:

78
uv.lock generated
View file

@ -483,45 +483,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" },
] ]
[[package]]
name = "gendb"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "agentd" },
{ name = "aiofiles" },
{ name = "cryptography" },
{ name = "docling" },
{ name = "google-api-python-client" },
{ name = "google-auth-httplib2" },
{ name = "google-auth-oauthlib" },
{ name = "httpx" },
{ name = "opensearch-py", extra = ["async"] },
{ name = "pyjwt" },
{ name = "python-multipart" },
{ name = "starlette" },
{ name = "torch" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "agentd", specifier = ">=0.2.2" },
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "cryptography", specifier = ">=45.0.6" },
{ name = "docling", specifier = ">=2.41.0" },
{ name = "google-api-python-client", specifier = ">=2.143.0" },
{ name = "google-auth-httplib2", specifier = ">=0.2.0" },
{ name = "google-auth-oauthlib", specifier = ">=1.2.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "opensearch-py", extras = ["async"], specifier = ">=3.0.0" },
{ name = "pyjwt", specifier = ">=2.8.0" },
{ name = "python-multipart", specifier = ">=0.0.20" },
{ name = "starlette", specifier = ">=0.47.1" },
{ name = "torch", specifier = ">=2.7.1", index = "https://download.pytorch.org/whl/cu128" },
{ name = "uvicorn", specifier = ">=0.35.0" },
]
[[package]] [[package]]
name = "google-api-core" name = "google-api-core"
version = "2.25.1" version = "2.25.1"
@ -1354,6 +1315,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
] ]
[[package]]
name = "openrag"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "agentd" },
{ name = "aiofiles" },
{ name = "cryptography" },
{ name = "docling" },
{ name = "google-api-python-client" },
{ name = "google-auth-httplib2" },
{ name = "google-auth-oauthlib" },
{ name = "httpx" },
{ name = "opensearch-py", extra = ["async"] },
{ name = "pyjwt" },
{ name = "python-multipart" },
{ name = "starlette" },
{ name = "torch" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "agentd", specifier = ">=0.2.2" },
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "cryptography", specifier = ">=45.0.6" },
{ name = "docling", specifier = ">=2.41.0" },
{ name = "google-api-python-client", specifier = ">=2.143.0" },
{ name = "google-auth-httplib2", specifier = ">=0.2.0" },
{ name = "google-auth-oauthlib", specifier = ">=1.2.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "opensearch-py", extras = ["async"], specifier = ">=3.0.0" },
{ name = "pyjwt", specifier = ">=2.8.0" },
{ name = "python-multipart", specifier = ">=0.0.20" },
{ name = "starlette", specifier = ">=0.47.1" },
{ name = "torch", specifier = ">=2.7.1", index = "https://download.pytorch.org/whl/cu128" },
{ name = "uvicorn", specifier = ">=0.35.0" },
]
[[package]] [[package]]
name = "opensearch-py" name = "opensearch-py"
version = "3.0.0" version = "3.0.0"