Added restore flow functionality

This commit is contained in:
Lucas Oliveira 2025-09-08 18:07:36 -03:00
parent cd780072b5
commit 24b4d8a83f
4 changed files with 569 additions and 256 deletions

View file

@ -1,50 +1,55 @@
"use client" "use client";
import { useState, useEffect, useCallback, Suspense } from "react"
import { useSearchParams } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Switch } from "@/components/ui/switch"
import { Loader2, PlugZap, RefreshCw } from "lucide-react"
import { ProtectedRoute } from "@/components/protected-route"
import { useTask } from "@/contexts/task-context"
import { useAuth } from "@/contexts/auth-context"
import { ConfirmationDialog } from "@/components/confirmation-dialog"
import { Loader2, PlugZap, RefreshCw } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { Suspense, useCallback, useEffect, useState } from "react";
import { ConfirmationDialog } from "@/components/confirmation-dialog";
import { ProtectedRoute } from "@/components/protected-route";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useAuth } from "@/contexts/auth-context";
import { useTask } from "@/contexts/task-context";
interface GoogleDriveFile { interface GoogleDriveFile {
id: string id: string;
name: string name: string;
mimeType: string mimeType: string;
webViewLink?: string webViewLink?: string;
iconLink?: string iconLink?: string;
} }
interface OneDriveFile { interface OneDriveFile {
id: string id: string;
name: string name: string;
mimeType?: string mimeType?: string;
webUrl?: string webUrl?: string;
driveItem?: { driveItem?: {
file?: { mimeType: string } file?: { mimeType: string };
folder?: unknown folder?: unknown;
} };
} }
interface Connector { interface Connector {
id: string id: string;
name: string name: string;
description: string description: string;
icon: React.ReactNode icon: React.ReactNode;
status: "not_connected" | "connecting" | "connected" | "error" status: "not_connected" | "connecting" | "connected" | "error";
type: string type: string;
connectionId?: string connectionId?: string;
access_token?: string access_token?: string;
selectedFiles?: GoogleDriveFile[] | OneDriveFile[] selectedFiles?: GoogleDriveFile[] | OneDriveFile[];
} }
interface SyncResult { interface SyncResult {
@ -56,192 +61,203 @@ interface SyncResult {
} }
interface Connection { interface Connection {
connection_id: string connection_id: string;
is_active: boolean is_active: boolean;
created_at: string created_at: string;
last_sync?: string last_sync?: string;
} }
function KnowledgeSourcesPage() { function KnowledgeSourcesPage() {
const { isAuthenticated, isNoAuthMode } = useAuth() const { isAuthenticated, isNoAuthMode } = useAuth();
const { addTask, tasks } = useTask() const { addTask, tasks } = useTask();
const searchParams = useSearchParams() const searchParams = useSearchParams();
// Connectors state // Connectors state
const [connectors, setConnectors] = useState<Connector[]>([]) const [connectors, setConnectors] = useState<Connector[]>([]);
const [isConnecting, setIsConnecting] = useState<string | null>(null) const [isConnecting, setIsConnecting] = useState<string | null>(null);
const [isSyncing, setIsSyncing] = useState<string | null>(null) const [isSyncing, setIsSyncing] = useState<string | null>(null);
const [syncResults, setSyncResults] = useState<{[key: string]: SyncResult | null}>({}) const [syncResults, setSyncResults] = useState<{
const [maxFiles, setMaxFiles] = useState<number>(10) [key: string]: SyncResult | null;
const [syncAllFiles, setSyncAllFiles] = useState<boolean>(false) }>({});
const [maxFiles, setMaxFiles] = useState<number>(10);
const [syncAllFiles, setSyncAllFiles] = useState<boolean>(false);
// Settings state // Settings state
// Note: backend internal Langflow URL is not needed on the frontend // Note: backend internal Langflow URL is not needed on the frontend
const [flowId, setFlowId] = useState<string>('1098eea1-6649-4e1d-aed1-b77249fb8dd0') const [flowId, setFlowId] = useState<string>(
const [langflowEditUrl, setLangflowEditUrl] = useState<string>('') "1098eea1-6649-4e1d-aed1-b77249fb8dd0",
const [publicLangflowUrl, setPublicLangflowUrl] = useState<string>('') );
const [langflowEditUrl, setLangflowEditUrl] = useState<string>("");
const [publicLangflowUrl, setPublicLangflowUrl] = useState<string>("");
// Knowledge Ingest settings // Knowledge Ingest settings
const [ocrEnabled, setOcrEnabled] = useState<boolean>(false) const [ocrEnabled, setOcrEnabled] = useState<boolean>(false);
const [pictureDescriptionsEnabled, setPictureDescriptionsEnabled] = useState<boolean>(false) const [pictureDescriptionsEnabled, setPictureDescriptionsEnabled] =
useState<boolean>(false);
// Fetch settings from backend // Fetch settings from backend
const fetchSettings = useCallback(async () => { const fetchSettings = useCallback(async () => {
try { try {
const response = await fetch('/api/settings') const response = await fetch("/api/settings");
if (response.ok) { if (response.ok) {
const settings = await response.json() const settings = await response.json();
if (settings.flow_id) { if (settings.flow_id) {
setFlowId(settings.flow_id) setFlowId(settings.flow_id);
} }
if (settings.langflow_edit_url) { if (settings.langflow_edit_url) {
setLangflowEditUrl(settings.langflow_edit_url) setLangflowEditUrl(settings.langflow_edit_url);
} }
if (settings.langflow_public_url) { if (settings.langflow_public_url) {
setPublicLangflowUrl(settings.langflow_public_url) setPublicLangflowUrl(settings.langflow_public_url);
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch settings:', error) console.error("Failed to fetch settings:", error);
} }
}, []) }, []);
// Helper function to get connector icon // Helper function to get connector icon
const getConnectorIcon = (iconName: string) => { const getConnectorIcon = (iconName: string) => {
const iconMap: { [key: string]: React.ReactElement } = { const iconMap: { [key: string]: React.ReactElement } = {
'google-drive': ( "google-drive": (
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center text-white font-bold leading-none shrink-0"> <div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
G G
</div> </div>
), ),
'sharepoint': ( sharepoint: (
<div className="w-8 h-8 bg-blue-700 rounded flex items-center justify-center text-white font-bold leading-none shrink-0"> <div className="w-8 h-8 bg-blue-700 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
SP SP
</div> </div>
), ),
'onedrive': ( onedrive: (
<div className="w-8 h-8 bg-blue-400 rounded flex items-center justify-center text-white font-bold leading-none shrink-0"> <div className="w-8 h-8 bg-blue-400 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
OD OD
</div> </div>
), ),
} };
return iconMap[iconName] || ( return (
<div className="w-8 h-8 bg-gray-500 rounded flex items-center justify-center text-white font-bold leading-none shrink-0"> iconMap[iconName] || (
? <div className="w-8 h-8 bg-gray-500 rounded flex items-center justify-center text-white font-bold leading-none shrink-0">
</div> ?
) </div>
} )
);
};
// Connector functions // Connector functions
const checkConnectorStatuses = useCallback(async () => { const checkConnectorStatuses = useCallback(async () => {
try { try {
// Fetch available connectors from backend // Fetch available connectors from backend
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 connectorTypes = Object.keys(connectorsResult.connectors) const connectorTypes = Object.keys(connectorsResult.connectors);
// Initialize connectors list with metadata from backend // Initialize connectors list with metadata from backend
const initialConnectors = connectorTypes const initialConnectors = connectorTypes
.filter(type => connectorsResult.connectors[type].available) // Only show available connectors .filter((type) => connectorsResult.connectors[type].available) // Only show available connectors
.map(type => ({ .map((type) => ({
id: type, id: type,
name: connectorsResult.connectors[type].name, name: connectorsResult.connectors[type].name,
description: connectorsResult.connectors[type].description, description: connectorsResult.connectors[type].description,
icon: getConnectorIcon(connectorsResult.connectors[type].icon), icon: getConnectorIcon(connectorsResult.connectors[type].icon),
status: "not_connected" as const, status: "not_connected" as const,
type: type type: type,
})) }));
setConnectors(initialConnectors) setConnectors(initialConnectors);
// Check status for each connector type // Check status for each connector type
for (const connectorType of connectorTypes) { for (const connectorType of connectorTypes) {
const response = await fetch(`/api/connectors/${connectorType}/status`) const response = await fetch(`/api/connectors/${connectorType}/status`);
if (response.ok) { if (response.ok) {
const data = await response.json() const data = await response.json();
const connections = data.connections || [] const connections = data.connections || [];
const activeConnection = connections.find((conn: Connection) => conn.is_active) const activeConnection = connections.find(
const isConnected = activeConnection !== undefined (conn: Connection) => conn.is_active,
);
const isConnected = activeConnection !== undefined;
setConnectors(prev => prev.map(c =>
c.type === connectorType setConnectors((prev) =>
? { prev.map((c) =>
...c, c.type === connectorType
status: isConnected ? "connected" : "not_connected", ? {
connectionId: activeConnection?.connection_id ...c,
} status: isConnected ? "connected" : "not_connected",
: c connectionId: activeConnection?.connection_id,
)) }
: c,
),
);
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to check connector statuses:', error) console.error("Failed to check connector statuses:", error);
} }
}, []) }, []);
const handleConnect = async (connector: Connector) => { const handleConnect = async (connector: Connector) => {
setIsConnecting(connector.id) setIsConnecting(connector.id);
setSyncResults(prev => ({ ...prev, [connector.id]: null })) setSyncResults((prev) => ({ ...prev, [connector.id]: null }));
try { try {
// Use the shared auth callback URL, same as connectors page // Use the shared auth callback URL, same as connectors page
const redirectUri = `${window.location.origin}/auth/callback` const redirectUri = `${window.location.origin}/auth/callback`;
const response = await fetch('/api/auth/init', { const response = await fetch("/api/auth/init", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
connector_type: connector.type, connector_type: connector.type,
purpose: "data_source", purpose: "data_source",
name: `${connector.name} Connection`, name: `${connector.name} Connection`,
redirect_uri: redirectUri redirect_uri: redirectUri,
}), }),
}) });
if (response.ok) { if (response.ok) {
const result = await response.json() const result = await response.json();
if (result.oauth_config) { if (result.oauth_config) {
localStorage.setItem('connecting_connector_id', result.connection_id) localStorage.setItem("connecting_connector_id", result.connection_id);
localStorage.setItem('connecting_connector_type', connector.type) localStorage.setItem("connecting_connector_type", connector.type);
const authUrl = `${result.oauth_config.authorization_endpoint}?` + const authUrl =
`${result.oauth_config.authorization_endpoint}?` +
`client_id=${result.oauth_config.client_id}&` + `client_id=${result.oauth_config.client_id}&` +
`response_type=code&` + `response_type=code&` +
`scope=${result.oauth_config.scopes.join(' ')}&` + `scope=${result.oauth_config.scopes.join(" ")}&` +
`redirect_uri=${encodeURIComponent(result.oauth_config.redirect_uri)}&` + `redirect_uri=${encodeURIComponent(
result.oauth_config.redirect_uri,
)}&` +
`access_type=offline&` + `access_type=offline&` +
`prompt=consent&` + `prompt=consent&` +
`state=${result.connection_id}` `state=${result.connection_id}`;
window.location.href = authUrl window.location.href = authUrl;
} }
} else { } else {
console.error('Failed to initiate connection') console.error("Failed to initiate connection");
setIsConnecting(null) setIsConnecting(null);
} }
} catch (error) { } catch (error) {
console.error('Connection error:', error) console.error("Connection error:", error);
setIsConnecting(null) setIsConnecting(null);
} }
} };
const handleSync = async (connector: Connector) => { const handleSync = async (connector: Connector) => {
if (!connector.connectionId) return if (!connector.connectionId) return;
setIsSyncing(connector.id) setIsSyncing(connector.id);
setSyncResults(prev => ({ ...prev, [connector.id]: null })) setSyncResults((prev) => ({ ...prev, [connector.id]: null }));
try { try {
const syncBody: { const syncBody: {
connection_id: string; connection_id: string;
@ -249,123 +265,156 @@ function KnowledgeSourcesPage() {
selected_files?: string[]; selected_files?: string[];
} = { } = {
connection_id: connector.connectionId, connection_id: connector.connectionId,
max_files: syncAllFiles ? 0 : (maxFiles || undefined) max_files: syncAllFiles ? 0 : maxFiles || undefined,
} };
// Note: File selection is now handled via the cloud connectors dialog // Note: File selection is now handled via the cloud connectors dialog
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 taskId = result.task_id const taskId = result.task_id;
if (taskId) { if (taskId) {
addTask(taskId) addTask(taskId);
setSyncResults(prev => ({ setSyncResults((prev) => ({
...prev, ...prev,
[connector.id]: { [connector.id]: {
processed: 0, processed: 0,
total: result.total_files || 0 total: result.total_files || 0,
} },
})) }));
} }
} else if (response.ok) { } else if (response.ok) {
setSyncResults(prev => ({ ...prev, [connector.id]: result })) setSyncResults((prev) => ({ ...prev, [connector.id]: result }));
// Note: Stats will auto-refresh via task completion watcher for async syncs // Note: Stats will auto-refresh via task completion watcher for async syncs
} 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);
} finally { } finally {
setIsSyncing(null) setIsSyncing(null);
} }
} };
const getStatusBadge = (status: Connector["status"]) => { const getStatusBadge = (status: Connector["status"]) => {
switch (status) { switch (status) {
case "connected": case "connected":
return <Badge variant="default" className="bg-green-500/20 text-green-400 border-green-500/30">Connected</Badge> return (
<Badge
variant="default"
className="bg-green-500/20 text-green-400 border-green-500/30"
>
Connected
</Badge>
);
case "connecting": case "connecting":
return <Badge variant="secondary" className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">Connecting...</Badge> return (
<Badge
variant="secondary"
className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30"
>
Connecting...
</Badge>
);
case "error": case "error":
return <Badge variant="destructive">Error</Badge> return <Badge variant="destructive">Error</Badge>;
default: default:
return <Badge variant="outline" className="bg-muted/20 text-muted-foreground border-muted whitespace-nowrap">Not Connected</Badge> return (
<Badge
variant="outline"
className="bg-muted/20 text-muted-foreground border-muted whitespace-nowrap"
>
Not Connected
</Badge>
);
} }
} };
// Fetch settings on mount when authenticated // Fetch settings on mount when authenticated
useEffect(() => { useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
fetchSettings() fetchSettings();
} }
}, [isAuthenticated, fetchSettings]) }, [isAuthenticated, fetchSettings]);
// Check connector status on mount and when returning from OAuth // Check connector status on mount and when returning from OAuth
useEffect(() => { useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
checkConnectorStatuses() checkConnectorStatuses();
} }
if (searchParams.get('oauth_success') === 'true') { if (searchParams.get("oauth_success") === "true") {
const url = new URL(window.location.href) const url = new URL(window.location.href);
url.searchParams.delete('oauth_success') url.searchParams.delete("oauth_success");
window.history.replaceState({}, '', url.toString()) window.history.replaceState({}, "", url.toString());
} }
}, [searchParams, isAuthenticated, checkConnectorStatuses]) }, [searchParams, isAuthenticated, checkConnectorStatuses]);
// Track previous tasks to detect new completions // Track previous tasks to detect new completions
const [prevTasks, setPrevTasks] = useState<typeof tasks>([]) const [prevTasks, setPrevTasks] = useState<typeof tasks>([]);
// Watch for task completions and refresh stats // Watch for task completions and refresh stats
useEffect(() => { useEffect(() => {
// Find newly completed tasks by comparing with previous state // Find newly completed tasks by comparing with previous state
const newlyCompletedTasks = tasks.filter(task => { const newlyCompletedTasks = tasks.filter((task) => {
const wasCompleted = prevTasks.find(prev => prev.task_id === task.task_id)?.status === 'completed' const wasCompleted =
return task.status === 'completed' && !wasCompleted prevTasks.find((prev) => prev.task_id === task.task_id)?.status ===
}) "completed";
return task.status === "completed" && !wasCompleted;
});
if (newlyCompletedTasks.length > 0) { if (newlyCompletedTasks.length > 0) {
// Task completed - could refresh data here if needed // Task completed - could refresh data here if needed
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
// Stats refresh removed // Stats refresh removed
}, 1000) }, 1000);
// Update previous tasks state // Update previous tasks state
setPrevTasks(tasks) setPrevTasks(tasks);
return () => clearTimeout(timeoutId) return () => clearTimeout(timeoutId);
} else { } else {
// Always update previous tasks state // Always update previous tasks state
setPrevTasks(tasks) setPrevTasks(tasks);
} }
}, [tasks, prevTasks]) }, [tasks, prevTasks]);
const handleEditInLangflow = () => { const handleEditInLangflow = () => {
const derivedFromWindow = typeof window !== 'undefined' const derivedFromWindow =
? `${window.location.protocol}//${window.location.hostname}:7860` typeof window !== "undefined"
: '' ? `${window.location.protocol}//${window.location.hostname}:7860`
const base = (publicLangflowUrl || derivedFromWindow || 'http://localhost:7860').replace(/\/$/, '') : "";
const computed = flowId ? `${base}/flow/${flowId}` : base const base = (
const url = langflowEditUrl || computed publicLangflowUrl ||
window.open(url, '_blank') derivedFromWindow ||
} "http://localhost:7860"
).replace(/\/$/, "");
const computed = flowId ? `${base}/flow/${flowId}` : base;
const url = langflowEditUrl || computed;
window.open(url, "_blank");
};
const handleRestoreFlow = () => { const handleRestoreFlow = () => {
// TODO: Implement restore flow functionality fetch(`/api/reset-flow/retrieval`, {
console.log('Restore flow confirmed') method: "POST",
} })
.then((response) => response.json())
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Error restoring flow:", error);
});
};
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@ -375,15 +424,13 @@ function KnowledgeSourcesPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-lg">Knowledge Ingest</CardTitle> <CardTitle className="text-lg">Knowledge Ingest</CardTitle>
<CardDescription>Quick ingest options. Edit in Langflow for full control.</CardDescription> <CardDescription>
Quick ingest options. Edit in Langflow for full control.
</CardDescription>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<ConfirmationDialog <ConfirmationDialog
trigger={ trigger={<Button variant="secondary">Restore flow</Button>}
<Button variant="secondary">
Restore flow
</Button>
}
title="Restore default Ingest flow" title="Restore default Ingest flow"
description="This restores defaults and discards all custom settings and overrides. This can't be undone." description="This restores defaults and discards all custom settings and overrides. This can't be undone."
confirmText="Restore" confirmText="Restore"
@ -393,10 +440,25 @@ function KnowledgeSourcesPage() {
<ConfirmationDialog <ConfirmationDialog
trigger={ trigger={
<Button> <Button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="22" viewBox="0 0 24 22" className="h-4 w-4 mr-2"> <svg
<path fill="currentColor" d="M13.0486 0.462158H9.75399C9.44371 0.462158 9.14614 0.586082 8.92674 0.806667L4.03751 5.72232C3.81811 5.9429 3.52054 6.06682 3.21026 6.06682H1.16992C0.511975 6.06682 -0.0165756 6.61212 0.000397655 7.2734L0.0515933 9.26798C0.0679586 9.90556 0.586745 10.4139 1.22111 10.4139H3.59097C3.90124 10.4139 4.19881 10.2899 4.41821 10.0694L9.34823 5.11269C9.56763 4.89211 9.8652 4.76818 10.1755 4.76818H13.0486C13.6947 4.76818 14.2185 4.24157 14.2185 3.59195V1.63839C14.2185 0.988773 13.6947 0.462158 13.0486 0.462158Z"></path> xmlns="http://www.w3.org/2000/svg"
<path fill="currentColor" d="M19.5355 11.5862H22.8301C23.4762 11.5862 24 12.1128 24 12.7624V14.716C24 15.3656 23.4762 15.8922 22.8301 15.8922H19.957C19.6467 15.8922 19.3491 16.0161 19.1297 16.2367L14.1997 21.1934C13.9803 21.414 13.6827 21.5379 13.3725 21.5379H11.0026C10.3682 21.5379 9.84945 21.0296 9.83309 20.392L9.78189 18.3974C9.76492 17.7361 10.2935 17.1908 10.9514 17.1908H12.9918C13.302 17.1908 13.5996 17.0669 13.819 16.8463L18.7082 11.9307C18.9276 11.7101 19.2252 11.5862 19.5355 11.5862Z"></path> width="24"
<path fill="currentColor" d="M19.5355 2.9796L22.8301 2.9796C23.4762 2.9796 24 3.50622 24 4.15583V6.1094C24 6.75901 23.4762 7.28563 22.8301 7.28563H19.957C19.6467 7.28563 19.3491 7.40955 19.1297 7.63014L14.1997 12.5868C13.9803 12.8074 13.6827 12.9313 13.3725 12.9313H10.493C10.1913 12.9313 9.90126 13.0485 9.68346 13.2583L4.14867 18.5917C3.93087 18.8016 3.64085 18.9187 3.33917 18.9187H1.32174C0.675616 18.9187 0.151832 18.3921 0.151832 17.7425V15.7343C0.151832 15.0846 0.675616 14.558 1.32174 14.558H3.32468C3.63496 14.558 3.93253 14.4341 4.15193 14.2135L9.40827 8.92878C9.62767 8.70819 9.92524 8.58427 10.2355 8.58427H12.9918C13.302 8.58427 13.5996 8.46034 13.819 8.23976L18.7082 3.32411C18.9276 3.10353 19.2252 2.9796 19.5355 2.9796Z"></path> height="22"
viewBox="0 0 24 22"
className="h-4 w-4 mr-2"
>
<path
fill="currentColor"
d="M13.0486 0.462158H9.75399C9.44371 0.462158 9.14614 0.586082 8.92674 0.806667L4.03751 5.72232C3.81811 5.9429 3.52054 6.06682 3.21026 6.06682H1.16992C0.511975 6.06682 -0.0165756 6.61212 0.000397655 7.2734L0.0515933 9.26798C0.0679586 9.90556 0.586745 10.4139 1.22111 10.4139H3.59097C3.90124 10.4139 4.19881 10.2899 4.41821 10.0694L9.34823 5.11269C9.56763 4.89211 9.8652 4.76818 10.1755 4.76818H13.0486C13.6947 4.76818 14.2185 4.24157 14.2185 3.59195V1.63839C14.2185 0.988773 13.6947 0.462158 13.0486 0.462158Z"
></path>
<path
fill="currentColor"
d="M19.5355 11.5862H22.8301C23.4762 11.5862 24 12.1128 24 12.7624V14.716C24 15.3656 23.4762 15.8922 22.8301 15.8922H19.957C19.6467 15.8922 19.3491 16.0161 19.1297 16.2367L14.1997 21.1934C13.9803 21.414 13.6827 21.5379 13.3725 21.5379H11.0026C10.3682 21.5379 9.84945 21.0296 9.83309 20.392L9.78189 18.3974C9.76492 17.7361 10.2935 17.1908 10.9514 17.1908H12.9918C13.302 17.1908 13.5996 17.0669 13.819 16.8463L18.7082 11.9307C18.9276 11.7101 19.2252 11.5862 19.5355 11.5862Z"
></path>
<path
fill="currentColor"
d="M19.5355 2.9796L22.8301 2.9796C23.4762 2.9796 24 3.50622 24 4.15583V6.1094C24 6.75901 23.4762 7.28563 22.8301 7.28563H19.957C19.6467 7.28563 19.3491 7.40955 19.1297 7.63014L14.1997 12.5868C13.9803 12.8074 13.6827 12.9313 13.3725 12.9313H10.493C10.1913 12.9313 9.90126 13.0485 9.68346 13.2583L4.14867 18.5917C3.93087 18.8016 3.64085 18.9187 3.33917 18.9187H1.32174C0.675616 18.9187 0.151832 18.3921 0.151832 17.7425V15.7343C0.151832 15.0846 0.675616 14.558 1.32174 14.558H3.32468C3.63496 14.558 3.93253 14.4341 4.15193 14.2135L9.40827 8.92878C9.62767 8.70819 9.92524 8.58427 10.2355 8.58427H12.9918C13.302 8.58427 13.5996 8.46034 13.819 8.23976L18.7082 3.32411C18.9276 3.10353 19.2252 2.9796 19.5355 2.9796Z"
></path>
</svg> </svg>
Edit in Langflow Edit in Langflow
</Button> </Button>
@ -420,25 +482,31 @@ function KnowledgeSourcesPage() {
Extracts text from images/PDFs. Ingest is slower when enabled. Extracts text from images/PDFs. Ingest is slower when enabled.
</div> </div>
</div> </div>
<Switch <Switch
id="ocrEnabled" id="ocrEnabled"
checked={ocrEnabled} checked={ocrEnabled}
onCheckedChange={(checked) => setOcrEnabled(checked)} onCheckedChange={(checked) => setOcrEnabled(checked)}
/> />
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="pictureDescriptions" className="text-base font-medium"> <Label
htmlFor="pictureDescriptions"
className="text-base font-medium"
>
Picture descriptions Picture descriptions
</Label> </Label>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
Adds captions for images. Ingest is more expensive when enabled. Adds captions for images. Ingest is more expensive when
enabled.
</div> </div>
</div> </div>
<Switch <Switch
id="pictureDescriptions" id="pictureDescriptions"
checked={pictureDescriptionsEnabled} checked={pictureDescriptionsEnabled}
onCheckedChange={(checked) => setPictureDescriptionsEnabled(checked)} onCheckedChange={(checked) =>
setPictureDescriptionsEnabled(checked)
}
/> />
</div> </div>
</div> </div>
@ -451,23 +519,45 @@ function KnowledgeSourcesPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-lg">Agent behavior</CardTitle> <CardTitle className="text-lg">Agent behavior</CardTitle>
<CardDescription>Adjust your retrieval agent flow</CardDescription> <CardDescription>
Adjust your retrieval agent flow
</CardDescription>
</div> </div>
<Button <Button
onClick={() => { onClick={() => {
const derivedFromWindow = typeof window !== 'undefined' const derivedFromWindow =
? `${window.location.protocol}//${window.location.hostname}:7860` typeof window !== "undefined"
: '' ? `${window.location.protocol}//${window.location.hostname}:7860`
const base = (publicLangflowUrl || derivedFromWindow || 'http://localhost:7860').replace(/\/$/, '') : "";
const computed = flowId ? `${base}/flow/${flowId}` : base const base = (
const url = langflowEditUrl || computed publicLangflowUrl ||
window.open(url, '_blank') derivedFromWindow ||
"http://localhost:7860"
).replace(/\/$/, "");
const computed = flowId ? `${base}/flow/${flowId}` : base;
const url = langflowEditUrl || computed;
window.open(url, "_blank");
}} }}
> >
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="22" viewBox="0 0 24 22" className="h-4 w-4 mr-2"> <svg
<path fill="currentColor" d="M13.0486 0.462158H9.75399C9.44371 0.462158 9.14614 0.586082 8.92674 0.806667L4.03751 5.72232C3.81811 5.9429 3.52054 6.06682 3.21026 6.06682H1.16992C0.511975 6.06682 -0.0165756 6.61212 0.000397655 7.2734L0.0515933 9.26798C0.0679586 9.90556 0.586745 10.4139 1.22111 10.4139H3.59097C3.90124 10.4139 4.19881 10.2899 4.41821 10.0694L9.34823 5.11269C9.56763 4.89211 9.8652 4.76818 10.1755 4.76818H13.0486C13.6947 4.76818 14.2185 4.24157 14.2185 3.59195V1.63839C14.2185 0.988773 13.6947 0.462158 13.0486 0.462158Z"></path> xmlns="http://www.w3.org/2000/svg"
<path fill="currentColor" d="M19.5355 11.5862H22.8301C23.4762 11.5862 24 12.1128 24 12.7624V14.716C24 15.3656 23.4762 15.8922 22.8301 15.8922H19.957C19.6467 15.8922 19.3491 16.0161 19.1297 16.2367L14.1997 21.1934C13.9803 21.414 13.6827 21.5379 13.3725 21.5379H11.0026C10.3682 21.5379 9.84945 21.0296 9.83309 20.392L9.78189 18.3974C9.76492 17.7361 10.2935 17.1908 10.9514 17.1908H12.9918C13.302 17.1908 13.5996 17.0669 13.819 16.8463L18.7082 11.9307C18.9276 11.7101 19.2252 11.5862 19.5355 11.5862Z"></path> width="24"
<path fill="currentColor" d="M19.5355 2.9796L22.8301 2.9796C23.4762 2.9796 24 3.50622 24 4.15583V6.1094C24 6.75901 23.4762 7.28563 22.8301 7.28563H19.957C19.6467 7.28563 19.3491 7.40955 19.1297 7.63014L14.1997 12.5868C13.9803 12.8074 13.6827 12.9313 13.3725 12.9313H10.493C10.1913 12.9313 9.90126 13.0485 9.68346 13.2583L4.14867 18.5917C3.93087 18.8016 3.64085 18.9187 3.33917 18.9187H1.32174C0.675616 18.9187 0.151832 18.3921 0.151832 17.7425V15.7343C0.151832 15.0846 0.675616 14.558 1.32174 14.558H3.32468C3.63496 14.558 3.93253 14.4341 4.15193 14.2135L9.40827 8.92878C9.62767 8.70819 9.92524 8.58427 10.2355 8.58427H12.9918C13.302 8.58427 13.5996 8.46034 13.819 8.23976L18.7082 3.32411C18.9276 3.10353 19.2252 2.9796 19.5355 2.9796Z"></path> height="22"
viewBox="0 0 24 22"
className="h-4 w-4 mr-2"
>
<path
fill="currentColor"
d="M13.0486 0.462158H9.75399C9.44371 0.462158 9.14614 0.586082 8.92674 0.806667L4.03751 5.72232C3.81811 5.9429 3.52054 6.06682 3.21026 6.06682H1.16992C0.511975 6.06682 -0.0165756 6.61212 0.000397655 7.2734L0.0515933 9.26798C0.0679586 9.90556 0.586745 10.4139 1.22111 10.4139H3.59097C3.90124 10.4139 4.19881 10.2899 4.41821 10.0694L9.34823 5.11269C9.56763 4.89211 9.8652 4.76818 10.1755 4.76818H13.0486C13.6947 4.76818 14.2185 4.24157 14.2185 3.59195V1.63839C14.2185 0.988773 13.6947 0.462158 13.0486 0.462158Z"
></path>
<path
fill="currentColor"
d="M19.5355 11.5862H22.8301C23.4762 11.5862 24 12.1128 24 12.7624V14.716C24 15.3656 23.4762 15.8922 22.8301 15.8922H19.957C19.6467 15.8922 19.3491 16.0161 19.1297 16.2367L14.1997 21.1934C13.9803 21.414 13.6827 21.5379 13.3725 21.5379H11.0026C10.3682 21.5379 9.84945 21.0296 9.83309 20.392L9.78189 18.3974C9.76492 17.7361 10.2935 17.1908 10.9514 17.1908H12.9918C13.302 17.1908 13.5996 17.0669 13.819 16.8463L18.7082 11.9307C18.9276 11.7101 19.2252 11.5862 19.5355 11.5862Z"
></path>
<path
fill="currentColor"
d="M19.5355 2.9796L22.8301 2.9796C23.4762 2.9796 24 3.50622 24 4.15583V6.1094C24 6.75901 23.4762 7.28563 22.8301 7.28563H19.957C19.6467 7.28563 19.3491 7.40955 19.1297 7.63014L14.1997 12.5868C13.9803 12.8074 13.6827 12.9313 13.3725 12.9313H10.493C10.1913 12.9313 9.90126 13.0485 9.68346 13.2583L4.14867 18.5917C3.93087 18.8016 3.64085 18.9187 3.33917 18.9187H1.32174C0.675616 18.9187 0.151832 18.3921 0.151832 17.7425V15.7343C0.151832 15.0846 0.675616 14.558 1.32174 14.558H3.32468C3.63496 14.558 3.93253 14.4341 4.15193 14.2135L9.40827 8.92878C9.62767 8.70819 9.92524 8.58427 10.2355 8.58427H12.9918C13.302 8.58427 13.5996 8.46034 13.819 8.23976L18.7082 3.32411C18.9276 3.10353 19.2252 2.9796 19.5355 2.9796Z"
></path>
</svg> </svg>
Edit in Langflow Edit in Langflow
</Button> </Button>
@ -475,25 +565,30 @@ function KnowledgeSourcesPage() {
</CardHeader> </CardHeader>
</Card> </Card>
{/* Connectors Section */} {/* Connectors Section */}
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h2 className="text-2xl font-semibold tracking-tight mb-2">Cloud Connectors</h2> <h2 className="text-2xl font-semibold tracking-tight mb-2">
Cloud Connectors
</h2>
</div> </div>
{/* Conditional Sync Settings or No-Auth Message */} {/* Conditional Sync Settings or No-Auth Message */}
{isNoAuthMode ? ( {isNoAuthMode ? (
<Card className="border-yellow-500/50 bg-yellow-500/5"> <Card className="border-yellow-500/50 bg-yellow-500/5">
<CardHeader> <CardHeader>
<CardTitle className="text-lg text-yellow-600">Cloud connectors are only available with auth mode enabled</CardTitle> <CardTitle className="text-lg text-yellow-600">
Cloud connectors are only available with auth mode enabled
</CardTitle>
<CardDescription className="text-sm"> <CardDescription className="text-sm">
Please provide the following environment variables and restart: Please provide the following environment variables and restart:
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="bg-muted rounded-md p-4 font-mono text-sm"> <div className="bg-muted rounded-md p-4 font-mono text-sm">
<div className="text-muted-foreground mb-2"># make here https://console.cloud.google.com/apis/credentials</div> <div className="text-muted-foreground mb-2">
# make here https://console.cloud.google.com/apis/credentials
</div>
<div>GOOGLE_OAUTH_CLIENT_ID=</div> <div>GOOGLE_OAUTH_CLIENT_ID=</div>
<div>GOOGLE_OAUTH_CLIENT_SECRET=</div> <div>GOOGLE_OAUTH_CLIENT_SECRET=</div>
</div> </div>
@ -503,27 +598,35 @@ function KnowledgeSourcesPage() {
<div className="flex items-center justify-between py-4"> <div className="flex items-center justify-between py-4">
<div> <div>
<h3 className="text-lg font-medium">Sync Settings</h3> <h3 className="text-lg font-medium">Sync Settings</h3>
<p className="text-sm text-muted-foreground">Configure how many files to sync when manually triggering a sync</p> <p className="text-sm text-muted-foreground">
Configure how many files to sync when manually triggering a sync
</p>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="syncAllFiles" id="syncAllFiles"
checked={syncAllFiles} checked={syncAllFiles}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setSyncAllFiles(!!checked) setSyncAllFiles(!!checked);
if (checked) { if (checked) {
setMaxFiles(0) setMaxFiles(0);
} else { } else {
setMaxFiles(10) setMaxFiles(10);
} }
}} }}
/> />
<Label htmlFor="syncAllFiles" className="font-medium whitespace-nowrap"> <Label
htmlFor="syncAllFiles"
className="font-medium whitespace-nowrap"
>
Sync all files Sync all files
</Label> </Label>
</div> </div>
<Label htmlFor="maxFiles" className="font-medium whitespace-nowrap"> <Label
htmlFor="maxFiles"
className="font-medium whitespace-nowrap"
>
Max files per sync: Max files per sync:
</Label> </Label>
<div className="relative"> <div className="relative">
@ -536,7 +639,11 @@ function KnowledgeSourcesPage() {
className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed" className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
min="1" min="1"
max="100" max="100"
title={syncAllFiles ? "Disabled when 'Sync all files' is checked" : "Leave blank or set to 0 for unlimited"} title={
syncAllFiles
? "Disabled when 'Sync all files' is checked"
: "Leave blank or set to 0 for unlimited"
}
/> />
</div> </div>
</div> </div>
@ -552,7 +659,9 @@ function KnowledgeSourcesPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{connector.icon} {connector.icon}
<div> <div>
<CardTitle className="text-lg">{connector.name}</CardTitle> <CardTitle className="text-lg">
{connector.name}
</CardTitle>
<CardDescription className="text-sm"> <CardDescription className="text-sm">
{connector.description} {connector.description}
</CardDescription> </CardDescription>
@ -582,11 +691,15 @@ function KnowledgeSourcesPage() {
</> </>
)} )}
</Button> </Button>
{syncResults[connector.id] && ( {syncResults[connector.id] && (
<div className="text-xs text-muted-foreground bg-muted/50 p-2 rounded"> <div className="text-xs text-muted-foreground bg-muted/50 p-2 rounded">
<div>Processed: {syncResults[connector.id]?.processed || 0}</div> <div>
<div>Added: {syncResults[connector.id]?.added || 0}</div> Processed: {syncResults[connector.id]?.processed || 0}
</div>
<div>
Added: {syncResults[connector.id]?.added || 0}
</div>
{syncResults[connector.id]?.errors && ( {syncResults[connector.id]?.errors && (
<div>Errors: {syncResults[connector.id]?.errors}</div> <div>Errors: {syncResults[connector.id]?.errors}</div>
)} )}
@ -616,10 +729,9 @@ function KnowledgeSourcesPage() {
</Card> </Card>
))} ))}
</div> </div>
</div> </div>
</div> </div>
) );
} }
export default function ProtectedKnowledgeSourcesPage() { export default function ProtectedKnowledgeSourcesPage() {
@ -629,5 +741,5 @@ export default function ProtectedKnowledgeSourcesPage() {
<KnowledgeSourcesPage /> <KnowledgeSourcesPage />
</Suspense> </Suspense>
</ProtectedRoute> </ProtectedRoute>
) );
} }

66
src/api/flows.py Normal file
View file

@ -0,0 +1,66 @@
"""Reset Flow API endpoints"""
from starlette.requests import Request
from starlette.responses import JSONResponse
from utils.logging_config import get_logger
logger = get_logger(__name__)
async def reset_flow_endpoint(
request: Request,
chat_service,
):
"""Reset a Langflow flow by type (nudges or retrieval)"""
# Get flow type from path parameter
flow_type = request.path_params.get("flow_type")
if flow_type not in ["nudges", "retrieval"]:
return JSONResponse(
{
"success": False,
"error": "Invalid flow type. Must be 'nudges' or 'retrieval'"
},
status_code=400
)
try:
# Get user information from session for logging
# Call the chat service to reset the flow
result = await chat_service.reset_langflow_flow(flow_type)
if result.get("success"):
logger.info(
f"Flow reset successful",
flow_type=flow_type,
flow_id=result.get("flow_id")
)
return JSONResponse(result, status_code=200)
else:
logger.error(
f"Flow reset failed",
flow_type=flow_type,
error=result.get("error")
)
return JSONResponse(result, status_code=500)
except ValueError as e:
logger.error(f"Invalid request for flow reset", error=str(e))
return JSONResponse(
{
"success": False,
"error": str(e)
},
status_code=400
)
except Exception as e:
logger.error(f"Unexpected error in flow reset", error=str(e))
return JSONResponse(
{
"success": False,
"error": f"Internal server error: {str(e)}"
},
status_code=500
)

View file

@ -1,6 +1,6 @@
import sys
# Configure structured logging early # Configure structured logging early
from services.flows_service import FlowsService
from utils.logging_config import configure_from_env, get_logger from utils.logging_config import configure_from_env, get_logger
configure_from_env() configure_from_env()
@ -43,6 +43,7 @@ from auth_middleware import require_auth, optional_auth
# API endpoints # API endpoints
from api import ( from api import (
flows,
nudges, nudges,
upload, upload,
search, search,
@ -274,6 +275,7 @@ async def initialize_services():
search_service = SearchService(session_manager) search_service = SearchService(session_manager)
task_service = TaskService(document_service, process_pool) task_service = TaskService(document_service, process_pool)
chat_service = ChatService() chat_service = ChatService()
flows_service = FlowsService()
knowledge_filter_service = KnowledgeFilterService(session_manager) knowledge_filter_service = KnowledgeFilterService(session_manager)
monitor_service = MonitorService(session_manager) monitor_service = MonitorService(session_manager)
@ -318,6 +320,7 @@ async def initialize_services():
"search_service": search_service, "search_service": search_service,
"task_service": task_service, "task_service": task_service,
"chat_service": chat_service, "chat_service": chat_service,
"flows_service": flows_service,
"auth_service": auth_service, "auth_service": auth_service,
"connector_service": connector_service, "connector_service": connector_service,
"knowledge_filter_service": knowledge_filter_service, "knowledge_filter_service": knowledge_filter_service,
@ -727,6 +730,17 @@ async def create_app():
), ),
methods=["GET"], methods=["GET"],
), ),
# Reset Flow endpoint
Route(
"/reset-flow/{flow_type}",
require_auth(services["session_manager"])(
partial(
flows.reset_flow_endpoint,
chat_service=services["flows_service"],
)
),
methods=["POST"],
),
] ]
app = Starlette(debug=True, routes=routes) app = Starlette(debug=True, routes=routes)

View file

@ -0,0 +1,121 @@
from config.settings import NUDGES_FLOW_ID, LANGFLOW_URL, FLOW_ID
import json
import os
import aiohttp
from utils.logging_config import get_logger
logger = get_logger(__name__)
class FlowsService:
async def reset_langflow_flow(self, flow_type: str):
"""Reset a Langflow flow by uploading the corresponding JSON file
Args:
flow_type: Either 'nudges' or 'retrieval'
Returns:
dict: Success/error response
"""
if not LANGFLOW_URL:
raise ValueError("LANGFLOW_URL environment variable is required")
# Determine flow file and ID based on type
if flow_type == "nudges":
flow_file = "flows/openrag_nudges.json"
flow_id = NUDGES_FLOW_ID
elif flow_type == "retrieval":
flow_file = "flows/openrag_agent.json"
flow_id = FLOW_ID
else:
raise ValueError("flow_type must be either 'nudges' or 'retrieval'")
# Load flow JSON file
try:
# Get the project root directory (go up from src/services/ to project root)
# __file__ is src/services/chat_service.py
# os.path.dirname(__file__) is src/services/
# os.path.dirname(os.path.dirname(__file__)) is src/
# os.path.dirname(os.path.dirname(os.path.dirname(__file__))) is project root
current_file_dir = os.path.dirname(os.path.abspath(__file__)) # src/services/
src_dir = os.path.dirname(current_file_dir) # src/
project_root = os.path.dirname(src_dir) # project root
flow_path = os.path.join(project_root, flow_file)
if not os.path.exists(flow_path):
# List contents of project root to help debug
try:
contents = os.listdir(project_root)
logger.info(f"Project root contents: {contents}")
flows_dir = os.path.join(project_root, "flows")
if os.path.exists(flows_dir):
flows_contents = os.listdir(flows_dir)
logger.info(f"Flows directory contents: {flows_contents}")
else:
logger.info("Flows directory does not exist")
except Exception as e:
logger.error(f"Error listing directory contents: {e}")
raise FileNotFoundError(f"Flow file not found at: {flow_path}")
with open(flow_path, 'r') as f:
flow_data = json.load(f)
logger.info(f"Successfully loaded flow data from {flow_file}")
except FileNotFoundError:
raise ValueError(f"Flow file not found: {flow_path}")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in flow file {flow_file}: {e}")
# Get API key for Langflow
from config.settings import LANGFLOW_KEY
if not LANGFLOW_KEY:
raise ValueError("LANGFLOW_KEY is required for flow reset")
# Make PATCH request to Langflow API to update the flow
url = f"{LANGFLOW_URL}/api/v1/flows/{flow_id}"
headers = {
"x-api-key": LANGFLOW_KEY,
"Content-Type": "application/json"
}
try:
async with aiohttp.ClientSession() as session:
async with session.patch(url, json=flow_data, headers=headers) as response:
if response.status == 200:
result = await response.json()
logger.info(
f"Successfully reset {flow_type} flow",
flow_id=flow_id,
flow_file=flow_file
)
return {
"success": True,
"message": f"Successfully reset {flow_type} flow",
"flow_id": flow_id,
"flow_type": flow_type
}
else:
error_text = await response.text()
logger.error(
f"Failed to reset {flow_type} flow",
status_code=response.status,
error=error_text
)
return {
"success": False,
"error": f"Failed to reset flow: HTTP {response.status} - {error_text}"
}
except aiohttp.ClientError as e:
logger.error(f"Network error while resetting {flow_type} flow", error=str(e))
return {
"success": False,
"error": f"Network error: {str(e)}"
}
except Exception as e:
logger.error(f"Unexpected error while resetting {flow_type} flow", error=str(e))
return {
"success": False,
"error": f"Unexpected error: {str(e)}"
}