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
|
- ./documents:/app/documents:Z
|
||||||
- ./keys:/app/keys:Z
|
- ./keys:/app/keys:Z
|
||||||
- ./flows:/app/flows:Z
|
- ./flows:/app/flows:Z
|
||||||
|
- ./config.yaml:/app/config.yaml:Z
|
||||||
|
|
||||||
openrag-frontend:
|
openrag-frontend:
|
||||||
image: phact/openrag-frontend:${OPENRAG_VERSION:-latest}
|
image: phact/openrag-frontend:${OPENRAG_VERSION:-latest}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ services:
|
||||||
- ./documents:/app/documents:Z
|
- ./documents:/app/documents:Z
|
||||||
- ./keys:/app/keys:Z
|
- ./keys:/app/keys:Z
|
||||||
- ./flows:/app/flows:z
|
- ./flows:/app/flows:z
|
||||||
|
- ./config.yaml:/app/config.yaml:Z
|
||||||
gpus: all
|
gpus: all
|
||||||
|
|
||||||
openrag-frontend:
|
openrag-frontend:
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export function LabelWrapper({
|
||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={id}
|
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}
|
{label}
|
||||||
{required && <span className="text-red-500">*</span>}
|
{required && <span className="text-red-500">*</span>}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,24 @@ export default function IBMLogo(props: React.SVGProps<SVGSVGElement>) {
|
||||||
fill="none"
|
fill="none"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<title>IBM Logo</title>
|
<title>IBM watsonx.ai Logo</title>
|
||||||
<path
|
<g clip-path="url(#clip0_2620_2081)">
|
||||||
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"
|
<path
|
||||||
fill="currentColor"
|
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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const Card = React.forwardRef<
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState, useRef, Suspense } from "react"
|
import { useEffect, useState, Suspense } from "react"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
@ -14,17 +14,20 @@ function AuthCallbackContent() {
|
||||||
const [status, setStatus] = useState<"processing" | "success" | "error">("processing")
|
const [status, setStatus] = useState<"processing" | "success" | "error">("processing")
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [purpose, setPurpose] = useState<string>("app_auth")
|
const [purpose, setPurpose] = useState<string>("app_auth")
|
||||||
const hasProcessed = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prevent double execution in React Strict Mode
|
const code = searchParams.get('code')
|
||||||
if (hasProcessed.current) return
|
const callbackKey = `callback_processed_${code}`
|
||||||
hasProcessed.current = true
|
|
||||||
|
// Prevent double execution across component remounts
|
||||||
|
if (sessionStorage.getItem(callbackKey)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(callbackKey, 'true')
|
||||||
|
|
||||||
const handleCallback = async () => {
|
const handleCallback = async () => {
|
||||||
try {
|
try {
|
||||||
// Get parameters from URL
|
// Get parameters from URL
|
||||||
const code = searchParams.get('code')
|
|
||||||
const state = searchParams.get('state')
|
const state = searchParams.get('state')
|
||||||
const errorParam = searchParams.get('error')
|
const errorParam = searchParams.get('error')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,6 @@ function ChatPage() {
|
||||||
>(new Set());
|
>(new Set());
|
||||||
// previousResponseIds now comes from useChat context
|
// previousResponseIds now comes from useChat context
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
|
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
|
||||||
const [availableFilters, setAvailableFilters] = useState<
|
const [availableFilters, setAvailableFilters] = useState<
|
||||||
KnowledgeFilterData[]
|
KnowledgeFilterData[]
|
||||||
|
|
@ -132,7 +131,6 @@ function ChatPage() {
|
||||||
const [dropdownDismissed, setDropdownDismissed] = useState(false);
|
const [dropdownDismissed, setDropdownDismissed] = useState(false);
|
||||||
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||||
const [isForkingInProgress, setIsForkingInProgress] = useState(false);
|
const [isForkingInProgress, setIsForkingInProgress] = useState(false);
|
||||||
const dragCounterRef = useRef(0);
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(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 = () => {
|
const handleFilePickerClick = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
@ -1958,31 +1919,12 @@ function ChatPage() {
|
||||||
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-hidden">
|
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-hidden">
|
||||||
{/* Messages Area */}
|
{/* Messages Area */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 overflow-y-auto overflow-x-hidden scrollbar-hide space-y-6 min-h-0 transition-all relative ${
|
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}
|
|
||||||
>
|
>
|
||||||
{messages.length === 0 && !streamingMessage ? (
|
{messages.length === 0 && !streamingMessage ? (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{isDragOver ? (
|
{isUploading ? (
|
||||||
<>
|
|
||||||
<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 ? (
|
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-12 w-12 mx-auto mb-4 animate-spin" />
|
<Loader2 className="h-12 w-12 mx-auto mb-4 animate-spin" />
|
||||||
<p>Processing your document...</p>
|
<p>Processing your document...</p>
|
||||||
|
|
@ -1999,8 +1941,8 @@ function ChatPage() {
|
||||||
<div key={index} className="space-y-6 group">
|
<div key={index} className="space-y-6 group">
|
||||||
{message.role === "user" && (
|
{message.role === "user" && (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Avatar className="w-8 h-8 flex-shrink-0">
|
<Avatar className="w-8 h-8 flex-shrink-0 select-none">
|
||||||
<AvatarImage src={user?.picture} alt={user?.name} />
|
<AvatarImage draggable={false} src={user?.picture} alt={user?.name} />
|
||||||
<AvatarFallback className="text-sm bg-primary/20 text-primary">
|
<AvatarFallback className="text-sm bg-primary/20 text-primary">
|
||||||
{user?.name ? (
|
{user?.name ? (
|
||||||
user.name.charAt(0).toUpperCase()
|
user.name.charAt(0).toUpperCase()
|
||||||
|
|
@ -2019,7 +1961,7 @@ function ChatPage() {
|
||||||
|
|
||||||
{message.role === "assistant" && (
|
{message.role === "assistant" && (
|
||||||
<div className="flex gap-3">
|
<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" />
|
<Bot className="h-4 w-4 text-accent-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|
@ -2083,18 +2025,6 @@ function ChatPage() {
|
||||||
<div ref={messagesEndRef} />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2344,12 +2274,14 @@ function ChatPage() {
|
||||||
<button
|
<button
|
||||||
key={filter.id}
|
key={filter.id}
|
||||||
onClick={() => handleFilterSelect(filter)}
|
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" : ""
|
index === selectedFilterIndex ? "bg-muted/50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div>
|
<div className="overflow-hidden">
|
||||||
<div className="font-medium">{filter.name}</div>
|
<div className="font-medium truncate">
|
||||||
|
{filter.name}
|
||||||
|
</div>
|
||||||
{filter.description && (
|
{filter.description && (
|
||||||
<div className="text-xs text-muted-foreground truncate">
|
<div className="text-xs text-muted-foreground truncate">
|
||||||
{filter.description}
|
{filter.description}
|
||||||
|
|
@ -2357,7 +2289,7 @@ function ChatPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{selectedFilter?.id === filter.id && (
|
{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>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -69,18 +69,12 @@ function LoginPageContent() {
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center justify-center gap-4 z-10">
|
<div className="flex flex-col items-center justify-center gap-4 z-10">
|
||||||
<Logo className="fill-primary" width={32} height={28} />
|
<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>
|
<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">
|
<Button onClick={login} className="w-80 gap-1.5" size="lg">
|
||||||
<GoogleLogo className="h-4 w-4" />
|
<GoogleLogo className="h-4 w-4" />
|
||||||
Continue with Google
|
Continue with Google
|
||||||
</Button>
|
</Button></div>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ export function IBMOnboarding({
|
||||||
<ModelSelector
|
<ModelSelector
|
||||||
options={options}
|
options={options}
|
||||||
value={endpoint}
|
value={endpoint}
|
||||||
|
custom
|
||||||
onValueChange={setEndpoint}
|
onValueChange={setEndpoint}
|
||||||
searchPlaceholder="Search endpoint..."
|
searchPlaceholder="Search endpoint..."
|
||||||
noOptionsPlaceholder="No endpoints available"
|
noOptionsPlaceholder="No endpoints available"
|
||||||
|
|
@ -118,8 +119,17 @@ export function IBMOnboarding({
|
||||||
/>
|
/>
|
||||||
</LabelWrapper>
|
</LabelWrapper>
|
||||||
<LabelInput
|
<LabelInput
|
||||||
label="IBM API key"
|
label="watsonx Project ID"
|
||||||
helperText="The API key for your watsonx.ai account."
|
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"
|
id="api-key"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
|
|
@ -127,15 +137,6 @@ export function IBMOnboarding({
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
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 && (
|
{isLoadingModels && (
|
||||||
<p className="text-mmd text-muted-foreground">
|
<p className="text-mmd text-muted-foreground">
|
||||||
Validating configuration...
|
Validating configuration...
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,158 @@
|
||||||
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
|
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function ModelSelector({
|
export function ModelSelector({
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
icon,
|
icon,
|
||||||
placeholder = "Select model...",
|
placeholder = "Select model...",
|
||||||
searchPlaceholder = "Search model...",
|
searchPlaceholder = "Search model...",
|
||||||
noOptionsPlaceholder = "No models available",
|
noOptionsPlaceholder = "No models available",
|
||||||
|
custom = false,
|
||||||
}: {
|
}: {
|
||||||
options: {
|
options: {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
}[];
|
}[];
|
||||||
value: string;
|
value: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
noOptionsPlaceholder?: string;
|
noOptionsPlaceholder?: string;
|
||||||
onValueChange: (value: string) => void;
|
custom?: boolean;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
useEffect(() => {
|
const [searchValue, setSearchValue] = useState("");
|
||||||
if (value && !options.find((option) => option.value === value)) {
|
|
||||||
onValueChange("");
|
useEffect(() => {
|
||||||
}
|
if (value && (!options.find((option) => option.value === value) && !custom)) {
|
||||||
}, [options, value, onValueChange]);
|
onValueChange("");
|
||||||
return (
|
}
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
}, [options, value, custom, onValueChange]);
|
||||||
<PopoverTrigger asChild>
|
return (
|
||||||
{/** biome-ignore lint/a11y/useSemanticElements: has to be a Button */}
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<Button
|
<PopoverTrigger asChild>
|
||||||
variant="outline"
|
{/** biome-ignore lint/a11y/useSemanticElements: has to be a Button */}
|
||||||
role="combobox"
|
<Button
|
||||||
disabled={options.length === 0}
|
variant="outline"
|
||||||
aria-expanded={open}
|
role="combobox"
|
||||||
className="w-full gap-2 justify-between font-normal text-sm"
|
disabled={options.length === 0}
|
||||||
>
|
aria-expanded={open}
|
||||||
{value ? (
|
className="w-full gap-2 justify-between font-normal text-sm"
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
{icon && <div className="w-4 h-4">{icon}</div>}
|
{value ? (
|
||||||
{options.find((framework) => framework.value === value)?.label}
|
<div className="flex items-center gap-2">
|
||||||
{options.find((framework) => framework.value === value)
|
{icon && <div className="w-4 h-4">{icon}</div>}
|
||||||
?.default && (
|
{options.find((framework) => framework.value === value)?.label ||
|
||||||
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
|
value}
|
||||||
Default
|
{/* {options.find((framework) => framework.value === value)
|
||||||
</span>
|
?.default && (
|
||||||
)}
|
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
|
||||||
</div>
|
Default
|
||||||
) : options.length === 0 ? (
|
</span>
|
||||||
noOptionsPlaceholder
|
)} */}
|
||||||
) : (
|
{custom &&
|
||||||
placeholder
|
value &&
|
||||||
)}
|
!options.find((framework) => framework.value === value) && (
|
||||||
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<Badge variant="outline" className="text-xs">
|
||||||
</Button>
|
CUSTOM
|
||||||
</PopoverTrigger>
|
</Badge>
|
||||||
<PopoverContent align="start" className="w-[400px] p-0">
|
)}
|
||||||
<Command>
|
</div>
|
||||||
<CommandInput placeholder={searchPlaceholder} />
|
) : options.length === 0 ? (
|
||||||
<CommandList>
|
noOptionsPlaceholder
|
||||||
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
|
) : (
|
||||||
<CommandGroup>
|
placeholder
|
||||||
{options.map((option) => (
|
)}
|
||||||
<CommandItem
|
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
key={option.value}
|
</Button>
|
||||||
value={option.value}
|
</PopoverTrigger>
|
||||||
onSelect={(currentValue) => {
|
<PopoverContent align="start" className=" p-0 w-[var(--radix-popover-trigger-width)]">
|
||||||
if (currentValue !== value) {
|
<Command>
|
||||||
onValueChange(currentValue);
|
<CommandInput
|
||||||
}
|
placeholder={searchPlaceholder}
|
||||||
setOpen(false);
|
value={searchValue}
|
||||||
}}
|
onValueChange={setSearchValue}
|
||||||
>
|
/>
|
||||||
<CheckIcon
|
<CommandList>
|
||||||
className={cn(
|
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
|
||||||
"mr-2 h-4 w-4",
|
<CommandGroup>
|
||||||
value === option.value ? "opacity-100" : "opacity-0",
|
{options.map((option) => (
|
||||||
)}
|
<CommandItem
|
||||||
/>
|
key={option.value}
|
||||||
<div className="flex items-center gap-2">
|
value={option.value}
|
||||||
{option.label}
|
onSelect={(currentValue) => {
|
||||||
{option.default && (
|
if (currentValue !== value) {
|
||||||
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
|
onValueChange(currentValue);
|
||||||
Default
|
}
|
||||||
</span>
|
setOpen(false);
|
||||||
)}
|
}}
|
||||||
</div>
|
>
|
||||||
</CommandItem>
|
<CheckIcon
|
||||||
))}
|
className={cn(
|
||||||
</CommandGroup>
|
"mr-2 h-4 w-4",
|
||||||
</CommandList>
|
value === option.value ? "opacity-100" : "opacity-0",
|
||||||
</Command>
|
)}
|
||||||
</PopoverContent>
|
/>
|
||||||
</Popover>
|
<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 { LabelInput } from "@/components/label-input";
|
||||||
import { LabelWrapper } from "@/components/label-wrapper";
|
import { LabelWrapper } from "@/components/label-wrapper";
|
||||||
import OpenAILogo from "@/components/logo/openai-logo";
|
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 { useDebouncedValue } from "@/lib/debounce";
|
||||||
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
||||||
import { useGetOpenAIModelsQuery } from "../../api/queries/useGetModelsQuery";
|
import { useGetOpenAIModelsQuery } from "../../api/queries/useGetModelsQuery";
|
||||||
|
|
@ -11,121 +11,114 @@ import { useUpdateSettings } from "../hooks/useUpdateSettings";
|
||||||
import { AdvancedOnboarding } from "./advanced";
|
import { AdvancedOnboarding } from "./advanced";
|
||||||
|
|
||||||
export function OpenAIOnboarding({
|
export function OpenAIOnboarding({
|
||||||
setSettings,
|
setSettings,
|
||||||
sampleDataset,
|
sampleDataset,
|
||||||
setSampleDataset,
|
setSampleDataset,
|
||||||
}: {
|
}: {
|
||||||
setSettings: (settings: OnboardingVariables) => void;
|
setSettings: (settings: OnboardingVariables) => void;
|
||||||
sampleDataset: boolean;
|
sampleDataset: boolean;
|
||||||
setSampleDataset: (dataset: boolean) => void;
|
setSampleDataset: (dataset: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [apiKey, setApiKey] = useState("");
|
const [apiKey, setApiKey] = useState("");
|
||||||
const [getFromEnv, setGetFromEnv] = useState(true);
|
const [getFromEnv, setGetFromEnv] = useState(true);
|
||||||
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
||||||
|
|
||||||
// Fetch models from API when API key is provided
|
// Fetch models from API when API key is provided
|
||||||
const {
|
const {
|
||||||
data: modelsData,
|
data: modelsData,
|
||||||
isLoading: isLoadingModels,
|
isLoading: isLoadingModels,
|
||||||
error: modelsError,
|
error: modelsError,
|
||||||
} = useGetOpenAIModelsQuery(
|
} = useGetOpenAIModelsQuery(
|
||||||
getFromEnv
|
getFromEnv
|
||||||
? { apiKey: "" }
|
? { apiKey: "" }
|
||||||
: debouncedApiKey
|
: debouncedApiKey
|
||||||
? { apiKey: debouncedApiKey }
|
? { apiKey: debouncedApiKey }
|
||||||
: undefined,
|
: undefined,
|
||||||
{ enabled: debouncedApiKey !== "" || getFromEnv },
|
{ enabled: debouncedApiKey !== "" || getFromEnv },
|
||||||
);
|
);
|
||||||
// Use custom hook for model selection logic
|
// Use custom hook for model selection logic
|
||||||
const {
|
const {
|
||||||
languageModel,
|
languageModel,
|
||||||
embeddingModel,
|
embeddingModel,
|
||||||
setLanguageModel,
|
setLanguageModel,
|
||||||
setEmbeddingModel,
|
setEmbeddingModel,
|
||||||
languageModels,
|
languageModels,
|
||||||
embeddingModels,
|
embeddingModels,
|
||||||
} = useModelSelection(modelsData);
|
} = useModelSelection(modelsData);
|
||||||
const handleSampleDatasetChange = (dataset: boolean) => {
|
const handleSampleDatasetChange = (dataset: boolean) => {
|
||||||
setSampleDataset(dataset);
|
setSampleDataset(dataset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGetFromEnvChange = (fromEnv: boolean) => {
|
const handleGetFromEnvChange = (fromEnv: boolean) => {
|
||||||
setGetFromEnv(fromEnv);
|
setGetFromEnv(fromEnv);
|
||||||
if (fromEnv) {
|
if (fromEnv) {
|
||||||
setApiKey("");
|
setApiKey("");
|
||||||
}
|
}
|
||||||
setLanguageModel("");
|
setLanguageModel("");
|
||||||
setEmbeddingModel("");
|
setEmbeddingModel("");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update settings when values change
|
// Update settings when values change
|
||||||
useUpdateSettings(
|
useUpdateSettings(
|
||||||
"openai",
|
"openai",
|
||||||
{
|
{
|
||||||
apiKey,
|
apiKey,
|
||||||
languageModel,
|
languageModel,
|
||||||
embeddingModel,
|
embeddingModel,
|
||||||
},
|
},
|
||||||
setSettings,
|
setSettings,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<LabelWrapper
|
<LabelWrapper
|
||||||
label="Use environment OpenAI API key"
|
label="Use environment OpenAI API key"
|
||||||
id="get-api-key"
|
id="get-api-key"
|
||||||
helperText={
|
description="Reuse the key from your environment config. Turn off to enter a different key."
|
||||||
<>
|
flex
|
||||||
Reuse the key from your environment config.
|
>
|
||||||
<br />
|
<Switch
|
||||||
Uncheck to enter a different key.
|
checked={getFromEnv}
|
||||||
</>
|
onCheckedChange={handleGetFromEnvChange}
|
||||||
}
|
/>
|
||||||
flex
|
</LabelWrapper>
|
||||||
start
|
{!getFromEnv && (
|
||||||
>
|
<div className="space-y-1">
|
||||||
<Checkbox
|
<LabelInput
|
||||||
checked={getFromEnv}
|
label="OpenAI API key"
|
||||||
onCheckedChange={handleGetFromEnvChange}
|
helperText="The API key for your OpenAI account."
|
||||||
/>
|
className={modelsError ? "!border-destructive" : ""}
|
||||||
</LabelWrapper>
|
id="api-key"
|
||||||
{!getFromEnv && (
|
type="password"
|
||||||
<div className="space-y-1">
|
required
|
||||||
<LabelInput
|
placeholder="sk-..."
|
||||||
label="OpenAI API key"
|
value={apiKey}
|
||||||
helperText="The API key for your OpenAI account."
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
className={modelsError ? "!border-destructive" : ""}
|
/>
|
||||||
id="api-key"
|
{isLoadingModels && (
|
||||||
type="password"
|
<p className="text-mmd text-muted-foreground">
|
||||||
required
|
Validating API key...
|
||||||
placeholder="sk-..."
|
</p>
|
||||||
value={apiKey}
|
)}
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
{modelsError && (
|
||||||
/>
|
<p className="text-mmd text-destructive">
|
||||||
{isLoadingModels && (
|
Invalid OpenAI API key. Verify or replace the key.
|
||||||
<p className="text-mmd text-muted-foreground">
|
</p>
|
||||||
Validating API key...
|
)}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
{modelsError && (
|
</div>
|
||||||
<p className="text-mmd text-destructive">
|
<AdvancedOnboarding
|
||||||
Invalid OpenAI API key. Verify or replace the key.
|
icon={<OpenAILogo className="w-4 h-4" />}
|
||||||
</p>
|
languageModels={languageModels}
|
||||||
)}
|
embeddingModels={embeddingModels}
|
||||||
</div>
|
languageModel={languageModel}
|
||||||
)}
|
embeddingModel={embeddingModel}
|
||||||
</div>
|
sampleDataset={sampleDataset}
|
||||||
<AdvancedOnboarding
|
setLanguageModel={setLanguageModel}
|
||||||
icon={<OpenAILogo className="w-4 h-4" />}
|
setSampleDataset={handleSampleDatasetChange}
|
||||||
languageModels={languageModels}
|
setEmbeddingModel={setEmbeddingModel}
|
||||||
embeddingModels={embeddingModels}
|
/>
|
||||||
languageModel={languageModel}
|
</>
|
||||||
embeddingModel={embeddingModel}
|
);
|
||||||
sampleDataset={sampleDataset}
|
|
||||||
setLanguageModel={setLanguageModel}
|
|
||||||
setSampleDataset={handleSampleDatasetChange}
|
|
||||||
setEmbeddingModel={setEmbeddingModel}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ function OnboardingPage() {
|
||||||
// Mutations
|
// Mutations
|
||||||
const onboardingMutation = useOnboardingMutation({
|
const onboardingMutation = useOnboardingMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success("Onboarding completed successfully!");
|
|
||||||
console.log("Onboarding completed successfully", data);
|
console.log("Onboarding completed successfully", data);
|
||||||
router.push(redirect);
|
router.push(redirect);
|
||||||
},
|
},
|
||||||
|
|
@ -137,7 +136,7 @@ function OnboardingPage() {
|
||||||
Connect a model provider
|
Connect a model provider
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<Card className="w-full max-w-[580px]">
|
<Card className="w-full max-w-[600px]">
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue={modelProvider}
|
defaultValue={modelProvider}
|
||||||
onValueChange={handleSetModelProvider}
|
onValueChange={handleSetModelProvider}
|
||||||
|
|
@ -150,7 +149,7 @@ function OnboardingPage() {
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="watsonx">
|
<TabsTrigger value="watsonx">
|
||||||
<IBMLogo className="w-4 h-4" />
|
<IBMLogo className="w-4 h-4" />
|
||||||
IBM
|
IBM watsonx.ai
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="ollama">
|
<TabsTrigger value="ollama">
|
||||||
<OllamaLogo className="w-4 h-4" />
|
<OllamaLogo className="w-4 h-4" />
|
||||||
|
|
@ -192,7 +191,7 @@ function OnboardingPage() {
|
||||||
disabled={!isComplete}
|
disabled={!isComplete}
|
||||||
loading={onboardingMutation.isPending}
|
loading={onboardingMutation.isPending}
|
||||||
>
|
>
|
||||||
Complete
|
<span className="select-none">Complete</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
|
||||||
|
|
@ -1,386 +1,378 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { AlertCircle, ArrowLeft } from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { useEffect, useState } from "react";
|
||||||
import { ArrowLeft, AlertCircle } from "lucide-react";
|
import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker";
|
||||||
import { UnifiedCloudPicker, CloudFile } from "@/components/cloud-picker";
|
|
||||||
import type { IngestSettings } from "@/components/cloud-picker/types";
|
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 { Toast } from "@/components/ui/toast";
|
||||||
|
import { useTask } from "@/contexts/task-context";
|
||||||
|
|
||||||
// CloudFile interface is now imported from the unified cloud picker
|
// CloudFile interface is now imported from the unified cloud picker
|
||||||
|
|
||||||
interface CloudConnector {
|
interface CloudConnector {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
status: "not_connected" | "connecting" | "connected" | "error";
|
status: "not_connected" | "connecting" | "connected" | "error";
|
||||||
type: string;
|
type: string;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
hasAccessToken: boolean;
|
hasAccessToken: boolean;
|
||||||
accessTokenError?: string;
|
accessTokenError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UploadProviderPage() {
|
export default function UploadProviderPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const provider = params.provider as string;
|
const provider = params.provider as string;
|
||||||
const { addTask, tasks } = useTask();
|
const { addTask, tasks } = useTask();
|
||||||
|
|
||||||
const [connector, setConnector] = useState<CloudConnector | null>(null);
|
const [connector, setConnector] = useState<CloudConnector | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
|
const [selectedFiles, setSelectedFiles] = useState<CloudFile[]>([]);
|
||||||
const [isIngesting, setIsIngesting] = useState<boolean>(false);
|
const [isIngesting, setIsIngesting] = useState<boolean>(false);
|
||||||
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(
|
const [currentSyncTaskId, setCurrentSyncTaskId] = useState<string | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
const [showSuccessToast, setShowSuccessToast] = useState(false);
|
const [ingestSettings, setIngestSettings] = useState<IngestSettings>({
|
||||||
const [ingestSettings, setIngestSettings] = useState<IngestSettings>({
|
chunkSize: 1000,
|
||||||
chunkSize: 1000,
|
chunkOverlap: 200,
|
||||||
chunkOverlap: 200,
|
ocr: false,
|
||||||
ocr: false,
|
pictureDescriptions: false,
|
||||||
pictureDescriptions: false,
|
embeddingModel: "text-embedding-3-small",
|
||||||
embeddingModel: "text-embedding-3-small",
|
});
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConnectorInfo = async () => {
|
const fetchConnectorInfo = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch available connectors to validate the provider
|
// Fetch available connectors to validate the provider
|
||||||
const connectorsResponse = await fetch("/api/connectors");
|
const connectorsResponse = await fetch("/api/connectors");
|
||||||
if (!connectorsResponse.ok) {
|
if (!connectorsResponse.ok) {
|
||||||
throw new Error("Failed to load connectors");
|
throw new Error("Failed to load connectors");
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectorsResult = await connectorsResponse.json();
|
const connectorsResult = await connectorsResponse.json();
|
||||||
const providerInfo = connectorsResult.connectors[provider];
|
const providerInfo = connectorsResult.connectors[provider];
|
||||||
|
|
||||||
if (!providerInfo || !providerInfo.available) {
|
if (!providerInfo || !providerInfo.available) {
|
||||||
setError(
|
setError(
|
||||||
`Cloud provider "${provider}" is not available or configured.`
|
`Cloud provider "${provider}" is not available or configured.`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connector status
|
// Check connector status
|
||||||
const statusResponse = await fetch(
|
const statusResponse = await fetch(
|
||||||
`/api/connectors/${provider}/status`
|
`/api/connectors/${provider}/status`,
|
||||||
);
|
);
|
||||||
if (!statusResponse.ok) {
|
if (!statusResponse.ok) {
|
||||||
throw new Error(`Failed to check ${provider} status`);
|
throw new Error(`Failed to check ${provider} status`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusData = await statusResponse.json();
|
const statusData = await statusResponse.json();
|
||||||
const connections = statusData.connections || [];
|
const connections = statusData.connections || [];
|
||||||
const activeConnection = connections.find(
|
const activeConnection = connections.find(
|
||||||
(conn: { is_active: boolean; connection_id: string }) =>
|
(conn: { is_active: boolean; connection_id: string }) =>
|
||||||
conn.is_active
|
conn.is_active,
|
||||||
);
|
);
|
||||||
const isConnected = activeConnection !== undefined;
|
const isConnected = activeConnection !== undefined;
|
||||||
|
|
||||||
let hasAccessToken = false;
|
let hasAccessToken = false;
|
||||||
let accessTokenError: string | undefined = undefined;
|
let accessTokenError: string | undefined;
|
||||||
|
|
||||||
// Try to get access token for connected connectors
|
// Try to get access token for connected connectors
|
||||||
if (isConnected && activeConnection) {
|
if (isConnected && activeConnection) {
|
||||||
try {
|
try {
|
||||||
const tokenResponse = await fetch(
|
const tokenResponse = await fetch(
|
||||||
`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`
|
`/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`,
|
||||||
);
|
);
|
||||||
if (tokenResponse.ok) {
|
if (tokenResponse.ok) {
|
||||||
const tokenData = await tokenResponse.json();
|
const tokenData = await tokenResponse.json();
|
||||||
if (tokenData.access_token) {
|
if (tokenData.access_token) {
|
||||||
hasAccessToken = true;
|
hasAccessToken = true;
|
||||||
setAccessToken(tokenData.access_token);
|
setAccessToken(tokenData.access_token);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const errorData = await tokenResponse
|
const errorData = await tokenResponse
|
||||||
.json()
|
.json()
|
||||||
.catch(() => ({ error: "Token unavailable" }));
|
.catch(() => ({ error: "Token unavailable" }));
|
||||||
accessTokenError = errorData.error || "Access token unavailable";
|
accessTokenError = errorData.error || "Access token unavailable";
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
accessTokenError = "Failed to fetch access token";
|
accessTokenError = "Failed to fetch access token";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setConnector({
|
setConnector({
|
||||||
id: provider,
|
id: provider,
|
||||||
name: providerInfo.name,
|
name: providerInfo.name,
|
||||||
description: providerInfo.description,
|
description: providerInfo.description,
|
||||||
status: isConnected ? "connected" : "not_connected",
|
status: isConnected ? "connected" : "not_connected",
|
||||||
type: provider,
|
type: provider,
|
||||||
connectionId: activeConnection?.connection_id,
|
connectionId: activeConnection?.connection_id,
|
||||||
clientId: activeConnection?.client_id,
|
clientId: activeConnection?.client_id,
|
||||||
hasAccessToken,
|
hasAccessToken,
|
||||||
accessTokenError,
|
accessTokenError,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load connector info:", error);
|
console.error("Failed to load connector info:", error);
|
||||||
setError(
|
setError(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: "Failed to load connector information"
|
: "Failed to load connector information",
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (provider) {
|
if (provider) {
|
||||||
fetchConnectorInfo();
|
fetchConnectorInfo();
|
||||||
}
|
}
|
||||||
}, [provider]);
|
}, [provider]);
|
||||||
|
|
||||||
// Watch for sync task completion and redirect
|
// Watch for sync task completion and redirect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentSyncTaskId) return;
|
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") {
|
if (currentTask && currentTask.status === "completed") {
|
||||||
// Task completed successfully, show toast and redirect
|
// Task completed successfully, show toast and redirect
|
||||||
setIsIngesting(false);
|
setIsIngesting(false);
|
||||||
setShowSuccessToast(true);
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
router.push("/knowledge");
|
||||||
router.push("/knowledge");
|
}, 2000); // 2 second delay to let user see toast
|
||||||
}, 2000); // 2 second delay to let user see toast
|
} else if (currentTask && currentTask.status === "failed") {
|
||||||
} else if (currentTask && currentTask.status === "failed") {
|
// Task failed, clear the tracking but don't redirect
|
||||||
// Task failed, clear the tracking but don't redirect
|
setIsIngesting(false);
|
||||||
setIsIngesting(false);
|
setCurrentSyncTaskId(null);
|
||||||
setCurrentSyncTaskId(null);
|
}
|
||||||
}
|
}, [tasks, currentSyncTaskId, router]);
|
||||||
}, [tasks, currentSyncTaskId, router]);
|
|
||||||
|
|
||||||
const handleFileSelected = (files: CloudFile[]) => {
|
const handleFileSelected = (files: CloudFile[]) => {
|
||||||
setSelectedFiles(files);
|
setSelectedFiles(files);
|
||||||
console.log(`Selected ${files.length} files from ${provider}:`, files);
|
console.log(`Selected ${files.length} files from ${provider}:`, files);
|
||||||
// You can add additional handling here like triggering sync, etc.
|
// You can add additional handling here like triggering sync, etc.
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSync = async (connector: CloudConnector) => {
|
const handleSync = async (connector: CloudConnector) => {
|
||||||
if (!connector.connectionId || selectedFiles.length === 0) return;
|
if (!connector.connectionId || selectedFiles.length === 0) return;
|
||||||
|
|
||||||
setIsIngesting(true);
|
setIsIngesting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const syncBody: {
|
const syncBody: {
|
||||||
connection_id: string;
|
connection_id: string;
|
||||||
max_files?: number;
|
max_files?: number;
|
||||||
selected_files?: string[];
|
selected_files?: string[];
|
||||||
settings?: IngestSettings;
|
settings?: IngestSettings;
|
||||||
} = {
|
} = {
|
||||||
connection_id: connector.connectionId,
|
connection_id: connector.connectionId,
|
||||||
selected_files: selectedFiles.map(file => file.id),
|
selected_files: selectedFiles.map((file) => file.id),
|
||||||
settings: ingestSettings,
|
settings: ingestSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
|
const response = await fetch(`/api/connectors/${connector.type}/sync`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(syncBody),
|
body: JSON.stringify(syncBody),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.status === 201) {
|
if (response.status === 201) {
|
||||||
const taskIds = result.task_ids;
|
const taskIds = result.task_ids;
|
||||||
if (taskIds && taskIds.length > 0) {
|
if (taskIds && taskIds.length > 0) {
|
||||||
const taskId = taskIds[0]; // Use the first task ID
|
const taskId = taskIds[0]; // Use the first task ID
|
||||||
addTask(taskId);
|
addTask(taskId);
|
||||||
setCurrentSyncTaskId(taskId);
|
setCurrentSyncTaskId(taskId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Sync failed:", result.error);
|
console.error("Sync failed:", result.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Sync error:", error);
|
console.error("Sync error:", error);
|
||||||
setIsIngesting(false);
|
setIsIngesting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProviderDisplayName = () => {
|
const getProviderDisplayName = () => {
|
||||||
const nameMap: { [key: string]: string } = {
|
const nameMap: { [key: string]: string } = {
|
||||||
google_drive: "Google Drive",
|
google_drive: "Google Drive",
|
||||||
onedrive: "OneDrive",
|
onedrive: "OneDrive",
|
||||||
sharepoint: "SharePoint",
|
sharepoint: "SharePoint",
|
||||||
};
|
};
|
||||||
return nameMap[provider] || provider;
|
return nameMap[provider] || provider;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
<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>
|
<p>Loading {getProviderDisplayName()} connector...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !connector) {
|
if (error || !connector) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center max-w-md">
|
<div className="text-center max-w-md">
|
||||||
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||||
<h2 className="text-xl font-semibold mb-2">
|
<h2 className="text-xl font-semibold mb-2">
|
||||||
Provider Not Available
|
Provider Not Available
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mb-4">{error}</p>
|
<p className="text-muted-foreground mb-4">{error}</p>
|
||||||
<Button onClick={() => router.push("/settings")}>
|
<Button onClick={() => router.push("/settings")}>
|
||||||
Configure Connectors
|
Configure Connectors
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connector.status !== "connected") {
|
if (connector.status !== "connected") {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center max-w-md">
|
<div className="text-center max-w-md">
|
||||||
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
|
<AlertCircle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
|
||||||
<h2 className="text-xl font-semibold mb-2">
|
<h2 className="text-xl font-semibold mb-2">
|
||||||
{connector.name} Not Connected
|
{connector.name} Not Connected
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">
|
||||||
You need to connect your {connector.name} account before you can
|
You need to connect your {connector.name} account before you can
|
||||||
select files.
|
select files.
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => router.push("/settings")}>
|
<Button onClick={() => router.push("/settings")}>
|
||||||
Connect {connector.name}
|
Connect {connector.name}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connector.hasAccessToken) {
|
if (!connector.hasAccessToken) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center max-w-md">
|
<div className="text-center max-w-md">
|
||||||
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||||
<h2 className="text-xl font-semibold mb-2">
|
<h2 className="text-xl font-semibold mb-2">
|
||||||
Access Token Required
|
Access Token Required
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">
|
||||||
{connector.accessTokenError ||
|
{connector.accessTokenError ||
|
||||||
`Unable to get access token for ${connector.name}. Try reconnecting your account.`}
|
`Unable to get access token for ${connector.name}. Try reconnecting your account.`}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => router.push("/settings")}>
|
<Button onClick={() => router.push("/settings")}>
|
||||||
Reconnect {connector.name}
|
Reconnect {connector.name}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-3xl p-6">
|
<div className="container mx-auto max-w-3xl p-6">
|
||||||
<div className="mb-6 flex gap-2 items-center">
|
<div className="mb-6 flex gap-2 items-center">
|
||||||
<Button variant="ghost" onClick={() => router.back()}>
|
<Button variant="ghost" onClick={() => router.back()}>
|
||||||
<ArrowLeft className="h-4 w-4 scale-125" />
|
<ArrowLeft className="h-4 w-4 scale-125" />
|
||||||
</Button>
|
</Button>
|
||||||
<h2 className="text-2xl font-bold">
|
<h2 className="text-2xl font-bold">
|
||||||
Add from {getProviderDisplayName()}
|
Add from {getProviderDisplayName()}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
<UnifiedCloudPicker
|
<UnifiedCloudPicker
|
||||||
provider={
|
provider={
|
||||||
connector.type as "google_drive" | "onedrive" | "sharepoint"
|
connector.type as "google_drive" | "onedrive" | "sharepoint"
|
||||||
}
|
}
|
||||||
onFileSelected={handleFileSelected}
|
onFileSelected={handleFileSelected}
|
||||||
selectedFiles={selectedFiles}
|
selectedFiles={selectedFiles}
|
||||||
isAuthenticated={true}
|
isAuthenticated={true}
|
||||||
accessToken={accessToken || undefined}
|
accessToken={accessToken || undefined}
|
||||||
clientId={connector.clientId}
|
clientId={connector.clientId}
|
||||||
onSettingsChange={setIngestSettings}
|
onSettingsChange={setIngestSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-3xl mx-auto mt-6">
|
<div className="max-w-3xl mx-auto mt-6">
|
||||||
<div className="flex justify-between gap-3 mb-4">
|
<div className="flex justify-between gap-3 mb-4">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className=" border bg-transparent border-border rounded-lg text-secondary-foreground"
|
className=" border bg-transparent border-border rounded-lg text-secondary-foreground"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => handleSync(connector)}
|
onClick={() => handleSync(connector)}
|
||||||
disabled={selectedFiles.length === 0 || isIngesting}
|
disabled={selectedFiles.length === 0 || isIngesting}
|
||||||
>
|
>
|
||||||
{isIngesting ? (
|
{isIngesting ? (
|
||||||
<>Ingesting {selectedFiles.length} Files...</>
|
<>Ingesting {selectedFiles.length} Files...</>
|
||||||
) : (
|
) : (
|
||||||
<>Start ingest</>
|
<>Start ingest</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Success toast notification */}
|
);
|
||||||
<Toast
|
|
||||||
message="Ingested successfully!."
|
|
||||||
show={showSuccessToast}
|
|
||||||
onHide={() => setShowSuccessToast(false)}
|
|
||||||
duration={20000}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
||||||
newTask.status === "completed"
|
newTask.status === "completed"
|
||||||
) {
|
) {
|
||||||
// Task just completed - show success toast
|
// Task just completed - show success toast
|
||||||
toast.success("Task completed successfully!", {
|
toast.success("Task completed successfully", {
|
||||||
description: `Task ${newTask.task_id} has finished processing.`,
|
description: `Task ${newTask.task_id} has finished processing.`,
|
||||||
action: {
|
action: {
|
||||||
label: "View",
|
label: "View",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
from starlette.requests import Request
|
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
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
@ -22,7 +27,7 @@ class ConnectorRouter:
|
||||||
self.openrag_connector_service = openrag_connector_service
|
self.openrag_connector_service = openrag_connector_service
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"ConnectorRouter initialized",
|
"ConnectorRouter initialized",
|
||||||
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW
|
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_active_service(self):
|
def get_active_service(self):
|
||||||
|
|
@ -38,6 +43,8 @@ class ConnectorRouter:
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""Initialize the active connector service."""
|
"""Initialize the active connector service."""
|
||||||
|
# Initialize OpenSearch index if using traditional OpenRAG connector service
|
||||||
|
|
||||||
return await self.get_active_service().initialize()
|
return await self.get_active_service().initialize()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -49,7 +56,9 @@ class ConnectorRouter:
|
||||||
"""Get a connector instance from the active service."""
|
"""Get a connector instance from the active service."""
|
||||||
return await self.get_active_service().get_connector(connection_id)
|
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."""
|
"""Sync specific files using the active service."""
|
||||||
return await self.get_active_service().sync_specific_files(
|
return await self.get_active_service().sync_specific_files(
|
||||||
connection_id, user_id, file_list, jwt_token
|
connection_id, user_id, file_list, jwt_token
|
||||||
|
|
@ -64,4 +73,6 @@ class ConnectorRouter:
|
||||||
if hasattr(active_service, name):
|
if hasattr(active_service, name):
|
||||||
return getattr(active_service, name)
|
return getattr(active_service, name)
|
||||||
else:
|
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.container_utils import transform_localhost_url
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
from config.settings import (
|
from config.settings import (
|
||||||
|
DISABLE_INGEST_WITH_LANGFLOW,
|
||||||
LANGFLOW_URL,
|
LANGFLOW_URL,
|
||||||
LANGFLOW_CHAT_FLOW_ID,
|
LANGFLOW_CHAT_FLOW_ID,
|
||||||
LANGFLOW_INGEST_FLOW_ID,
|
LANGFLOW_INGEST_FLOW_ID,
|
||||||
|
|
@ -450,7 +451,7 @@ async def onboarding(request, flows_service):
|
||||||
config_updated = True
|
config_updated = True
|
||||||
|
|
||||||
# Update knowledge settings
|
# Update knowledge settings
|
||||||
if "embedding_model" in body:
|
if "embedding_model" in body and not DISABLE_INGEST_WITH_LANGFLOW:
|
||||||
if (
|
if (
|
||||||
not isinstance(body["embedding_model"], str)
|
not isinstance(body["embedding_model"], str)
|
||||||
or not body["embedding_model"].strip()
|
or not body["embedding_model"].strip()
|
||||||
|
|
@ -600,11 +601,16 @@ async def onboarding(request, flows_service):
|
||||||
# Import here to avoid circular imports
|
# Import here to avoid circular imports
|
||||||
from main import init_index
|
from main import init_index
|
||||||
|
|
||||||
logger.info("Initializing OpenSearch index after onboarding configuration")
|
logger.info(
|
||||||
|
"Initializing OpenSearch index after onboarding configuration"
|
||||||
|
)
|
||||||
await init_index()
|
await init_index()
|
||||||
logger.info("OpenSearch index initialization completed successfully")
|
logger.info("OpenSearch index initialization completed successfully")
|
||||||
except Exception as e:
|
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
|
# Don't fail the entire onboarding process if index creation fails
|
||||||
# The application can still work, but document operations may fail
|
# 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 (
|
from config.settings import (
|
||||||
DISABLE_INGEST_WITH_LANGFLOW,
|
DISABLE_INGEST_WITH_LANGFLOW,
|
||||||
EMBED_MODEL,
|
EMBED_MODEL,
|
||||||
|
INDEX_BODY,
|
||||||
INDEX_NAME,
|
INDEX_NAME,
|
||||||
SESSION_SECRET,
|
SESSION_SECRET,
|
||||||
clients,
|
clients,
|
||||||
|
|
@ -82,6 +83,7 @@ logger.info(
|
||||||
cuda_version=torch.version.cuda,
|
cuda_version=torch.version.cuda,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_opensearch():
|
async def wait_for_opensearch():
|
||||||
"""Wait for OpenSearch to be ready with retries"""
|
"""Wait for OpenSearch to be ready with retries"""
|
||||||
max_retries = 30
|
max_retries = 30
|
||||||
|
|
@ -128,6 +130,34 @@ async def configure_alerting_security():
|
||||||
# Don't fail startup if alerting config fails
|
# 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():
|
async def init_index():
|
||||||
"""Initialize OpenSearch index and security roles"""
|
"""Initialize OpenSearch index and security roles"""
|
||||||
await wait_for_opensearch()
|
await wait_for_opensearch()
|
||||||
|
|
@ -141,10 +171,20 @@ async def init_index():
|
||||||
|
|
||||||
# Create documents index
|
# Create documents index
|
||||||
if not await clients.opensearch.indices.exists(index=INDEX_NAME):
|
if not await clients.opensearch.indices.exists(index=INDEX_NAME):
|
||||||
await clients.opensearch.indices.create(index=INDEX_NAME, body=dynamic_index_body)
|
await clients.opensearch.indices.create(
|
||||||
logger.info("Created OpenSearch index", index_name=INDEX_NAME, embedding_model=embedding_model)
|
index=INDEX_NAME, body=dynamic_index_body
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Created OpenSearch index",
|
||||||
|
index_name=INDEX_NAME,
|
||||||
|
embedding_model=embedding_model,
|
||||||
|
)
|
||||||
else:
|
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
|
# Create knowledge filters index
|
||||||
knowledge_filter_index_name = "knowledge_filters"
|
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
|
# Index will be created after onboarding when we know the embedding model
|
||||||
await wait_for_opensearch()
|
await wait_for_opensearch()
|
||||||
|
|
||||||
|
if DISABLE_INGEST_WITH_LANGFLOW:
|
||||||
|
await _ensure_opensearch_index()
|
||||||
|
|
||||||
# Configure alerting security
|
# Configure alerting security
|
||||||
await configure_alerting_security()
|
await configure_alerting_security()
|
||||||
|
|
||||||
|
|
@ -1075,14 +1118,6 @@ async def create_app():
|
||||||
return 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():
|
def cleanup():
|
||||||
"""Cleanup on application shutdown"""
|
"""Cleanup on application shutdown"""
|
||||||
# Cleanup process pools only (webhooks handled by Starlette shutdown)
|
# Cleanup process pools only (webhooks handled by Starlette shutdown)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from config.settings import (
|
from config.settings import (
|
||||||
|
DISABLE_INGEST_WITH_LANGFLOW,
|
||||||
NUDGES_FLOW_ID,
|
NUDGES_FLOW_ID,
|
||||||
LANGFLOW_URL,
|
LANGFLOW_URL,
|
||||||
LANGFLOW_CHAT_FLOW_ID,
|
LANGFLOW_CHAT_FLOW_ID,
|
||||||
|
|
@ -73,17 +74,17 @@ class FlowsService:
|
||||||
# Scan all JSON files in the flows directory
|
# Scan all JSON files in the flows directory
|
||||||
try:
|
try:
|
||||||
for filename in os.listdir(flows_dir):
|
for filename in os.listdir(flows_dir):
|
||||||
if not filename.endswith('.json'):
|
if not filename.endswith(".json"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(flows_dir, filename)
|
file_path = os.path.join(flows_dir, filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r') as f:
|
with open(file_path, "r") as f:
|
||||||
flow_data = json.load(f)
|
flow_data = json.load(f)
|
||||||
|
|
||||||
# Check if this file contains the flow we're looking for
|
# 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
|
# Cache the result
|
||||||
self._flow_file_cache[flow_id] = file_path
|
self._flow_file_cache[flow_id] = file_path
|
||||||
logger.info(f"Found flow {flow_id} in file: {filename}")
|
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")
|
logger.warning(f"Flow with ID {flow_id} not found in flows directory")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def reset_langflow_flow(self, flow_type: str):
|
async def reset_langflow_flow(self, flow_type: str):
|
||||||
"""Reset a Langflow flow by uploading the corresponding JSON file
|
"""Reset a Langflow flow by uploading the corresponding JSON file
|
||||||
|
|
||||||
|
|
@ -135,7 +137,9 @@ class FlowsService:
|
||||||
try:
|
try:
|
||||||
with open(flow_path, "r") as f:
|
with open(flow_path, "r") as f:
|
||||||
flow_data = json.load(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:
|
except json.JSONDecodeError as e:
|
||||||
raise ValueError(f"Invalid JSON in flow file {flow_path}: {e}")
|
raise ValueError(f"Invalid JSON in flow file {flow_path}: {e}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|
@ -161,43 +165,62 @@ class FlowsService:
|
||||||
|
|
||||||
# Check if configuration has been edited (onboarding completed)
|
# Check if configuration has been edited (onboarding completed)
|
||||||
if config.edited:
|
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()
|
provider = config.provider.model_provider.lower()
|
||||||
|
|
||||||
# Step 1: Assign model provider (replace components) if not OpenAI
|
# Step 1: Assign model provider (replace components) if not OpenAI
|
||||||
if provider != "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)
|
provider_result = await self.assign_model_provider(provider)
|
||||||
|
|
||||||
if not provider_result.get("success"):
|
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
|
# Continue anyway, maybe just value updates will work
|
||||||
|
|
||||||
# Step 2: Update model values for the specific flow being reset
|
# Step 2: Update model values for the specific flow being reset
|
||||||
single_flow_config = [{
|
single_flow_config = [
|
||||||
"name": flow_type,
|
{
|
||||||
"flow_id": flow_id,
|
"name": flow_type,
|
||||||
}]
|
"flow_id": flow_id,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
logger.info(f"Updating {flow_type} flow model values")
|
logger.info(f"Updating {flow_type} flow model values")
|
||||||
update_result = await self.change_langflow_model_value(
|
update_result = await self.change_langflow_model_value(
|
||||||
provider=provider,
|
provider=provider,
|
||||||
embedding_model=config.knowledge.embedding_model,
|
embedding_model=config.knowledge.embedding_model,
|
||||||
llm_model=config.agent.llm_model,
|
llm_model=config.agent.llm_model,
|
||||||
endpoint=config.provider.endpoint if config.provider.endpoint else None,
|
endpoint=config.provider.endpoint
|
||||||
flow_configs=single_flow_config
|
if config.provider.endpoint
|
||||||
|
else None,
|
||||||
|
flow_configs=single_flow_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
if update_result.get("success"):
|
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:
|
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:
|
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:
|
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
|
# Don't fail the entire reset operation if configuration update fails
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -243,7 +266,9 @@ class FlowsService:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Load component templates based on provider
|
# 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")
|
logger.info(f"Assigning {provider} components")
|
||||||
|
|
||||||
|
|
@ -358,7 +383,9 @@ class FlowsService:
|
||||||
logger.info(f"Loaded component templates for {provider}")
|
logger.info(f"Loaded component templates for {provider}")
|
||||||
return llm_template, embedding_template, llm_text_template
|
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"""
|
"""Update components in a specific flow"""
|
||||||
flow_name = config["name"]
|
flow_name = config["name"]
|
||||||
flow_id = config["flow_id"]
|
flow_id = config["flow_id"]
|
||||||
|
|
@ -383,20 +410,23 @@ class FlowsService:
|
||||||
components_updated = []
|
components_updated = []
|
||||||
|
|
||||||
# Replace embedding component
|
# Replace embedding component
|
||||||
embedding_node = self._find_node_by_id(flow_data, old_embedding_id)
|
if not DISABLE_INGEST_WITH_LANGFLOW:
|
||||||
if embedding_node:
|
embedding_node = self._find_node_by_id(flow_data, old_embedding_id)
|
||||||
# Preserve position
|
if embedding_node:
|
||||||
original_position = embedding_node.get("position", {})
|
# Preserve position
|
||||||
|
original_position = embedding_node.get("position", {})
|
||||||
|
|
||||||
# Replace with new template
|
# Replace with new template
|
||||||
new_embedding_node = embedding_template.copy()
|
new_embedding_node = embedding_template.copy()
|
||||||
new_embedding_node["position"] = original_position
|
new_embedding_node["position"] = original_position
|
||||||
|
|
||||||
# Replace in flow
|
# Replace in flow
|
||||||
self._replace_node_in_flow(flow_data, old_embedding_id, new_embedding_node)
|
self._replace_node_in_flow(
|
||||||
components_updated.append(
|
flow_data, old_embedding_id, new_embedding_node
|
||||||
f"embedding: {old_embedding_id} -> {new_embedding_id}"
|
)
|
||||||
)
|
components_updated.append(
|
||||||
|
f"embedding: {old_embedding_id} -> {new_embedding_id}"
|
||||||
|
)
|
||||||
|
|
||||||
# Replace LLM component (if exists in this flow)
|
# Replace LLM component (if exists in this flow)
|
||||||
if old_llm_id:
|
if old_llm_id:
|
||||||
|
|
@ -425,27 +455,30 @@ class FlowsService:
|
||||||
new_llm_text_node["position"] = original_position
|
new_llm_text_node["position"] = original_position
|
||||||
|
|
||||||
# Replace in flow
|
# Replace in flow
|
||||||
self._replace_node_in_flow(flow_data, old_llm_text_id, new_llm_text_node)
|
self._replace_node_in_flow(
|
||||||
components_updated.append(f"llm: {old_llm_text_id} -> {new_llm_text_id}")
|
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
|
# Update all edge references using regex replacement
|
||||||
flow_json_str = json.dumps(flow_data)
|
flow_json_str = json.dumps(flow_data)
|
||||||
|
|
||||||
# Replace embedding ID references
|
# Replace embedding ID references
|
||||||
flow_json_str = re.sub(
|
if not DISABLE_INGEST_WITH_LANGFLOW:
|
||||||
re.escape(old_embedding_id), new_embedding_id, flow_json_str
|
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]),
|
flow_json_str = re.sub(
|
||||||
new_embedding_id.split("-")[0],
|
re.escape(old_embedding_id.split("-")[0]),
|
||||||
flow_json_str,
|
new_embedding_id.split("-")[0],
|
||||||
)
|
flow_json_str,
|
||||||
|
)
|
||||||
|
|
||||||
# Replace LLM ID references (if applicable)
|
# Replace LLM ID references (if applicable)
|
||||||
if old_llm_id:
|
if old_llm_id:
|
||||||
flow_json_str = re.sub(
|
flow_json_str = re.sub(re.escape(old_llm_id), new_llm_id, flow_json_str)
|
||||||
re.escape(old_llm_id), new_llm_id, flow_json_str
|
|
||||||
)
|
|
||||||
if old_llm_text_id:
|
if old_llm_text_id:
|
||||||
flow_json_str = re.sub(
|
flow_json_str = re.sub(
|
||||||
re.escape(old_llm_text_id), new_llm_text_id, flow_json_str
|
re.escape(old_llm_text_id), new_llm_text_id, flow_json_str
|
||||||
|
|
@ -506,7 +539,14 @@ class FlowsService:
|
||||||
|
|
||||||
return None, None
|
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.
|
Generic helper function to update any field in any Langflow component.
|
||||||
|
|
||||||
|
|
@ -521,22 +561,26 @@ class FlowsService:
|
||||||
raise ValueError("flow_id is required")
|
raise ValueError("flow_id is required")
|
||||||
|
|
||||||
# Get the current flow data from Langflow
|
# Get the current flow data from Langflow
|
||||||
response = await clients.langflow_request(
|
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
|
||||||
"GET", f"/api/v1/flows/{flow_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
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()
|
flow_data = response.json()
|
||||||
|
|
||||||
# Find the target component by display name first, then by ID as fallback
|
# Find the target component by display name first, then by ID as fallback
|
||||||
target_node, target_node_index = None, None
|
target_node, target_node_index = None, None
|
||||||
if node_display_name:
|
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:
|
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:
|
if target_node is None:
|
||||||
identifier = node_display_name or node_id
|
identifier = node_display_name or node_id
|
||||||
|
|
@ -545,7 +589,9 @@ class FlowsService:
|
||||||
# Update the field value directly in the existing node
|
# Update the field value directly in the existing node
|
||||||
template = target_node.get("data", {}).get("node", {}).get("template", {})
|
template = target_node.get("data", {}).get("node", {}).get("template", {})
|
||||||
if template.get(field_name):
|
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:
|
else:
|
||||||
identifier = node_display_name or node_id
|
identifier = node_display_name or node_id
|
||||||
raise Exception(f"{field_name} field not found in {identifier} component")
|
raise Exception(f"{field_name} field not found in {identifier} component")
|
||||||
|
|
@ -556,21 +602,31 @@ class FlowsService:
|
||||||
)
|
)
|
||||||
|
|
||||||
if patch_response.status_code != 200:
|
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):
|
async def update_chat_flow_model(self, model_name: str):
|
||||||
"""Helper function to update the model in the chat flow"""
|
"""Helper function to update the model in the chat flow"""
|
||||||
if not LANGFLOW_CHAT_FLOW_ID:
|
if not LANGFLOW_CHAT_FLOW_ID:
|
||||||
raise ValueError("LANGFLOW_CHAT_FLOW_ID is not configured")
|
raise ValueError("LANGFLOW_CHAT_FLOW_ID is not configured")
|
||||||
await self._update_flow_field(LANGFLOW_CHAT_FLOW_ID, "model_name", model_name,
|
await self._update_flow_field(
|
||||||
node_display_name="Language Model")
|
LANGFLOW_CHAT_FLOW_ID,
|
||||||
|
"model_name",
|
||||||
|
model_name,
|
||||||
|
node_display_name="Language Model",
|
||||||
|
)
|
||||||
|
|
||||||
async def update_chat_flow_system_prompt(self, system_prompt: str):
|
async def update_chat_flow_system_prompt(self, system_prompt: str):
|
||||||
"""Helper function to update the system prompt in the chat flow"""
|
"""Helper function to update the system prompt in the chat flow"""
|
||||||
if not LANGFLOW_CHAT_FLOW_ID:
|
if not LANGFLOW_CHAT_FLOW_ID:
|
||||||
raise ValueError("LANGFLOW_CHAT_FLOW_ID is not configured")
|
raise ValueError("LANGFLOW_CHAT_FLOW_ID is not configured")
|
||||||
await self._update_flow_field(LANGFLOW_CHAT_FLOW_ID, "system_prompt", system_prompt,
|
await self._update_flow_field(
|
||||||
node_display_name="Agent")
|
LANGFLOW_CHAT_FLOW_ID,
|
||||||
|
"system_prompt",
|
||||||
|
system_prompt,
|
||||||
|
node_display_name="Agent",
|
||||||
|
)
|
||||||
|
|
||||||
async def update_flow_docling_preset(self, preset: str, preset_config: dict):
|
async def update_flow_docling_preset(self, preset: str, preset_config: dict):
|
||||||
"""Helper function to update docling preset in the ingest flow"""
|
"""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")
|
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
|
||||||
|
|
||||||
from config.settings import DOCLING_COMPONENT_ID
|
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):
|
async def update_ingest_flow_chunk_size(self, chunk_size: int):
|
||||||
"""Helper function to update chunk size in the ingest flow"""
|
"""Helper function to update chunk size in the ingest flow"""
|
||||||
if not LANGFLOW_INGEST_FLOW_ID:
|
if not LANGFLOW_INGEST_FLOW_ID:
|
||||||
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
|
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
|
||||||
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "chunk_size", chunk_size,
|
await self._update_flow_field(
|
||||||
node_display_name="Split Text")
|
LANGFLOW_INGEST_FLOW_ID,
|
||||||
|
"chunk_size",
|
||||||
|
chunk_size,
|
||||||
|
node_display_name="Split Text",
|
||||||
|
)
|
||||||
|
|
||||||
async def update_ingest_flow_chunk_overlap(self, chunk_overlap: int):
|
async def update_ingest_flow_chunk_overlap(self, chunk_overlap: int):
|
||||||
"""Helper function to update chunk overlap in the ingest flow"""
|
"""Helper function to update chunk overlap in the ingest flow"""
|
||||||
if not LANGFLOW_INGEST_FLOW_ID:
|
if not LANGFLOW_INGEST_FLOW_ID:
|
||||||
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
|
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
|
||||||
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "chunk_overlap", chunk_overlap,
|
await self._update_flow_field(
|
||||||
node_display_name="Split Text")
|
LANGFLOW_INGEST_FLOW_ID,
|
||||||
|
"chunk_overlap",
|
||||||
|
chunk_overlap,
|
||||||
|
node_display_name="Split Text",
|
||||||
|
)
|
||||||
|
|
||||||
async def update_ingest_flow_embedding_model(self, embedding_model: str):
|
async def update_ingest_flow_embedding_model(self, embedding_model: str):
|
||||||
"""Helper function to update embedding model in the ingest flow"""
|
"""Helper function to update embedding model in the ingest flow"""
|
||||||
if not LANGFLOW_INGEST_FLOW_ID:
|
if not LANGFLOW_INGEST_FLOW_ID:
|
||||||
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
|
raise ValueError("LANGFLOW_INGEST_FLOW_ID is not configured")
|
||||||
await self._update_flow_field(LANGFLOW_INGEST_FLOW_ID, "model", embedding_model,
|
await self._update_flow_field(
|
||||||
node_display_name="Embedding Model")
|
LANGFLOW_INGEST_FLOW_ID,
|
||||||
|
"model",
|
||||||
|
embedding_model,
|
||||||
|
node_display_name="Embedding Model",
|
||||||
|
)
|
||||||
|
|
||||||
def _replace_node_in_flow(self, flow_data, old_id, new_node):
|
def _replace_node_in_flow(self, flow_data, old_id, new_node):
|
||||||
"""Replace a node in the flow data"""
|
"""Replace a node in the flow data"""
|
||||||
|
|
@ -612,7 +685,12 @@ class FlowsService:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def change_langflow_model_value(
|
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
|
Change dropdown values for provider-specific components across flows
|
||||||
|
|
@ -656,8 +734,8 @@ class FlowsService:
|
||||||
]
|
]
|
||||||
|
|
||||||
# Determine target component IDs based on provider
|
# Determine target component IDs based on provider
|
||||||
target_embedding_id, target_llm_id, target_llm_text_id = self._get_provider_component_ids(
|
target_embedding_id, target_llm_id, target_llm_text_id = (
|
||||||
provider
|
self._get_provider_component_ids(provider)
|
||||||
)
|
)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
@ -713,12 +791,24 @@ class FlowsService:
|
||||||
def _get_provider_component_ids(self, provider: str):
|
def _get_provider_component_ids(self, provider: str):
|
||||||
"""Get the component IDs for a specific provider"""
|
"""Get the component IDs for a specific provider"""
|
||||||
if provider == "watsonx":
|
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":
|
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":
|
elif provider == "openai":
|
||||||
# OpenAI components are the default ones
|
# 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:
|
else:
|
||||||
raise ValueError(f"Unsupported provider: {provider}")
|
raise ValueError(f"Unsupported provider: {provider}")
|
||||||
|
|
||||||
|
|
@ -738,9 +828,7 @@ class FlowsService:
|
||||||
flow_id = config["flow_id"]
|
flow_id = config["flow_id"]
|
||||||
|
|
||||||
# Get flow data from Langflow API instead of file
|
# Get flow data from Langflow API instead of file
|
||||||
response = await clients.langflow_request(
|
response = await clients.langflow_request("GET", f"/api/v1/flows/{flow_id}")
|
||||||
"GET", f"/api/v1/flows/{flow_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
|
@ -752,12 +840,13 @@ class FlowsService:
|
||||||
updates_made = []
|
updates_made = []
|
||||||
|
|
||||||
# Update embedding component
|
# Update embedding component
|
||||||
embedding_node = self._find_node_by_id(flow_data, target_embedding_id)
|
if not DISABLE_INGEST_WITH_LANGFLOW:
|
||||||
if embedding_node:
|
embedding_node = self._find_node_by_id(flow_data, target_embedding_id)
|
||||||
if self._update_component_fields(
|
if embedding_node:
|
||||||
embedding_node, provider, embedding_model, endpoint
|
if self._update_component_fields(
|
||||||
):
|
embedding_node, provider, embedding_model, endpoint
|
||||||
updates_made.append(f"embedding model: {embedding_model}")
|
):
|
||||||
|
updates_made.append(f"embedding model: {embedding_model}")
|
||||||
|
|
||||||
# Update LLM component (if exists in this flow)
|
# Update LLM component (if exists in this flow)
|
||||||
if target_llm_id:
|
if target_llm_id:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,27 @@ class ModelsService:
|
||||||
"jina-embeddings-v2-base-en",
|
"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):
|
def __init__(self):
|
||||||
self.session_manager = None
|
self.session_manager = None
|
||||||
|
|
||||||
|
|
@ -49,12 +70,12 @@ class ModelsService:
|
||||||
model_id = model.get("id", "")
|
model_id = model.get("id", "")
|
||||||
|
|
||||||
# Language models (GPT models)
|
# 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(
|
language_models.append(
|
||||||
{
|
{
|
||||||
"value": model_id,
|
"value": model_id,
|
||||||
"label": model_id,
|
"label": model_id,
|
||||||
"default": model_id == "gpt-4o-mini",
|
"default": model_id == "gpt-5",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue