Merge branch 'main' of https://github.com/langflow-ai/openrag into app-shell

This commit is contained in:
Deon Sanchez 2025-10-01 14:33:07 -06:00
commit 93c2d2f1ad
20 changed files with 918 additions and 784 deletions

View file

@ -74,6 +74,7 @@ services:
- ./documents:/app/documents:Z
- ./keys:/app/keys:Z
- ./flows:/app/flows:Z
- ./config.yaml:/app/config.yaml:Z
openrag-frontend:
image: phact/openrag-frontend:${OPENRAG_VERSION:-latest}

View file

@ -73,6 +73,7 @@ services:
- ./documents:/app/documents:Z
- ./keys:/app/keys:Z
- ./flows:/app/flows:z
- ./config.yaml:/app/config.yaml:Z
gpus: all
openrag-frontend:

View file

@ -37,7 +37,7 @@ export function LabelWrapper({
>
<Label
htmlFor={id}
className="!text-mmd font-medium flex items-center gap-1.5"
className={cn("font-medium flex items-center gap-1.5", description ? "!text-sm" : "!text-mmd")}
>
{label}
{required && <span className="text-red-500">*</span>}

View file

@ -8,11 +8,24 @@ export default function IBMLogo(props: React.SVGProps<SVGSVGElement>) {
fill="none"
{...props}
>
<title>IBM Logo</title>
<path
d="M15.696 10.9901C15.7213 10.9901 15.7356 10.979 15.7356 10.9552V10.9313C15.7356 10.9076 15.7213 10.8964 15.696 10.8964H15.6359V10.9901H15.696ZM15.6359 11.1649H15.5552V10.8329H15.7055C15.7799 10.8329 15.8179 10.8773 15.8179 10.9378C15.8179 10.9901 15.7942 11.0235 15.7577 11.0378L15.8321 11.1649H15.7436L15.6818 11.0504H15.6359V11.1649ZM15.9255 11.0171V10.9759C15.9255 10.8424 15.821 10.7376 15.6833 10.7376C15.5456 10.7376 15.4412 10.8424 15.4412 10.9759V11.0171C15.4412 11.1505 15.5456 11.2554 15.6833 11.2554C15.821 11.2554 15.9255 11.1505 15.9255 11.0171ZM15.3668 10.9964C15.3668 10.8107 15.5077 10.6693 15.6833 10.6693C15.859 10.6693 16 10.8107 16 10.9964C16 11.1823 15.859 11.3237 15.6833 11.3237C15.5077 11.3237 15.3668 11.1823 15.3668 10.9964ZM10.8069 5.74885L10.6627 5.33301H8.28904V5.74885H10.8069ZM11.0821 6.54285L10.9379 6.12691H8.28904V6.54285H11.0821ZM12.8481 11.3067H14.9203V10.8908H12.8481V11.3067ZM12.8481 10.5126H14.9203V10.0968H12.8481V10.5126ZM12.8481 9.71873H14.0914V9.3028H12.8481V9.71873ZM12.8481 8.92474H14.0914V8.50889H12.8481V8.92474ZM12.8481 8.13084H14.0914V7.7149H11.7212L11.6047 8.05102L11.4882 7.7149H9.11794V8.13084H10.3613V7.74863L10.4951 8.13084H12.7143L12.8481 7.74863V8.13084ZM14.0914 6.921H11.9964L11.8522 7.33675H14.0914V6.921ZM9.11794 8.92474H10.3613V8.50889H9.11794V8.92474ZM9.11794 9.71873H10.3613V9.3028H9.11794V9.71873ZM8.28904 10.5126H10.3613V10.0968H8.28904V10.5126ZM8.28904 11.3067H10.3613V10.8908H8.28904V11.3067ZM12.5466 5.33301L12.4025 5.74885H14.9203V5.33301H12.5466ZM12.1273 6.54285H14.9203V6.12691H12.2714L12.1273 6.54285ZM9.11794 7.33675H11.3572L11.213 6.921H9.11794V7.33675ZM10.7727 8.92474H12.4366L12.5821 8.50889H10.6272L10.7727 8.92474ZM11.0505 9.71873H12.1588L12.3042 9.3028H10.9051L11.0505 9.71873ZM11.3283 10.5126H11.881L12.0265 10.0969H11.1828L11.3283 10.5126ZM11.604 11.3067L11.7487 10.8908H11.4606L11.604 11.3067ZM3.31561 11.3026L6.36754 11.3067C6.78195 11.3067 7.15365 11.1491 7.43506 10.8908H3.31561V11.3026ZM6.55592 9.3028V9.71873H7.94994C7.94994 9.57477 7.93029 9.43551 7.89456 9.3028H6.55592ZM4.14452 9.71873H5.38783V9.3028H4.14452V9.71873ZM6.55592 7.33675H7.89456C7.93029 7.20422 7.94994 7.06486 7.94994 6.921H6.55592V7.33675ZM4.14452 7.33675H5.38783V6.9209H4.14452V7.33675ZM6.36754 5.33301H3.31561V5.74885H7.43506C7.15365 5.49061 6.77892 5.33301 6.36754 5.33301ZM7.73778 6.12691H3.31561V6.54285H7.90448C7.86839 6.39502 7.81172 6.25539 7.73778 6.12691ZM4.14452 7.7149V8.13084H7.39152C7.5292 8.01333 7.64621 7.87268 7.73732 7.7149H4.14452ZM7.39152 8.50889H4.14452V8.92474H7.73732C7.64621 8.76695 7.5292 8.62631 7.39152 8.50889ZM3.31561 10.5126H7.73778C7.81172 10.3843 7.86839 10.2447 7.90448 10.0969H3.31561V10.5126ZM0 5.74885H2.90121V5.33301H0V5.74885ZM0 6.54285H2.90121V6.12691H0V6.54285ZM0.828996 7.33684H2.0723V6.921H0.828996V7.33684ZM0.828996 8.13084H2.0723V7.7149H0.828996V8.13084ZM0.828996 8.92474H2.0723V8.50889H0.828996V8.92474ZM0.828996 9.71873H2.0723V9.3028H0.828996V9.71873ZM0 10.5126H2.90121V10.0968H0V10.5126ZM0 11.3067H2.90121V10.8908H0V11.3067Z"
fill="currentColor"
/>
<title>IBM watsonx.ai Logo</title>
<g clip-path="url(#clip0_2620_2081)">
<path
d="M13 12.0007C12.4477 12.0007 12 12.4484 12 13.0007C12 13.0389 12.0071 13.0751 12.0112 13.1122C10.8708 14.0103 9.47165 14.5007 8 14.5007C5.86915 14.5007 4 12.5146 4 10.2507C4 7.90722 5.9065 6.00072 8.25 6.00072H8.5V5.00072H8.25C5.3552 5.00072 3 7.35592 3 10.2507C3 11.1927 3.2652 12.0955 3.71855 12.879C2.3619 11.6868 1.5 9.94447 1.5 8.00072C1.5 6.94312 1.74585 5.93432 2.23095 5.00292L1.34375 4.54102C0.79175 5.60157 0.5 6.79787 0.5 8.00072C0.5 12.1362 3.8645 15.5007 8 15.5007C9.6872 15.5007 11.2909 14.9411 12.6024 13.9176C12.7244 13.9706 12.8586 14.0007 13 14.0007C13.5523 14.0007 14 13.553 14 13.0007C14 12.4484 13.5523 12.0007 13 12.0007Z"
fill="currentColor"
/>
<path d="M6.5 11V10H5.5V11H6.5Z" fill="currentColor" />
<path d="M10.5 6V5H9.5V6H10.5Z" fill="currentColor" />
<path
d="M8 0.5C6.3128 0.5 4.7091 1.05965 3.3976 2.0831C3.2756 2.0301 3.14145 2 3 2C2.4477 2 2 2.4477 2 3C2 3.5523 2.4477 4 3 4C3.5523 4 4 3.5523 4 3C4 2.9618 3.9929 2.9256 3.98875 2.88855C5.12915 1.9904 6.52835 1.5 8 1.5C10.1308 1.5 12 3.4861 12 5.75C12 8.0935 10.0935 10 7.75 10H7.5V11H7.75C10.6448 11 13 8.6448 13 5.75C13 4.80735 12.7339 3.90415 12.28 3.12035C13.6375 4.3125 14.5 6.05555 14.5 8C14.5 9.0576 14.2541 10.0664 13.769 10.9978L14.6562 11.4597C15.2083 10.3991 15.5 9.20285 15.5 8C15.5 3.8645 12.1355 0.5 8 0.5Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_2620_2081">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View file

@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none disabled:select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {

View file

@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
"rounded-xl border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}

View file

@ -1,6 +1,6 @@
"use client"
import { useEffect, useState, useRef, Suspense } from "react"
import { useEffect, useState, Suspense } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@ -14,17 +14,20 @@ function AuthCallbackContent() {
const [status, setStatus] = useState<"processing" | "success" | "error">("processing")
const [error, setError] = useState<string | null>(null)
const [purpose, setPurpose] = useState<string>("app_auth")
const hasProcessed = useRef(false)
useEffect(() => {
// Prevent double execution in React Strict Mode
if (hasProcessed.current) return
hasProcessed.current = true
const code = searchParams.get('code')
const callbackKey = `callback_processed_${code}`
// Prevent double execution across component remounts
if (sessionStorage.getItem(callbackKey)) {
return
}
sessionStorage.setItem(callbackKey, 'true')
const handleCallback = async () => {
try {
// Get parameters from URL
const code = searchParams.get('code')
const state = searchParams.get('state')
const errorParam = searchParams.get('error')

View file

@ -121,7 +121,6 @@ function ChatPage() {
>(new Set());
// previousResponseIds now comes from useChat context
const [isUploading, setIsUploading] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
const [availableFilters, setAvailableFilters] = useState<
KnowledgeFilterData[]
@ -132,7 +131,6 @@ function ChatPage() {
const [dropdownDismissed, setDropdownDismissed] = useState(false);
const [isUserInteracting, setIsUserInteracting] = useState(false);
const [isForkingInProgress, setIsForkingInProgress] = useState(false);
const dragCounterRef = useRef(0);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -275,43 +273,6 @@ function ChatPage() {
}
};
// Remove the old pollTaskStatus function since we're using centralized system
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragCounterRef.current++;
if (dragCounterRef.current === 1) {
setIsDragOver(true);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragCounterRef.current--;
if (dragCounterRef.current === 0) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragCounterRef.current = 0;
setIsDragOver(false);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
handleFileUpload(files[0]); // Upload first file only
}
};
const handleFilePickerClick = () => {
fileInputRef.current?.click();
};
@ -1958,31 +1919,12 @@ function ChatPage() {
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-hidden">
{/* Messages Area */}
<div
className={`flex-1 overflow-y-auto overflow-x-hidden scrollbar-hide space-y-6 min-h-0 transition-all relative ${
isDragOver
? "bg-primary/10 border-2 border-dashed border-primary rounded-lg p-4"
: ""
}`}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`flex-1 overflow-y-auto overflow-x-hidden scrollbar-hide space-y-6 min-h-0 transition-all relative`}
>
{messages.length === 0 && !streamingMessage ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center">
{isDragOver ? (
<>
<Upload className="h-12 w-12 mx-auto mb-4 text-primary" />
<p className="text-primary font-medium">
Drop your document here
</p>
<p className="text-sm mt-2">
I&apos;ll process it and add it to our conversation
context
</p>
</>
) : isUploading ? (
{isUploading ? (
<>
<Loader2 className="h-12 w-12 mx-auto mb-4 animate-spin" />
<p>Processing your document...</p>
@ -1999,8 +1941,8 @@ function ChatPage() {
<div key={index} className="space-y-6 group">
{message.role === "user" && (
<div className="flex gap-3">
<Avatar className="w-8 h-8 flex-shrink-0">
<AvatarImage src={user?.picture} alt={user?.name} />
<Avatar className="w-8 h-8 flex-shrink-0 select-none">
<AvatarImage draggable={false} src={user?.picture} alt={user?.name} />
<AvatarFallback className="text-sm bg-primary/20 text-primary">
{user?.name ? (
user.name.charAt(0).toUpperCase()
@ -2019,7 +1961,7 @@ function ChatPage() {
{message.role === "assistant" && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
<Bot className="h-4 w-4 text-accent-foreground" />
</div>
<div className="flex-1 min-w-0">
@ -2083,18 +2025,6 @@ function ChatPage() {
<div ref={messagesEndRef} />
</>
)}
{/* Drag overlay for existing messages */}
{isDragOver && messages.length > 0 && (
<div className="absolute inset-0 bg-primary/20 backdrop-blur-sm flex items-center justify-center rounded-lg">
<div className="text-center">
<Upload className="h-8 w-8 mx-auto mb-2 text-primary" />
<p className="text-primary font-medium">
Drop document to add context
</p>
</div>
</div>
)}
</div>
</div>
</div>
@ -2344,12 +2274,14 @@ function ChatPage() {
<button
key={filter.id}
onClick={() => handleFilterSelect(filter)}
className={`w-full text-left px-2 py-2 text-sm rounded hover:bg-muted/50 flex items-center justify-between ${
className={`w-full overflow-hidden text-left px-2 py-2 gap-2 text-sm rounded hover:bg-muted/50 flex items-center justify-between ${
index === selectedFilterIndex ? "bg-muted/50" : ""
}`}
>
<div>
<div className="font-medium">{filter.name}</div>
<div className="overflow-hidden">
<div className="font-medium truncate">
{filter.name}
</div>
{filter.description && (
<div className="text-xs text-muted-foreground truncate">
{filter.description}
@ -2357,7 +2289,7 @@ function ChatPage() {
)}
</div>
{selectedFilter?.id === filter.id && (
<div className="w-2 h-2 rounded-full bg-blue-500" />
<div className="w-2 h-2 shrink-0 rounded-full bg-blue-500" />
)}
</button>
))}

View file

@ -69,18 +69,12 @@ function LoginPageContent() {
/>
<div className="flex flex-col items-center justify-center gap-4 z-10">
<Logo className="fill-primary" width={32} height={28} />
<div className="flex flex-col items-center justify-center gap-8">
<h1 className="text-2xl font-medium font-chivo">Welcome to OpenRAG</h1>
<p className="text-sm text-muted-foreground">
All your knowledge at your fingertips.
</p>
<Button onClick={login} className="w-80 gap-1.5" size="lg">
<GoogleLogo className="h-4 w-4" />
Continue with Google
</Button>
</div>
<div className="flex items-center justify-center gap-2 absolute bottom-6 text-xs text-muted-foreground z-10">
<p className="text-accent-emerald-foreground">Systems Operational</p>
<p>Privacy Policy</p>
</Button></div>
</div>
</div>
);

View file

@ -111,6 +111,7 @@ export function IBMOnboarding({
<ModelSelector
options={options}
value={endpoint}
custom
onValueChange={setEndpoint}
searchPlaceholder="Search endpoint..."
noOptionsPlaceholder="No endpoints available"
@ -118,8 +119,17 @@ export function IBMOnboarding({
/>
</LabelWrapper>
<LabelInput
label="IBM API key"
helperText="The API key for your watsonx.ai account."
label="watsonx Project ID"
helperText="Project ID for the model"
id="project-id"
required
placeholder="your-project-id"
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
/>
<LabelInput
label="watsonx API key"
helperText="API key to access watsonx.ai"
id="api-key"
type="password"
required
@ -127,15 +137,6 @@ export function IBMOnboarding({
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
<LabelInput
label="IBM Project ID"
helperText="The project ID for your watsonx.ai account."
id="project-id"
required
placeholder="your-project-id"
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
/>
{isLoadingModels && (
<p className="text-mmd text-muted-foreground">
Validating configuration...

View file

@ -1,115 +1,158 @@
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
export function ModelSelector({
options,
value,
onValueChange,
icon,
placeholder = "Select model...",
searchPlaceholder = "Search model...",
noOptionsPlaceholder = "No models available",
options,
value,
onValueChange,
icon,
placeholder = "Select model...",
searchPlaceholder = "Search model...",
noOptionsPlaceholder = "No models available",
custom = false,
}: {
options: {
value: string;
label: string;
default?: boolean;
}[];
value: string;
icon?: React.ReactNode;
placeholder?: string;
searchPlaceholder?: string;
noOptionsPlaceholder?: string;
onValueChange: (value: string) => void;
options: {
value: string;
label: string;
default?: boolean;
}[];
value: string;
icon?: React.ReactNode;
placeholder?: string;
searchPlaceholder?: string;
noOptionsPlaceholder?: string;
custom?: boolean;
onValueChange: (value: string) => void;
}) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (value && !options.find((option) => option.value === value)) {
onValueChange("");
}
}, [options, value, onValueChange]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{/** biome-ignore lint/a11y/useSemanticElements: has to be a Button */}
<Button
variant="outline"
role="combobox"
disabled={options.length === 0}
aria-expanded={open}
className="w-full gap-2 justify-between font-normal text-sm"
>
{value ? (
<div className="flex items-center gap-2">
{icon && <div className="w-4 h-4">{icon}</div>}
{options.find((framework) => framework.value === value)?.label}
{options.find((framework) => framework.value === value)
?.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Default
</span>
)}
</div>
) : options.length === 0 ? (
noOptionsPlaceholder
) : (
placeholder
)}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-[400px] p-0">
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
if (currentValue !== value) {
onValueChange(currentValue);
}
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex items-center gap-2">
{option.label}
{option.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Default
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState("");
useEffect(() => {
if (value && (!options.find((option) => option.value === value) && !custom)) {
onValueChange("");
}
}, [options, value, custom, onValueChange]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{/** biome-ignore lint/a11y/useSemanticElements: has to be a Button */}
<Button
variant="outline"
role="combobox"
disabled={options.length === 0}
aria-expanded={open}
className="w-full gap-2 justify-between font-normal text-sm"
>
{value ? (
<div className="flex items-center gap-2">
{icon && <div className="w-4 h-4">{icon}</div>}
{options.find((framework) => framework.value === value)?.label ||
value}
{/* {options.find((framework) => framework.value === value)
?.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Default
</span>
)} */}
{custom &&
value &&
!options.find((framework) => framework.value === value) && (
<Badge variant="outline" className="text-xs">
CUSTOM
</Badge>
)}
</div>
) : options.length === 0 ? (
noOptionsPlaceholder
) : (
placeholder
)}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className=" p-0 w-[var(--radix-popover-trigger-width)]">
<Command>
<CommandInput
placeholder={searchPlaceholder}
value={searchValue}
onValueChange={setSearchValue}
/>
<CommandList>
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {
if (currentValue !== value) {
onValueChange(currentValue);
}
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex items-center gap-2">
{option.label}
{/* {option.default && (
<span className="text-xs text-foreground p-1 rounded-md bg-muted"> // DISABLING DEFAULT TAG FOR NOW
Default
</span>
)} */}
</div>
</CommandItem>
))}
{custom &&
searchValue &&
!options.find((option) => option.value === searchValue) && (
<CommandItem
value={searchValue}
onSelect={(currentValue) => {
if (currentValue !== value) {
onValueChange(currentValue);
}
setOpen(false);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === searchValue ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex items-center gap-2">
{searchValue}
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
Custom
</span>
</div>
</CommandItem>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View file

@ -2,7 +2,7 @@ import { useState } from "react";
import { LabelInput } from "@/components/label-input";
import { LabelWrapper } from "@/components/label-wrapper";
import OpenAILogo from "@/components/logo/openai-logo";
import { Checkbox } from "@/components/ui/checkbox";
import { Switch } from "@/components/ui/switch";
import { useDebouncedValue } from "@/lib/debounce";
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
import { useGetOpenAIModelsQuery } from "../../api/queries/useGetModelsQuery";
@ -11,121 +11,114 @@ import { useUpdateSettings } from "../hooks/useUpdateSettings";
import { AdvancedOnboarding } from "./advanced";
export function OpenAIOnboarding({
setSettings,
sampleDataset,
setSampleDataset,
setSettings,
sampleDataset,
setSampleDataset,
}: {
setSettings: (settings: OnboardingVariables) => void;
sampleDataset: boolean;
setSampleDataset: (dataset: boolean) => void;
setSettings: (settings: OnboardingVariables) => void;
sampleDataset: boolean;
setSampleDataset: (dataset: boolean) => void;
}) {
const [apiKey, setApiKey] = useState("");
const [getFromEnv, setGetFromEnv] = useState(true);
const debouncedApiKey = useDebouncedValue(apiKey, 500);
const [apiKey, setApiKey] = useState("");
const [getFromEnv, setGetFromEnv] = useState(true);
const debouncedApiKey = useDebouncedValue(apiKey, 500);
// Fetch models from API when API key is provided
const {
data: modelsData,
isLoading: isLoadingModels,
error: modelsError,
} = useGetOpenAIModelsQuery(
getFromEnv
? { apiKey: "" }
: debouncedApiKey
? { apiKey: debouncedApiKey }
: undefined,
{ enabled: debouncedApiKey !== "" || getFromEnv },
);
// Use custom hook for model selection logic
const {
languageModel,
embeddingModel,
setLanguageModel,
setEmbeddingModel,
languageModels,
embeddingModels,
} = useModelSelection(modelsData);
const handleSampleDatasetChange = (dataset: boolean) => {
setSampleDataset(dataset);
};
// Fetch models from API when API key is provided
const {
data: modelsData,
isLoading: isLoadingModels,
error: modelsError,
} = useGetOpenAIModelsQuery(
getFromEnv
? { apiKey: "" }
: debouncedApiKey
? { apiKey: debouncedApiKey }
: undefined,
{ enabled: debouncedApiKey !== "" || getFromEnv },
);
// Use custom hook for model selection logic
const {
languageModel,
embeddingModel,
setLanguageModel,
setEmbeddingModel,
languageModels,
embeddingModels,
} = useModelSelection(modelsData);
const handleSampleDatasetChange = (dataset: boolean) => {
setSampleDataset(dataset);
};
const handleGetFromEnvChange = (fromEnv: boolean) => {
setGetFromEnv(fromEnv);
if (fromEnv) {
setApiKey("");
}
setLanguageModel("");
setEmbeddingModel("");
};
const handleGetFromEnvChange = (fromEnv: boolean) => {
setGetFromEnv(fromEnv);
if (fromEnv) {
setApiKey("");
}
setLanguageModel("");
setEmbeddingModel("");
};
// Update settings when values change
useUpdateSettings(
"openai",
{
apiKey,
languageModel,
embeddingModel,
},
setSettings,
);
return (
<>
<div className="space-y-5">
<LabelWrapper
label="Use environment OpenAI API key"
id="get-api-key"
helperText={
<>
Reuse the key from your environment config.
<br />
Uncheck to enter a different key.
</>
}
flex
start
>
<Checkbox
checked={getFromEnv}
onCheckedChange={handleGetFromEnvChange}
/>
</LabelWrapper>
{!getFromEnv && (
<div className="space-y-1">
<LabelInput
label="OpenAI API key"
helperText="The API key for your OpenAI account."
className={modelsError ? "!border-destructive" : ""}
id="api-key"
type="password"
required
placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
{isLoadingModels && (
<p className="text-mmd text-muted-foreground">
Validating API key...
</p>
)}
{modelsError && (
<p className="text-mmd text-destructive">
Invalid OpenAI API key. Verify or replace the key.
</p>
)}
</div>
)}
</div>
<AdvancedOnboarding
icon={<OpenAILogo className="w-4 h-4" />}
languageModels={languageModels}
embeddingModels={embeddingModels}
languageModel={languageModel}
embeddingModel={embeddingModel}
sampleDataset={sampleDataset}
setLanguageModel={setLanguageModel}
setSampleDataset={handleSampleDatasetChange}
setEmbeddingModel={setEmbeddingModel}
/>
</>
);
// Update settings when values change
useUpdateSettings(
"openai",
{
apiKey,
languageModel,
embeddingModel,
},
setSettings,
);
return (
<>
<div className="space-y-5">
<LabelWrapper
label="Use environment OpenAI API key"
id="get-api-key"
description="Reuse the key from your environment config. Turn off to enter a different key."
flex
>
<Switch
checked={getFromEnv}
onCheckedChange={handleGetFromEnvChange}
/>
</LabelWrapper>
{!getFromEnv && (
<div className="space-y-1">
<LabelInput
label="OpenAI API key"
helperText="The API key for your OpenAI account."
className={modelsError ? "!border-destructive" : ""}
id="api-key"
type="password"
required
placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
{isLoadingModels && (
<p className="text-mmd text-muted-foreground">
Validating API key...
</p>
)}
{modelsError && (
<p className="text-mmd text-destructive">
Invalid OpenAI API key. Verify or replace the key.
</p>
)}
</div>
)}
</div>
<AdvancedOnboarding
icon={<OpenAILogo className="w-4 h-4" />}
languageModels={languageModels}
embeddingModels={embeddingModels}
languageModel={languageModel}
embeddingModel={embeddingModel}
sampleDataset={sampleDataset}
setLanguageModel={setLanguageModel}
setSampleDataset={handleSampleDatasetChange}
setEmbeddingModel={setEmbeddingModel}
/>
</>
);
}

View file

@ -68,7 +68,6 @@ function OnboardingPage() {
// Mutations
const onboardingMutation = useOnboardingMutation({
onSuccess: (data) => {
toast.success("Onboarding completed successfully!");
console.log("Onboarding completed successfully", data);
router.push(redirect);
},
@ -137,7 +136,7 @@ function OnboardingPage() {
Connect a model provider
</h1>
</div>
<Card className="w-full max-w-[580px]">
<Card className="w-full max-w-[600px]">
<Tabs
defaultValue={modelProvider}
onValueChange={handleSetModelProvider}
@ -150,7 +149,7 @@ function OnboardingPage() {
</TabsTrigger>
<TabsTrigger value="watsonx">
<IBMLogo className="w-4 h-4" />
IBM
IBM watsonx.ai
</TabsTrigger>
<TabsTrigger value="ollama">
<OllamaLogo className="w-4 h-4" />
@ -192,7 +191,7 @@ function OnboardingPage() {
disabled={!isComplete}
loading={onboardingMutation.isPending}
>
Complete
<span className="select-none">Complete</span>
</Button>
</div>
</TooltipTrigger>

View file

@ -1,386 +1,378 @@
"use client";
import { useState, useEffect } from "react";
import { AlertCircle, ArrowLeft } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { ArrowLeft, AlertCircle } from "lucide-react";
import { UnifiedCloudPicker, CloudFile } from "@/components/cloud-picker";
import { useEffect, useState } from "react";
import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker";
import type { IngestSettings } from "@/components/cloud-picker/types";
import { useTask } from "@/contexts/task-context";
import { Button } from "@/components/ui/button";
import { Toast } from "@/components/ui/toast";
import { useTask } from "@/contexts/task-context";
// CloudFile interface is now imported from the unified cloud picker
interface CloudConnector {
id: string;
name: string;
description: string;
status: "not_connected" | "connecting" | "connected" | "error";
type: string;
connectionId?: string;
clientId: string;
hasAccessToken: boolean;
accessTokenError?: string;
id: string;
name: string;
description: string;
status: "not_connected" | "connecting" | "connected" | "error";
type: string;
connectionId?: string;
clientId: string;
hasAccessToken: boolean;
accessTokenError?: string;
}
export default function UploadProviderPage() {
const params = useParams();
const router = useRouter();
const provider = params.provider as string;
const { addTask, tasks } = useTask();
const params = useParams();
const router = useRouter();
const provider = params.provider as string;
const { addTask, tasks } = useTask();
const [connector, setConnector] = useState<CloudConnector | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
const [isIngesting, setIsIngesting] = useState<boolean>(false);
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(
null
);
const [showSuccessToast, setShowSuccessToast] = useState(false);
const [ingestSettings, setIngestSettings] = useState<IngestSettings>({
chunkSize: 1000,
chunkOverlap: 200,
ocr: false,
pictureDescriptions: false,
embeddingModel: "text-embedding-3-small",
});
const [connector, setConnector] = useState<CloudConnector | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
const [isIngesting, setIsIngesting] = useState<boolean>(false);
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(
null,
);
const [ingestSettings, setIngestSettings] = useState<IngestSettings>({
chunkSize: 1000,
chunkOverlap: 200,
ocr: false,
pictureDescriptions: false,
embeddingModel: "text-embedding-3-small",
});
useEffect(() => {
const fetchConnectorInfo = async () => {
setIsLoading(true);
setError(null);
useEffect(() => {
const fetchConnectorInfo = async () => {
setIsLoading(true);
setError(null);
try {
// Fetch available connectors to validate the provider
const connectorsResponse = await fetch("/api/connectors");
if (!connectorsResponse.ok) {
throw new Error("Failed to load connectors");
}
try {
// Fetch available connectors to validate the provider
const connectorsResponse = await fetch("/api/connectors");
if (!connectorsResponse.ok) {
throw new Error("Failed to load connectors");
}
const connectorsResult = await connectorsResponse.json();
const providerInfo = connectorsResult.connectors[provider];
const connectorsResult = await connectorsResponse.json();
const providerInfo = connectorsResult.connectors[provider];
if (!providerInfo || !providerInfo.available) {
setError(
`Cloud provider "${provider}" is not available or configured.`
);
return;
}
if (!providerInfo || !providerInfo.available) {
setError(
`Cloud provider "${provider}" is not available or configured.`,
);
return;
}
// Check connector status
const statusResponse = await fetch(
`/api/connectors/${provider}/status`
);
if (!statusResponse.ok) {
throw new Error(`Failed to check ${provider} status`);
}
// Check connector status
const statusResponse = await fetch(
`/api/connectors/${provider}/status`,
);
if (!statusResponse.ok) {
throw new Error(`Failed to check ${provider} status`);
}
const statusData = await statusResponse.json();
const connections = statusData.connections || [];
const activeConnection = connections.find(
(conn: { is_active: boolean; connection_id: string }) =>
conn.is_active
);
const isConnected = activeConnection !== undefined;
const statusData = await statusResponse.json();
const connections = statusData.connections || [];
const activeConnection = connections.find(
(conn: { is_active: boolean; connection_id: string }) =>
conn.is_active,
);
const isConnected = activeConnection !== undefined;
let hasAccessToken = false;
let accessTokenError: string | undefined = undefined;
let hasAccessToken = false;
let accessTokenError: string | undefined;
// Try to get access token for connected connectors
if (isConnected && activeConnection) {
try {
const tokenResponse = await fetch(
`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`
);
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
if (tokenData.access_token) {
hasAccessToken = true;
setAccessToken(tokenData.access_token);
}
} else {
const errorData = await tokenResponse
.json()
.catch(() => ({ error: "Token unavailable" }));
accessTokenError = errorData.error || "Access token unavailable";
}
} catch {
accessTokenError = "Failed to fetch access token";
}
}
// Try to get access token for connected connectors
if (isConnected && activeConnection) {
try {
const tokenResponse = await fetch(
`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`,
);
if (tokenResponse.ok) {
const tokenData = await tokenResponse.json();
if (tokenData.access_token) {
hasAccessToken = true;
setAccessToken(tokenData.access_token);
}
} else {
const errorData = await tokenResponse
.json()
.catch(() => ({ error: "Token unavailable" }));
accessTokenError = errorData.error || "Access token unavailable";
}
} catch {
accessTokenError = "Failed to fetch access token";
}
}
setConnector({
id: provider,
name: providerInfo.name,
description: providerInfo.description,
status: isConnected ? "connected" : "not_connected",
type: provider,
connectionId: activeConnection?.connection_id,
clientId: activeConnection?.client_id,
hasAccessToken,
accessTokenError,
});
} catch (error) {
console.error("Failed to load connector info:", error);
setError(
error instanceof Error
? error.message
: "Failed to load connector information"
);
} finally {
setIsLoading(false);
}
};
setConnector({
id: provider,
name: providerInfo.name,
description: providerInfo.description,
status: isConnected ? "connected" : "not_connected",
type: provider,
connectionId: activeConnection?.connection_id,
clientId: activeConnection?.client_id,
hasAccessToken,
accessTokenError,
});
} catch (error) {
console.error("Failed to load connector info:", error);
setError(
error instanceof Error
? error.message
: "Failed to load connector information",
);
} finally {
setIsLoading(false);
}
};
if (provider) {
fetchConnectorInfo();
}
}, [provider]);
if (provider) {
fetchConnectorInfo();
}
}, [provider]);
// Watch for sync task completion and redirect
useEffect(() => {
if (!currentSyncTaskId) return;
// Watch for sync task completion and redirect
useEffect(() => {
if (!currentSyncTaskId) return;
const currentTask = tasks.find(task => task.task_id === currentSyncTaskId);
const currentTask = tasks.find(
(task) => task.task_id === currentSyncTaskId,
);
if (currentTask && currentTask.status === "completed") {
// Task completed successfully, show toast and redirect
setIsIngesting(false);
setShowSuccessToast(true);
setTimeout(() => {
router.push("/knowledge");
}, 2000); // 2 second delay to let user see toast
} else if (currentTask && currentTask.status === "failed") {
// Task failed, clear the tracking but don't redirect
setIsIngesting(false);
setCurrentSyncTaskId(null);
}
}, [tasks, currentSyncTaskId, router]);
if (currentTask && currentTask.status === "completed") {
// Task completed successfully, show toast and redirect
setIsIngesting(false);
setTimeout(() => {
router.push("/knowledge");
}, 2000); // 2 second delay to let user see toast
} else if (currentTask && currentTask.status === "failed") {
// Task failed, clear the tracking but don't redirect
setIsIngesting(false);
setCurrentSyncTaskId(null);
}
}, [tasks, currentSyncTaskId, router]);
const handleFileSelected = (files: CloudFile[]) => {
setSelectedFiles(files);
console.log(`Selected ${files.length} files from ${provider}:`, files);
// You can add additional handling here like triggering sync, etc.
};
const handleFileSelected = (files: CloudFile[]) => {
setSelectedFiles(files);
console.log(`Selected ${files.length} files from ${provider}:`, files);
// You can add additional handling here like triggering sync, etc.
};
const handleSync = async (connector: CloudConnector) => {
if (!connector.connectionId || selectedFiles.length === 0) return;
const handleSync = async (connector: CloudConnector) => {
if (!connector.connectionId || selectedFiles.length === 0) return;
setIsIngesting(true);
setIsIngesting(true);
try {
const syncBody: {
connection_id: string;
max_files?: number;
selected_files?: string[];
settings?: IngestSettings;
} = {
connection_id: connector.connectionId,
selected_files: selectedFiles.map(file => file.id),
settings: ingestSettings,
};
try {
const syncBody: {
connection_id: string;
max_files?: number;
selected_files?: string[];
settings?: IngestSettings;
} = {
connection_id: connector.connectionId,
selected_files: selectedFiles.map((file) => file.id),
settings: ingestSettings,
};
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(syncBody),
});
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(syncBody),
});
const result = await response.json();
const result = await response.json();
if (response.status === 201) {
const taskIds = result.task_ids;
if (taskIds && taskIds.length > 0) {
const taskId = taskIds[0]; // Use the first task ID
addTask(taskId);
setCurrentSyncTaskId(taskId);
}
} else {
console.error("Sync failed:", result.error);
}
} catch (error) {
console.error("Sync error:", error);
setIsIngesting(false);
}
};
if (response.status === 201) {
const taskIds = result.task_ids;
if (taskIds && taskIds.length > 0) {
const taskId = taskIds[0]; // Use the first task ID
addTask(taskId);
setCurrentSyncTaskId(taskId);
}
} else {
console.error("Sync failed:", result.error);
}
} catch (error) {
console.error("Sync error:", error);
setIsIngesting(false);
}
};
const getProviderDisplayName = () => {
const nameMap: { [key: string]: string } = {
google_drive: "Google Drive",
onedrive: "OneDrive",
sharepoint: "SharePoint",
};
return nameMap[provider] || provider;
};
const getProviderDisplayName = () => {
const nameMap: { [key: string]: string } = {
google_drive: "Google Drive",
onedrive: "OneDrive",
sharepoint: "SharePoint",
};
return nameMap[provider] || provider;
};
if (isLoading) {
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Loading {getProviderDisplayName()} connector...</p>
</div>
</div>
</div>
);
}
if (isLoading) {
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Loading {getProviderDisplayName()} connector...</p>
</div>
</div>
</div>
);
}
if (error || !connector) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
if (error || !connector) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Provider Not Available
</h2>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={() => router.push("/settings")}>
Configure Connectors
</Button>
</div>
</div>
</div>
);
}
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Provider Not Available
</h2>
<p className="text-muted-foreground mb-4">{error}</p>
<Button onClick={() => router.push("/settings")}>
Configure Connectors
</Button>
</div>
</div>
</div>
);
}
if (connector.status !== "connected") {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
if (connector.status !== "connected") {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
{connector.name} Not Connected
</h2>
<p className="text-muted-foreground mb-4">
You need to connect your {connector.name} account before you can
select files.
</p>
<Button onClick={() => router.push("/settings")}>
Connect {connector.name}
</Button>
</div>
</div>
</div>
);
}
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
{connector.name} Not Connected
</h2>
<p className="text-muted-foreground mb-4">
You need to connect your {connector.name} account before you can
select files.
</p>
<Button onClick={() => router.push("/settings")}>
Connect {connector.name}
</Button>
</div>
</div>
</div>
);
}
if (!connector.hasAccessToken) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
if (!connector.hasAccessToken) {
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
</div>
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Access Token Required
</h2>
<p className="text-muted-foreground mb-4">
{connector.accessTokenError ||
`Unable to get access token for ${connector.name}. Try reconnecting your account.`}
</p>
<Button onClick={() => router.push("/settings")}>
Reconnect {connector.name}
</Button>
</div>
</div>
</div>
);
}
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">
Access Token Required
</h2>
<p className="text-muted-foreground mb-4">
{connector.accessTokenError ||
`Unable to get access token for ${connector.name}. Try reconnecting your account.`}
</p>
<Button onClick={() => router.push("/settings")}>
Reconnect {connector.name}
</Button>
</div>
</div>
</div>
);
}
return (
<div className="container mx-auto max-w-3xl p-6">
<div className="mb-6 flex gap-2 items-center">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 scale-125" />
</Button>
<h2 className="text-2xl font-bold">
Add from {getProviderDisplayName()}
</h2>
</div>
return (
<div className="container mx-auto max-w-3xl p-6">
<div className="mb-6 flex gap-2 items-center">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4 scale-125" />
</Button>
<h2 className="text-2xl font-bold">
Add from {getProviderDisplayName()}
</h2>
</div>
<div className="max-w-3xl mx-auto">
<UnifiedCloudPicker
provider={
connector.type as "google_drive" | "onedrive" | "sharepoint"
}
onFileSelected={handleFileSelected}
selectedFiles={selectedFiles}
isAuthenticated={true}
accessToken={accessToken || undefined}
clientId={connector.clientId}
onSettingsChange={setIngestSettings}
/>
</div>
<div className="max-w-3xl mx-auto">
<UnifiedCloudPicker
provider={
connector.type as "google_drive" | "onedrive" | "sharepoint"
}
onFileSelected={handleFileSelected}
selectedFiles={selectedFiles}
isAuthenticated={true}
accessToken={accessToken || undefined}
clientId={connector.clientId}
onSettingsChange={setIngestSettings}
/>
</div>
<div className="max-w-3xl mx-auto mt-6">
<div className="flex justify-between gap-3 mb-4">
<Button
variant="ghost"
className=" border bg-transparent border-border rounded-lg text-secondary-foreground"
onClick={() => router.back()}
>
Back
</Button>
<Button
variant="secondary"
onClick={() => handleSync(connector)}
disabled={selectedFiles.length === 0 || isIngesting}
>
{isIngesting ? (
<>Ingesting {selectedFiles.length} Files...</>
) : (
<>Start ingest</>
)}
</Button>
</div>
</div>
{/* Success toast notification */}
<Toast
message="Ingested successfully!."
show={showSuccessToast}
onHide={() => setShowSuccessToast(false)}
duration={20000}
/>
</div>
);
<div className="max-w-3xl mx-auto mt-6">
<div className="flex justify-between gap-3 mb-4">
<Button
variant="ghost"
className=" border bg-transparent border-border rounded-lg text-secondary-foreground"
onClick={() => router.back()}
>
Back
</Button>
<Button
variant="secondary"
onClick={() => handleSync(connector)}
disabled={selectedFiles.length === 0 || isIngesting}
>
{isIngesting ? (
<>Ingesting {selectedFiles.length} Files...</>
) : (
<>Start ingest</>
)}
</Button>
</div>
</div>
</div>
);
}

View file

@ -197,7 +197,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
newTask.status === "completed"
) {
// Task just completed - show success toast
toast.success("Task completed successfully!", {
toast.success("Task completed successfully", {
description: `Task ${newTask.task_id} has finished processing.`,
action: {
label: "View",

View file

@ -2,7 +2,12 @@
from starlette.requests import Request
from config.settings import DISABLE_INGEST_WITH_LANGFLOW
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
clients,
INDEX_NAME,
INDEX_BODY,
)
from utils.logging_config import get_logger
logger = get_logger(__name__)
@ -12,19 +17,19 @@ class ConnectorRouter:
"""
Router that automatically chooses between LangflowConnectorService and ConnectorService
based on the DISABLE_INGEST_WITH_LANGFLOW configuration.
- If DISABLE_INGEST_WITH_LANGFLOW is False (default): uses LangflowConnectorService
- If DISABLE_INGEST_WITH_LANGFLOW is True: uses traditional ConnectorService
"""
def __init__(self, langflow_connector_service, openrag_connector_service):
self.langflow_connector_service = langflow_connector_service
self.openrag_connector_service = openrag_connector_service
logger.debug(
"ConnectorRouter initialized",
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW,
)
def get_active_service(self):
"""Get the currently active connector service based on configuration."""
if DISABLE_INGEST_WITH_LANGFLOW:
@ -33,28 +38,32 @@ class ConnectorRouter:
else:
logger.debug("Using Langflow connector service")
return self.langflow_connector_service
# Proxy all connector service methods to the active service
async def initialize(self):
"""Initialize the active connector service."""
# Initialize OpenSearch index if using traditional OpenRAG connector service
return await self.get_active_service().initialize()
@property
def connection_manager(self):
"""Get the connection manager from the active service."""
return self.get_active_service().connection_manager
async def get_connector(self, connection_id: str):
"""Get a connector instance from the active service."""
return await self.get_active_service().get_connector(connection_id)
async def sync_specific_files(self, connection_id: str, user_id: str, file_list: list, jwt_token: str = None):
async def sync_specific_files(
self, connection_id: str, user_id: str, file_list: list, jwt_token: str = None
):
"""Sync specific files using the active service."""
return await self.get_active_service().sync_specific_files(
connection_id, user_id, file_list, jwt_token
)
def __getattr__(self, name):
"""
Proxy any other method calls to the active service.
@ -64,4 +73,6 @@ class ConnectorRouter:
if hasattr(active_service, name):
return getattr(active_service, name)
else:
raise AttributeError(f"'{type(active_service).__name__}' object has no attribute '{name}'")
raise AttributeError(
f"'{type(active_service).__name__}' object has no attribute '{name}'"
)

View file

@ -4,6 +4,7 @@ from starlette.responses import JSONResponse
from utils.container_utils import transform_localhost_url
from utils.logging_config import get_logger
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
LANGFLOW_URL,
LANGFLOW_CHAT_FLOW_ID,
LANGFLOW_INGEST_FLOW_ID,
@ -450,7 +451,7 @@ async def onboarding(request, flows_service):
config_updated = True
# Update knowledge settings
if "embedding_model" in body:
if "embedding_model" in body and not DISABLE_INGEST_WITH_LANGFLOW:
if (
not isinstance(body["embedding_model"], str)
or not body["embedding_model"].strip()
@ -600,11 +601,16 @@ async def onboarding(request, flows_service):
# Import here to avoid circular imports
from main import init_index
logger.info("Initializing OpenSearch index after onboarding configuration")
logger.info(
"Initializing OpenSearch index after onboarding configuration"
)
await init_index()
logger.info("OpenSearch index initialization completed successfully")
except Exception as e:
logger.error("Failed to initialize OpenSearch index after onboarding", error=str(e))
logger.error(
"Failed to initialize OpenSearch index after onboarding",
error=str(e),
)
# Don't fail the entire onboarding process if index creation fails
# The application can still work, but document operations may fail

View file

@ -53,6 +53,7 @@ from auth_middleware import optional_auth, require_auth
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
EMBED_MODEL,
INDEX_BODY,
INDEX_NAME,
SESSION_SECRET,
clients,
@ -82,6 +83,7 @@ logger.info(
cuda_version=torch.version.cuda,
)
async def wait_for_opensearch():
"""Wait for OpenSearch to be ready with retries"""
max_retries = 30
@ -128,6 +130,34 @@ async def configure_alerting_security():
# Don't fail startup if alerting config fails
async def _ensure_opensearch_index(self):
"""Ensure OpenSearch index exists when using traditional connector service."""
try:
# Check if index already exists
if await clients.opensearch.indices.exists(index=INDEX_NAME):
logger.debug("OpenSearch index already exists", index_name=INDEX_NAME)
return
# Create the index with hard-coded INDEX_BODY (uses OpenAI embedding dimensions)
await clients.opensearch.indices.create(index=INDEX_NAME, body=INDEX_BODY)
logger.info(
"Created OpenSearch index for traditional connector service",
index_name=INDEX_NAME,
vector_dimensions=INDEX_BODY["mappings"]["properties"]["chunk_embedding"][
"dimension"
],
)
except Exception as e:
logger.error(
"Failed to initialize OpenSearch index for traditional connector service",
error=str(e),
index_name=INDEX_NAME,
)
# Don't raise the exception to avoid breaking the initialization
# The service can still function, document operations might fail later
async def init_index():
"""Initialize OpenSearch index and security roles"""
await wait_for_opensearch()
@ -141,10 +171,20 @@ async def init_index():
# Create documents index
if not await clients.opensearch.indices.exists(index=INDEX_NAME):
await clients.opensearch.indices.create(index=INDEX_NAME, body=dynamic_index_body)
logger.info("Created OpenSearch index", index_name=INDEX_NAME, embedding_model=embedding_model)
await clients.opensearch.indices.create(
index=INDEX_NAME, body=dynamic_index_body
)
logger.info(
"Created OpenSearch index",
index_name=INDEX_NAME,
embedding_model=embedding_model,
)
else:
logger.info("Index already exists, skipping creation", index_name=INDEX_NAME, embedding_model=embedding_model)
logger.info(
"Index already exists, skipping creation",
index_name=INDEX_NAME,
embedding_model=embedding_model,
)
# Create knowledge filters index
knowledge_filter_index_name = "knowledge_filters"
@ -402,6 +442,9 @@ async def startup_tasks(services):
# Index will be created after onboarding when we know the embedding model
await wait_for_opensearch()
if DISABLE_INGEST_WITH_LANGFLOW:
await _ensure_opensearch_index()
# Configure alerting security
await configure_alerting_security()
@ -1075,14 +1118,6 @@ async def create_app():
return app
async def startup():
"""Application startup tasks"""
await init_index()
# Get services from app state if needed for initialization
# services = app.state.services
# await services['connector_service'].initialize()
def cleanup():
"""Cleanup on application shutdown"""
# Cleanup process pools only (webhooks handled by Starlette shutdown)

View file

@ -1,5 +1,6 @@
import asyncio
from config.settings import (
DISABLE_INGEST_WITH_LANGFLOW,
NUDGES_FLOW_ID,
LANGFLOW_URL,
LANGFLOW_CHAT_FLOW_ID,
@ -73,17 +74,17 @@ class FlowsService:
# Scan all JSON files in the flows directory
try:
for filename in os.listdir(flows_dir):
if not filename.endswith('.json'):
if not filename.endswith(".json"):
continue
file_path = os.path.join(flows_dir, filename)
try:
with open(file_path, 'r') as f:
with open(file_path, "r") as f:
flow_data = json.load(f)
# Check if this file contains the flow we're looking for
if flow_data.get('id') == flow_id:
if flow_data.get("id") == flow_id:
# Cache the result
self._flow_file_cache[flow_id] = file_path
logger.info(f"Found flow {flow_id} in file: {filename}")
@ -99,6 +100,7 @@ class FlowsService:
logger.warning(f"Flow with ID {flow_id} not found in flows directory")
return None
async def reset_langflow_flow(self, flow_type: str):
"""Reset a Langflow flow by uploading the corresponding JSON file
@ -135,7 +137,9 @@ class FlowsService:
try:
with open(flow_path, "r") as f:
flow_data = json.load(f)
logger.info(f"Successfully loaded flow data for {flow_type} from {os.path.basename(flow_path)}")
logger.info(
f"Successfully loaded flow data for {flow_type} from {os.path.basename(flow_path)}"
)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in flow file {flow_path}: {e}")
except FileNotFoundError:
@ -161,43 +165,62 @@ class FlowsService:
# Check if configuration has been edited (onboarding completed)
if config.edited:
logger.info(f"Updating {flow_type} flow with current configuration settings")
logger.info(
f"Updating {flow_type} flow with current configuration settings"
)
provider = config.provider.model_provider.lower()
# Step 1: Assign model provider (replace components) if not OpenAI
if provider != "openai":
logger.info(f"Assigning {provider} components to {flow_type} flow")
logger.info(
f"Assigning {provider} components to {flow_type} flow"
)
provider_result = await self.assign_model_provider(provider)
if not provider_result.get("success"):
logger.warning(f"Failed to assign {provider} components: {provider_result.get('error', 'Unknown error')}")
logger.warning(
f"Failed to assign {provider} components: {provider_result.get('error', 'Unknown error')}"
)
# Continue anyway, maybe just value updates will work
# Step 2: Update model values for the specific flow being reset
single_flow_config = [{
"name": flow_type,
"flow_id": flow_id,
}]
single_flow_config = [
{
"name": flow_type,
"flow_id": flow_id,
}
]
logger.info(f"Updating {flow_type} flow model values")
update_result = await self.change_langflow_model_value(
provider=provider,
embedding_model=config.knowledge.embedding_model,
llm_model=config.agent.llm_model,
endpoint=config.provider.endpoint if config.provider.endpoint else None,
flow_configs=single_flow_config
endpoint=config.provider.endpoint
if config.provider.endpoint
else None,
flow_configs=single_flow_config,
)
if update_result.get("success"):
logger.info(f"Successfully updated {flow_type} flow with current configuration")
logger.info(
f"Successfully updated {flow_type} flow with current configuration"
)
else:
logger.warning(f"Failed to update {flow_type} flow with current configuration: {update_result.get('error', 'Unknown error')}")
logger.warning(
f"Failed to update {flow_type} flow with current configuration: {update_result.get('error', 'Unknown error')}"
)
else:
logger.info(f"Configuration not yet edited (onboarding not completed), skipping model updates for {flow_type} flow")
logger.info(
f"Configuration not yet edited (onboarding not completed), skipping model updates for {flow_type} flow"
)
except Exception as e:
logger.error(f"Error updating {flow_type} flow with current configuration", error=str(e))
logger.error(
f"Error updating {flow_type} flow with current configuration",
error=str(e),
)
# Don't fail the entire reset operation if configuration update fails
return {
@ -243,7 +266,9 @@ class FlowsService:
try:
# Load component templates based on provider
llm_template, embedding_template, llm_text_template = self._load_component_templates(provider)
llm_template, embedding_template, llm_text_template = (
self._load_component_templates(provider)
)
logger.info(f"Assigning {provider} components")
@ -358,7 +383,9 @@ class FlowsService:
logger.info(f"Loaded component templates for {provider}")
return llm_template, embedding_template, llm_text_template
async def _update_flow_components(self, config, llm_template, embedding_template, llm_text_template):
async def _update_flow_components(
self, config, llm_template, embedding_template, llm_text_template
):
"""Update components in a specific flow"""
flow_name = config["name"]
flow_id = config["flow_id"]
@ -383,20 +410,23 @@ class FlowsService:
components_updated = []
# Replace embedding component
embedding_node = self._find_node_by_id(flow_data, old_embedding_id)
if embedding_node:
# Preserve position
original_position = embedding_node.get("position", {})
if not DISABLE_INGEST_WITH_LANGFLOW:
embedding_node = self._find_node_by_id(flow_data, old_embedding_id)
if embedding_node:
# Preserve position
original_position = embedding_node.get("position", {})
# Replace with new template
new_embedding_node = embedding_template.copy()
new_embedding_node["position"] = original_position
# Replace with new template
new_embedding_node = embedding_template.copy()
new_embedding_node["position"] = original_position
# Replace in flow
self._replace_node_in_flow(flow_data, old_embedding_id, new_embedding_node)
components_updated.append(
f"embedding: {old_embedding_id} -> {new_embedding_id}"
)
# Replace in flow
self._replace_node_in_flow(
flow_data, old_embedding_id, new_embedding_node
)
components_updated.append(
f"embedding: {old_embedding_id} -> {new_embedding_id}"
)
# Replace LLM component (if exists in this flow)
if old_llm_id:
@ -425,27 +455,30 @@ class FlowsService:
new_llm_text_node["position"] = original_position
# Replace in flow
self._replace_node_in_flow(flow_data, old_llm_text_id, new_llm_text_node)
components_updated.append(f"llm: {old_llm_text_id} -> {new_llm_text_id}")
self._replace_node_in_flow(
flow_data, old_llm_text_id, new_llm_text_node
)
components_updated.append(
f"llm: {old_llm_text_id} -> {new_llm_text_id}"
)
# Update all edge references using regex replacement
flow_json_str = json.dumps(flow_data)
# Replace embedding ID references
flow_json_str = re.sub(
re.escape(old_embedding_id), new_embedding_id, flow_json_str
)
flow_json_str = re.sub(
re.escape(old_embedding_id.split("-")[0]),
new_embedding_id.split("-")[0],
flow_json_str,
)
if not DISABLE_INGEST_WITH_LANGFLOW:
flow_json_str = re.sub(
re.escape(old_embedding_id), new_embedding_id, flow_json_str
)
flow_json_str = re.sub(
re.escape(old_embedding_id.split("-")[0]),
new_embedding_id.split("-")[0],
flow_json_str,
)
# Replace LLM ID references (if applicable)
if old_llm_id:
flow_json_str = re.sub(
re.escape(old_llm_id), new_llm_id, flow_json_str
)
flow_json_str = re.sub(re.escape(old_llm_id), new_llm_id, flow_json_str)
if old_llm_text_id:
flow_json_str = re.sub(
re.escape(old_llm_text_id), new_llm_text_id, flow_json_str
@ -506,7 +539,14 @@ class FlowsService:
return None, None
async def _update_flow_field(self, flow_id: str, field_name: str, field_value: str, node_display_name: str = None, node_id: str = None):
async def _update_flow_field(
self,
flow_id: str,
field_name: str,
field_value: str,
node_display_name: str = None,
node_id: str = None,
):
"""
Generic helper function to update any field in any Langflow component.
@ -521,22 +561,26 @@ class FlowsService:
raise ValueError("flow_id is required")
# Get the current flow data from Langflow
response = await clients.langflow_request(
"GET", f"/api/v1/flows/{flow_id}"
)
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
if response.status_code != 200:
raise Exception(f"Failed to get flow: HTTP {response.status_code} - {response.text}")
raise Exception(
f"Failed to get flow: HTTP {response.status_code} - {response.text}"
)
flow_data = response.json()
# Find the target component by display name first, then by ID as fallback
target_node, target_node_index = None, None
if node_display_name:
target_node, target_node_index = self._find_node_in_flow(flow_data, display_name=node_display_name)
target_node, target_node_index = self._find_node_in_flow(
flow_data, display_name=node_display_name
)
if target_node is None and node_id:
target_node, target_node_index = self._find_node_in_flow(flow_data, node_id=node_id)
target_node, target_node_index = self._find_node_in_flow(
flow_data, node_id=node_id
)
if target_node is None:
identifier = node_display_name or node_id
@ -545,7 +589,9 @@ class FlowsService:
# Update the field value directly in the existing node
template = target_node.get("data", {}).get("node", {}).get("template", {})
if template.get(field_name):
flow_data["data"]["nodes"][target_node_index]["data"]["node"]["template"][field_name]["value"] = field_value
flow_data["data"]["nodes"][target_node_index]["data"]["node"]["template"][
field_name
]["value"] = field_value
else:
identifier = node_display_name or node_id
raise Exception(f"{field_name} field not found in {identifier} component")
@ -556,21 +602,31 @@ class FlowsService:
)
if patch_response.status_code != 200:
raise Exception(f"Failed to update flow: HTTP {patch_response.status_code} - {patch_response.text}")
raise Exception(
f"Failed to update flow: HTTP {patch_response.status_code} - {patch_response.text}"
)
async def update_chat_flow_model(self, model_name: str):
"""Helper function to update the model in the chat flow"""
if not LANGFLOW_CHAT_FLOW_ID:
raise ValueError("LANGFLOW_CHAT_FLOW_ID is not configured")
await self._update_flow_field(LANGFLOW_CHAT_FLOW_ID, "model_name", model_name,
node_display_name="Language Model")
await self._update_flow_field(
LANGFLOW_CHAT_FLOW_ID,
"model_name",
model_name,
node_display_name="Language Model",
)
async def update_chat_flow_system_prompt(self, system_prompt: str):
"""Helper function to update the system prompt in the chat flow"""
if not LANGFLOW_CHAT_FLOW_ID:
raise ValueError("LANGFLOW_CHAT_FLOW_ID is not configured")
await self._update_flow_field(LANGFLOW_CHAT_FLOW_ID, "system_prompt", system_prompt,
node_display_name="Agent")
await self._update_flow_field(
LANGFLOW_CHAT_FLOW_ID,
"system_prompt",
system_prompt,
node_display_name="Agent",
)
async def update_flow_docling_preset(self, preset: str, preset_config: dict):
"""Helper function to update docling preset in the ingest flow"""
@ -578,29 +634,46 @@ class FlowsService:
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
from config.settings import DOCLING_COMPONENT_ID
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "docling_serve_opts", preset_config,
node_id=DOCLING_COMPONENT_ID)
await self._update_flow_field(
LANGFLOW_INGEST_FLOW_ID,
"docling_serve_opts",
preset_config,
node_id=DOCLING_COMPONENT_ID,
)
async def update_ingest_flow_chunk_size(self, chunk_size: int):
"""Helper function to update chunk size in the ingest flow"""
if not LANGFLOW_INGEST_FLOW_ID:
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "chunk_size", chunk_size,
node_display_name="Split Text")
await self._update_flow_field(
LANGFLOW_INGEST_FLOW_ID,
"chunk_size",
chunk_size,
node_display_name="Split Text",
)
async def update_ingest_flow_chunk_overlap(self, chunk_overlap: int):
"""Helper function to update chunk overlap in the ingest flow"""
if not LANGFLOW_INGEST_FLOW_ID:
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "chunk_overlap", chunk_overlap,
node_display_name="Split Text")
await self._update_flow_field(
LANGFLOW_INGEST_FLOW_ID,
"chunk_overlap",
chunk_overlap,
node_display_name="Split Text",
)
async def update_ingest_flow_embedding_model(self, embedding_model: str):
"""Helper function to update embedding model in the ingest flow"""
if not LANGFLOW_INGEST_FLOW_ID:
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "model", embedding_model,
node_display_name="Embedding Model")
await self._update_flow_field(
LANGFLOW_INGEST_FLOW_ID,
"model",
embedding_model,
node_display_name="Embedding Model",
)
def _replace_node_in_flow(self, flow_data, old_id, new_node):
"""Replace a node in the flow data"""
@ -612,7 +685,12 @@ class FlowsService:
return False
async def change_langflow_model_value(
self, provider: str, embedding_model: str, llm_model: str, endpoint: str = None, flow_configs: list = None
self,
provider: str,
embedding_model: str,
llm_model: str,
endpoint: str = None,
flow_configs: list = None,
):
"""
Change dropdown values for provider-specific components across flows
@ -656,8 +734,8 @@ class FlowsService:
]
# Determine target component IDs based on provider
target_embedding_id, target_llm_id, target_llm_text_id = self._get_provider_component_ids(
provider
target_embedding_id, target_llm_id, target_llm_text_id = (
self._get_provider_component_ids(provider)
)
results = []
@ -713,12 +791,24 @@ class FlowsService:
def _get_provider_component_ids(self, provider: str):
"""Get the component IDs for a specific provider"""
if provider == "watsonx":
return WATSONX_EMBEDDING_COMPONENT_ID, WATSONX_LLM_COMPONENT_ID, WATSONX_LLM_TEXT_COMPONENT_ID
return (
WATSONX_EMBEDDING_COMPONENT_ID,
WATSONX_LLM_COMPONENT_ID,
WATSONX_LLM_TEXT_COMPONENT_ID,
)
elif provider == "ollama":
return OLLAMA_EMBEDDING_COMPONENT_ID, OLLAMA_LLM_COMPONENT_ID, OLLAMA_LLM_TEXT_COMPONENT_ID
return (
OLLAMA_EMBEDDING_COMPONENT_ID,
OLLAMA_LLM_COMPONENT_ID,
OLLAMA_LLM_TEXT_COMPONENT_ID,
)
elif provider == "openai":
# OpenAI components are the default ones
return OPENAI_EMBEDDING_COMPONENT_ID, OPENAI_LLM_COMPONENT_ID, OPENAI_LLM_TEXT_COMPONENT_ID
return (
OPENAI_EMBEDDING_COMPONENT_ID,
OPENAI_LLM_COMPONENT_ID,
OPENAI_LLM_TEXT_COMPONENT_ID,
)
else:
raise ValueError(f"Unsupported provider: {provider}")
@ -738,26 +828,25 @@ class FlowsService:
flow_id = config["flow_id"]
# Get flow data from Langflow API instead of file
response = await clients.langflow_request(
"GET", f"/api/v1/flows/{flow_id}"
)
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
if response.status_code != 200:
raise Exception(
f"Failed to get flow from Langflow: HTTP {response.status_code} - {response.text}"
)
flow_data = response.json()
updates_made = []
# Update embedding component
embedding_node = self._find_node_by_id(flow_data, target_embedding_id)
if embedding_node:
if self._update_component_fields(
embedding_node, provider, embedding_model, endpoint
):
updates_made.append(f"embedding model: {embedding_model}")
if not DISABLE_INGEST_WITH_LANGFLOW:
embedding_node = self._find_node_by_id(flow_data, target_embedding_id)
if embedding_node:
if self._update_component_fields(
embedding_node, provider, embedding_model, endpoint
):
updates_made.append(f"embedding model: {embedding_model}")
# Update LLM component (if exists in this flow)
if target_llm_id:

View file

@ -21,6 +21,27 @@ class ModelsService:
"jina-embeddings-v2-base-en",
]
OPENAI_TOOL_CALLING_MODELS = [
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-4o-mini",
"gpt-4o",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4-turbo",
"gpt-4-turbo-preview",
"gpt-4",
"gpt-3.5-turbo",
"o1",
"o3-mini",
"o3",
"o3-pro",
"o4-mini",
"o4-mini-high",
]
def __init__(self):
self.session_manager = None
@ -49,12 +70,12 @@ class ModelsService:
model_id = model.get("id", "")
# Language models (GPT models)
if any(prefix in model_id for prefix in ["gpt-4", "gpt-3.5"]):
if model_id in self.OPENAI_TOOL_CALLING_MODELS:
language_models.append(
{
"value": model_id,
"label": model_id,
"default": model_id == "gpt-4o-mini",
"default": model_id == "gpt-5",
}
)