Merge branch 'main' of https://github.com/langflow-ai/openrag into app-shell
This commit is contained in:
commit
93c2d2f1ad
20 changed files with 918 additions and 784 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}'"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
57
src/main.py
57
src/main.py
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue