Merge pull request #26 from langflow-ai/ingest-settings
Add ingest settings
This commit is contained in:
commit
ab10e01286
11 changed files with 1172 additions and 720 deletions
|
|
@ -40,8 +40,9 @@ PY
|
||||||
|
|
||||||
#ENV EASYOCR_MODULE_PATH=~/.cache/docling/models/EasyOcr/
|
#ENV EASYOCR_MODULE_PATH=~/.cache/docling/models/EasyOcr/
|
||||||
|
|
||||||
# Copy Python source
|
# Copy Python source and flows
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
|
COPY flows/ ./flows/
|
||||||
|
|
||||||
# Expose backend port
|
# Expose backend port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./documents:/app/documents:Z
|
- ./documents:/app/documents:Z
|
||||||
- ./keys:/app/keys:Z
|
- ./keys:/app/keys:Z
|
||||||
|
- ./flows:/app/flows:Z
|
||||||
|
|
||||||
openrag-frontend:
|
openrag-frontend:
|
||||||
image: phact/openrag-frontend:${OPENRAG_VERSION:-latest}
|
image: phact/openrag-frontend:${OPENRAG_VERSION:-latest}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./documents:/app/documents:Z
|
- ./documents:/app/documents:Z
|
||||||
- ./keys:/app/keys:Z
|
- ./keys:/app/keys:Z
|
||||||
|
- ./flows:/app/flows:Z
|
||||||
gpus: all
|
gpus: all
|
||||||
|
|
||||||
openrag-frontend:
|
openrag-frontend:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, Suspense } from "react";
|
import { Loader2, PlugZap, RefreshCw } from "lucide-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -10,15 +14,30 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
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 { useAuth } from "@/contexts/auth-context";
|
||||||
|
import { useTask } from "@/contexts/task-context";
|
||||||
|
|
||||||
|
interface GoogleDriveFile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
webViewLink?: string;
|
||||||
|
iconLink?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OneDriveFile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mimeType?: string;
|
||||||
|
webUrl?: string;
|
||||||
|
driveItem?: {
|
||||||
|
file?: { mimeType: string };
|
||||||
|
folder?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface Connector {
|
interface Connector {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -29,6 +48,7 @@ interface Connector {
|
||||||
type: string;
|
type: string;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
access_token?: string;
|
access_token?: string;
|
||||||
|
selectedFiles?: GoogleDriveFile[] | OneDriveFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncResult {
|
interface SyncResult {
|
||||||
|
|
@ -63,22 +83,16 @@ function KnowledgeSourcesPage() {
|
||||||
|
|
||||||
// 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>(
|
const [chatFlowId, setChatFlowId] = useState<string>(
|
||||||
"1098eea1-6649-4e1d-aed1-b77249fb8dd0",
|
"1098eea1-6649-4e1d-aed1-b77249fb8dd0",
|
||||||
);
|
);
|
||||||
const [ingestFlowId, setIngestFlowId] = useState<string>("");
|
const [ingestFlowId, setIngestFlowId] = useState<string>(
|
||||||
|
"5488df7c-b93f-4f87-a446-b67028bc0813",
|
||||||
|
);
|
||||||
const [langflowEditUrl, setLangflowEditUrl] = useState<string>("");
|
const [langflowEditUrl, setLangflowEditUrl] = useState<string>("");
|
||||||
const [langflowIngestEditUrl, setLangflowIngestEditUrl] =
|
const [langflowIngestEditUrl, setLangflowIngestEditUrl] = useState<string>("");
|
||||||
useState<string>("");
|
|
||||||
const [publicLangflowUrl, setPublicLangflowUrl] = useState<string>("");
|
const [publicLangflowUrl, setPublicLangflowUrl] = useState<string>("");
|
||||||
|
|
||||||
// Ingestion settings state - will be populated from Langflow flow defaults
|
|
||||||
const [ingestionSettings, setIngestionSettings] = useState({
|
|
||||||
chunkSize: 1000,
|
|
||||||
chunkOverlap: 200,
|
|
||||||
separator: "\\n",
|
|
||||||
embeddingModel: "text-embedding-3-small",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch settings from backend
|
// Fetch settings from backend
|
||||||
const fetchSettings = useCallback(async () => {
|
const fetchSettings = useCallback(async () => {
|
||||||
|
|
@ -86,18 +100,20 @@ function KnowledgeSourcesPage() {
|
||||||
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();
|
||||||
// Update all state cleanly
|
if (settings.flow_id) {
|
||||||
if (settings.flow_id) setFlowId(settings.flow_id);
|
setChatFlowId(settings.flow_id);
|
||||||
if (settings.ingest_flow_id) setIngestFlowId(settings.ingest_flow_id);
|
}
|
||||||
if (settings.langflow_edit_url) setLangflowEditUrl(settings.langflow_edit_url);
|
if (settings.ingest_flow_id) {
|
||||||
if (settings.langflow_ingest_edit_url) setLangflowIngestEditUrl(settings.langflow_ingest_edit_url);
|
setIngestFlowId(settings.ingest_flow_id);
|
||||||
if (settings.langflow_public_url) setPublicLangflowUrl(settings.langflow_public_url);
|
}
|
||||||
if (settings.ingestion_defaults) {
|
if (settings.langflow_edit_url) {
|
||||||
console.log(
|
setLangflowEditUrl(settings.langflow_edit_url);
|
||||||
"Loading ingestion defaults from backend:",
|
}
|
||||||
settings.ingestion_defaults,
|
if (settings.langflow_ingest_edit_url) {
|
||||||
);
|
setLangflowIngestEditUrl(settings.langflow_ingest_edit_url);
|
||||||
setIngestionSettings(settings.ingestion_defaults);
|
}
|
||||||
|
if (settings.langflow_public_url) {
|
||||||
|
setPublicLangflowUrl(settings.langflow_public_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -222,7 +238,9 @@ function KnowledgeSourcesPage() {
|
||||||
`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}`;
|
||||||
|
|
@ -246,15 +264,23 @@ function KnowledgeSourcesPage() {
|
||||||
setSyncResults((prev) => ({ ...prev, [connector.id]: null }));
|
setSyncResults((prev) => ({ ...prev, [connector.id]: null }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const syncBody: {
|
||||||
|
connection_id: string;
|
||||||
|
max_files?: number;
|
||||||
|
selected_files?: string[];
|
||||||
|
} = {
|
||||||
|
connection_id: connector.connectionId,
|
||||||
|
max_files: syncAllFiles ? 0 : maxFiles || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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({
|
body: JSON.stringify(syncBody),
|
||||||
connection_id: connector.connectionId,
|
|
||||||
max_files: syncAllFiles ? 0 : maxFiles || undefined,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
@ -367,32 +393,176 @@ function KnowledgeSourcesPage() {
|
||||||
}
|
}
|
||||||
}, [tasks, prevTasks]);
|
}, [tasks, prevTasks]);
|
||||||
|
|
||||||
|
const handleEditInLangflow = (flowType: "chat" | "ingest", closeDialog: () => void) => {
|
||||||
|
// Select the appropriate flow ID and edit URL based on flow type
|
||||||
|
const targetFlowId = flowType === "ingest" ? ingestFlowId : chatFlowId;
|
||||||
|
const editUrl = flowType === "ingest" ? langflowIngestEditUrl : langflowEditUrl;
|
||||||
|
|
||||||
|
const derivedFromWindow =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? `${window.location.protocol}//${window.location.hostname}:7860`
|
||||||
|
: "";
|
||||||
|
const base = (
|
||||||
|
publicLangflowUrl ||
|
||||||
|
derivedFromWindow ||
|
||||||
|
"http://localhost:7860"
|
||||||
|
).replace(/\/$/, "");
|
||||||
|
const computed = targetFlowId ? `${base}/flow/${targetFlowId}` : base;
|
||||||
|
|
||||||
|
const url = editUrl || computed;
|
||||||
|
|
||||||
|
window.open(url, "_blank");
|
||||||
|
closeDialog(); // Close immediately after opening Langflow
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestoreRetrievalFlow = (closeDialog: () => void) => {
|
||||||
|
fetch(`/api/reset-flow/retrieval`, {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then(() => {
|
||||||
|
closeDialog(); // Close after successful completion
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error restoring retrieval flow:", error);
|
||||||
|
closeDialog(); // Close even on error (could show error toast instead)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestoreIngestFlow = (closeDialog: () => void) => {
|
||||||
|
fetch(`/api/reset-flow/ingest`, {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then(() => {
|
||||||
|
closeDialog(); // Close after successful completion
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error restoring ingest flow:", error);
|
||||||
|
closeDialog(); // Close even on error (could show error toast instead)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
{/* Knowledge Ingest Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg">Knowledge Ingest</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Quick ingest options. Edit in Langflow for full control.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<ConfirmationDialog
|
||||||
|
trigger={<Button variant="secondary">Restore flow</Button>}
|
||||||
|
title="Restore default Ingest flow"
|
||||||
|
description="This restores defaults and discards all custom settings and overrides. This can't be undone."
|
||||||
|
confirmText="Restore"
|
||||||
|
variant="destructive"
|
||||||
|
onConfirm={handleRestoreIngestFlow}
|
||||||
|
/>
|
||||||
|
<ConfirmationDialog
|
||||||
|
trigger={
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
Edit in Langflow
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title="Edit Ingest flow in Langflow"
|
||||||
|
description="You're entering Langflow. You can edit the Ingest flow and other underlying flows. Manual changes to components, wiring, or I/O can break this experience."
|
||||||
|
confirmText="Proceed"
|
||||||
|
onConfirm={(closeDialog) => handleEditInLangflow("ingest", closeDialog)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
{/* Hidden for now */}
|
||||||
|
{/* <CardContent>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="ocrEnabled" className="text-base font-medium">
|
||||||
|
OCR
|
||||||
|
</Label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Extracts text from images/PDFs. Ingest is slower when enabled.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="ocrEnabled"
|
||||||
|
checked={ocrEnabled}
|
||||||
|
onCheckedChange={(checked) => setOcrEnabled(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label
|
||||||
|
htmlFor="pictureDescriptions"
|
||||||
|
className="text-base font-medium"
|
||||||
|
>
|
||||||
|
Picture descriptions
|
||||||
|
</Label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Adds captions for images. Ingest is more expensive when
|
||||||
|
enabled.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="pictureDescriptions"
|
||||||
|
checked={pictureDescriptionsEnabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setPictureDescriptionsEnabled(checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent> */}
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Agent Behavior Section */}
|
{/* Agent Behavior Section */}
|
||||||
<div className="flex items-center justify-between py-4">
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Agent behavior</h3>
|
<CardTitle className="text-lg">Agent behavior</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground">
|
<CardDescription>
|
||||||
Adjust your retrieval agent flow
|
Adjust your retrieval agent flow
|
||||||
</p>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
onClick={() => {
|
<ConfirmationDialog
|
||||||
const derivedFromWindow =
|
trigger={<Button variant="secondary">Restore flow</Button>}
|
||||||
typeof window !== "undefined"
|
title="Restore default Agent flow"
|
||||||
? `${window.location.protocol}//${window.location.hostname}:7860`
|
description="This restores defaults and discards all custom settings and overrides. This can't be undone."
|
||||||
: "";
|
confirmText="Restore"
|
||||||
const base = (
|
variant="destructive"
|
||||||
publicLangflowUrl ||
|
onConfirm={handleRestoreRetrievalFlow}
|
||||||
derivedFromWindow ||
|
/>
|
||||||
"http://localhost:7860"
|
<ConfirmationDialog
|
||||||
).replace(/\/$/, "");
|
trigger={
|
||||||
const computed = flowId ? `${base}/flow/${flowId}` : base;
|
<Button>
|
||||||
const url = langflowEditUrl || computed;
|
|
||||||
window.open(url, "_blank");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
|
|
@ -415,154 +585,16 @@ function KnowledgeSourcesPage() {
|
||||||
</svg>
|
</svg>
|
||||||
Edit in Langflow
|
Edit in Langflow
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Ingest Flow Section */}
|
|
||||||
<div className="flex items-center justify-between py-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium">File ingestion</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Customize your file processing and indexing flow
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
const derivedFromWindow =
|
|
||||||
typeof window !== "undefined"
|
|
||||||
? `${window.location.protocol}//${window.location.hostname}:7860`
|
|
||||||
: "";
|
|
||||||
const base = (
|
|
||||||
publicLangflowUrl ||
|
|
||||||
derivedFromWindow ||
|
|
||||||
"http://localhost:7860"
|
|
||||||
).replace(/\/$/, "");
|
|
||||||
const computed = ingestFlowId
|
|
||||||
? `${base}/flow/${ingestFlowId}`
|
|
||||||
: base;
|
|
||||||
const url = langflowIngestEditUrl || 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"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
Edit in Langflow
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Ingestion Settings Section */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium">Ingestion settings</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Configure how your documents are processed and indexed
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">Document Processing</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Control how text is split and processed
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="chunkSize">Chunk Size</Label>
|
|
||||||
<Input
|
|
||||||
id="chunkSize"
|
|
||||||
type="number"
|
|
||||||
value={ingestionSettings.chunkSize}
|
|
||||||
onChange={(e) =>
|
|
||||||
setIngestionSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
chunkSize: parseInt(e.target.value) || 1000,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
min="100"
|
title="Edit Agent flow in Langflow"
|
||||||
max="4000"
|
description="You're entering Langflow. You can edit the Agent flow and other underlying flows. Manual changes to components, wiring, or I/O can break this experience."
|
||||||
|
confirmText="Proceed"
|
||||||
|
onConfirm={(closeDialog) => handleEditInLangflow("chat", closeDialog)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Maximum characters per text chunk (100-4000)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="chunkOverlap">Chunk Overlap</Label>
|
|
||||||
<Input
|
|
||||||
id="chunkOverlap"
|
|
||||||
type="number"
|
|
||||||
value={ingestionSettings.chunkOverlap}
|
|
||||||
onChange={(e) =>
|
|
||||||
setIngestionSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
chunkOverlap: parseInt(e.target.value) || 200,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
min="0"
|
|
||||||
max="500"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Character overlap between chunks (0-500)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">Embeddings</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Configure embedding model and search behavior
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="embeddingModel">Embedding Model</Label>
|
|
||||||
<select
|
|
||||||
id="embeddingModel"
|
|
||||||
value={ingestionSettings.embeddingModel}
|
|
||||||
onChange={(e) =>
|
|
||||||
setIngestionSettings((prev) => ({
|
|
||||||
...prev,
|
|
||||||
embeddingModel: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md text-sm"
|
|
||||||
>
|
|
||||||
<option value="text-embedding-3-small">
|
|
||||||
text-embedding-3-small (fast, cheaper)
|
|
||||||
</option>
|
|
||||||
<option value="text-embedding-3-large">
|
|
||||||
text-embedding-3-large (better quality)
|
|
||||||
</option>
|
|
||||||
<option value="text-embedding-ada-002">
|
|
||||||
text-embedding-ada-002 (legacy)
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connectors Section */}
|
{/* Connectors Section */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
|
||||||
77
frontend/src/components/confirmation-dialog.tsx
Normal file
77
frontend/src/components/confirmation-dialog.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { ReactNode, useState } from "react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
interface ConfirmationDialogProps {
|
||||||
|
trigger: ReactNode
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
confirmText?: string
|
||||||
|
cancelText?: string
|
||||||
|
onConfirm: (closeDialog: () => void) => void
|
||||||
|
onCancel?: () => void
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmationDialog({
|
||||||
|
trigger,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
confirmText = "Continue",
|
||||||
|
cancelText = "Cancel",
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
variant = "default"
|
||||||
|
}: ConfirmationDialogProps) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const closeDialog = () => setOpen(false)
|
||||||
|
onConfirm(closeDialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onCancel?.()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{trigger}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="mb-4">{title}</DialogTitle>
|
||||||
|
<DialogDescription className="text-left">
|
||||||
|
{description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={variant}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
122
frontend/src/components/ui/dialog.tsx
Normal file
122
frontend/src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
||||||
29
frontend/src/components/ui/switch.tsx
Normal file
29
frontend/src/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
66
src/api/flows.py
Normal file
66
src/api/flows.py
Normal 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, retrieval, or ingest)"""
|
||||||
|
|
||||||
|
# Get flow type from path parameter
|
||||||
|
flow_type = request.path_params.get("flow_type")
|
||||||
|
|
||||||
|
if flow_type not in ["nudges", "retrieval", "ingest"]:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": "Invalid flow type. Must be 'nudges', 'retrieval', or 'ingest'"
|
||||||
|
},
|
||||||
|
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
|
||||||
|
)
|
||||||
61
src/main.py
61
src/main.py
|
|
@ -1,8 +1,7 @@
|
||||||
import sys
|
|
||||||
|
|
||||||
# Configure structured logging early
|
# Configure structured logging early
|
||||||
from connectors.langflow_connector_service import LangflowConnectorService
|
from connectors.langflow_connector_service import LangflowConnectorService
|
||||||
from connectors.service import ConnectorService
|
from connectors.service import ConnectorService
|
||||||
|
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()
|
||||||
|
|
@ -23,24 +22,28 @@ from starlette.routing import Route
|
||||||
multiprocessing.set_start_method("spawn", force=True)
|
multiprocessing.set_start_method("spawn", force=True)
|
||||||
|
|
||||||
# Create process pool FIRST, before any torch/CUDA imports
|
# Create process pool FIRST, before any torch/CUDA imports
|
||||||
from utils.process_pool import process_pool
|
from utils.process_pool import process_pool # isort: skip
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
from api import (
|
from api import (
|
||||||
router,
|
|
||||||
auth,
|
auth,
|
||||||
chat,
|
chat,
|
||||||
connectors,
|
connectors,
|
||||||
|
flows,
|
||||||
knowledge_filter,
|
knowledge_filter,
|
||||||
langflow_files,
|
langflow_files,
|
||||||
|
nudges,
|
||||||
oidc,
|
oidc,
|
||||||
|
router,
|
||||||
search,
|
search,
|
||||||
settings,
|
settings,
|
||||||
tasks,
|
tasks,
|
||||||
upload,
|
upload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Existing services
|
||||||
|
from api.connector_router import ConnectorRouter
|
||||||
from auth_middleware import optional_auth, require_auth
|
from auth_middleware import optional_auth, require_auth
|
||||||
|
|
||||||
# Configuration and setup
|
# Configuration and setup
|
||||||
|
|
@ -53,9 +56,6 @@ from config.settings import (
|
||||||
clients,
|
clients,
|
||||||
is_no_auth_mode,
|
is_no_auth_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Existing services
|
|
||||||
from api.connector_router import ConnectorRouter
|
|
||||||
from services.auth_service import AuthService
|
from services.auth_service import AuthService
|
||||||
from services.chat_service import ChatService
|
from services.chat_service import ChatService
|
||||||
|
|
||||||
|
|
@ -70,23 +70,6 @@ from services.monitor_service import MonitorService
|
||||||
from services.search_service import SearchService
|
from services.search_service import SearchService
|
||||||
from services.task_service import TaskService
|
from services.task_service import TaskService
|
||||||
from session_manager import SessionManager
|
from session_manager import SessionManager
|
||||||
from utils.process_pool import process_pool
|
|
||||||
|
|
||||||
# API endpoints
|
|
||||||
from api import (
|
|
||||||
router,
|
|
||||||
nudges,
|
|
||||||
upload,
|
|
||||||
search,
|
|
||||||
chat,
|
|
||||||
auth,
|
|
||||||
connectors,
|
|
||||||
tasks,
|
|
||||||
oidc,
|
|
||||||
knowledge_filter,
|
|
||||||
settings,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"CUDA device information",
|
"CUDA device information",
|
||||||
|
|
@ -245,7 +228,10 @@ async def init_index_when_ready():
|
||||||
async def ingest_default_documents_when_ready(services):
|
async def ingest_default_documents_when_ready(services):
|
||||||
"""Scan the local documents folder and ingest files like a non-auth upload."""
|
"""Scan the local documents folder and ingest files like a non-auth upload."""
|
||||||
try:
|
try:
|
||||||
logger.info("Ingesting default documents when ready", disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW)
|
logger.info(
|
||||||
|
"Ingesting default documents when ready",
|
||||||
|
disable_langflow_ingest=DISABLE_INGEST_WITH_LANGFLOW,
|
||||||
|
)
|
||||||
base_dir = os.path.abspath(os.path.join(os.getcwd(), "documents"))
|
base_dir = os.path.abspath(os.path.join(os.getcwd(), "documents"))
|
||||||
if not os.path.isdir(base_dir):
|
if not os.path.isdir(base_dir):
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -294,7 +280,7 @@ async def _ingest_default_documents_langflow(services, file_paths):
|
||||||
logger.debug("Processing file with Langflow pipeline", file_path=file_path)
|
logger.debug("Processing file with Langflow pipeline", file_path=file_path)
|
||||||
|
|
||||||
# Read file content
|
# Read file content
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, "rb") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Create file tuple for upload
|
# Create file tuple for upload
|
||||||
|
|
@ -302,12 +288,13 @@ async def _ingest_default_documents_langflow(services, file_paths):
|
||||||
# Determine content type based on file extension
|
# Determine content type based on file extension
|
||||||
content_type, _ = mimetypes.guess_type(filename)
|
content_type, _ = mimetypes.guess_type(filename)
|
||||||
if not content_type:
|
if not content_type:
|
||||||
content_type = 'application/octet-stream'
|
content_type = "application/octet-stream"
|
||||||
|
|
||||||
file_tuple = (filename, content, content_type)
|
file_tuple = (filename, content, content_type)
|
||||||
|
|
||||||
# Use AnonymousUser details for default documents
|
# Use AnonymousUser details for default documents
|
||||||
from session_manager import AnonymousUser
|
from session_manager import AnonymousUser
|
||||||
|
|
||||||
anonymous_user = AnonymousUser()
|
anonymous_user = AnonymousUser()
|
||||||
|
|
||||||
# Get JWT token using same logic as DocumentFileProcessor
|
# Get JWT token using same logic as DocumentFileProcessor
|
||||||
|
|
@ -321,7 +308,7 @@ async def _ingest_default_documents_langflow(services, file_paths):
|
||||||
anonymous_user.user_id, effective_jwt
|
anonymous_user.user_id, effective_jwt
|
||||||
)
|
)
|
||||||
# Get the JWT that was created by session manager
|
# Get the JWT that was created by session manager
|
||||||
if hasattr(session_manager, '_anonymous_jwt'):
|
if hasattr(session_manager, "_anonymous_jwt"):
|
||||||
effective_jwt = session_manager._anonymous_jwt
|
effective_jwt = session_manager._anonymous_jwt
|
||||||
|
|
||||||
# Prepare tweaks for default documents with anonymous user metadata
|
# Prepare tweaks for default documents with anonymous user metadata
|
||||||
|
|
@ -331,7 +318,7 @@ async def _ingest_default_documents_langflow(services, file_paths):
|
||||||
{"key": "owner", "value": None},
|
{"key": "owner", "value": None},
|
||||||
{"key": "owner_name", "value": anonymous_user.name},
|
{"key": "owner_name", "value": anonymous_user.name},
|
||||||
{"key": "owner_email", "value": anonymous_user.email},
|
{"key": "owner_email", "value": anonymous_user.email},
|
||||||
{"key": "connector_type", "value": "system_default"}
|
{"key": "connector_type", "value": "system_default"},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -420,6 +407,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)
|
||||||
|
|
||||||
|
|
@ -445,7 +433,7 @@ async def initialize_services():
|
||||||
# Create connector router that chooses based on configuration
|
# Create connector router that chooses based on configuration
|
||||||
connector_service = ConnectorRouter(
|
connector_service = ConnectorRouter(
|
||||||
langflow_connector_service=langflow_connector_service,
|
langflow_connector_service=langflow_connector_service,
|
||||||
openrag_connector_service=openrag_connector_service
|
openrag_connector_service=openrag_connector_service,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize auth service
|
# Initialize auth service
|
||||||
|
|
@ -477,6 +465,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,
|
||||||
"langflow_file_service": langflow_file_service,
|
"langflow_file_service": langflow_file_service,
|
||||||
"auth_service": auth_service,
|
"auth_service": auth_service,
|
||||||
"connector_service": connector_service,
|
"connector_service": connector_service,
|
||||||
|
|
@ -933,6 +922,16 @@ async def create_app():
|
||||||
),
|
),
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
),
|
),
|
||||||
|
Route(
|
||||||
|
"/reset-flow/{flow_type}",
|
||||||
|
require_auth(services["session_manager"])(
|
||||||
|
partial(
|
||||||
|
flows.reset_flow_endpoint,
|
||||||
|
chat_service=services["flows_service"],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
methods=["POST"],
|
||||||
|
),
|
||||||
Route(
|
Route(
|
||||||
"/router/upload_ingest",
|
"/router/upload_ingest",
|
||||||
require_auth(services["session_manager"])(
|
require_auth(services["session_manager"])(
|
||||||
|
|
|
||||||
124
src/services/flows_service.py
Normal file
124
src/services/flows_service.py
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
from config.settings import NUDGES_FLOW_ID, LANGFLOW_URL, LANGFLOW_CHAT_FLOW_ID, LANGFLOW_INGEST_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', 'retrieval', or 'ingest'
|
||||||
|
|
||||||
|
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 = LANGFLOW_CHAT_FLOW_ID
|
||||||
|
elif flow_type == "ingest":
|
||||||
|
flow_file = "flows/ingestion_flow.json"
|
||||||
|
flow_id = LANGFLOW_INGEST_FLOW_ID
|
||||||
|
else:
|
||||||
|
raise ValueError("flow_type must be either 'nudges', 'retrieval', or 'ingest'")
|
||||||
|
|
||||||
|
# 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)}"
|
||||||
|
}
|
||||||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -1405,7 +1405,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openrag"
|
name = "openrag"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agentd" },
|
{ name = "agentd" },
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue