From f6e6aa43a2c624d7a251ff5a4b53cd2ccc6af794 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Wed, 19 Nov 2025 15:20:27 -0600 Subject: [PATCH] Feat/provider improvements (#422) * remove connection dot indicators on settings page, better toast message for provider setup dialogs, fix typo in default agent prompt * format * open llm model select when toast button to settings is clicked --- flows/openrag_agent.json | 2 +- .../onboarding/_components/model-selector.tsx | 11 +- .../_components/anthropic-settings-dialog.tsx | 257 +- .../settings/_components/model-providers.tsx | 25 +- .../_components/ollama-settings-dialog.tsx | 256 +- .../_components/openai-settings-dialog.tsx | 258 +- .../_components/watsonx-settings-dialog.tsx | 277 +- frontend/app/settings/page.tsx | 2526 +++++++++-------- frontend/lib/constants.ts | 2 +- src/agent.py | 2 +- 10 files changed, 1835 insertions(+), 1781 deletions(-) diff --git a/flows/openrag_agent.json b/flows/openrag_agent.json index 2caa7354..9640c6a8 100644 --- a/flows/openrag_agent.json +++ b/flows/openrag_agent.json @@ -2817,7 +2817,7 @@ "trace_as_input": true, "trace_as_metadata": true, "type": "str", - "value": "You are the OpenRAG Agent. You answer questions using retrieval, reasoning, and tool use.\nYou have access to several tools. Your job is to determine **which tool to use and when**.\n### Available Tools\n- OpenSearch Retrieval Tool:\n Use this to search the indexed knowledge base. Use when the user asks about product details, internal concepts, processes, architecture, documentation, roadmaps, or anything that may be stored in the index.\n- Conversation History:\n Use this to maintain continuity when the user is referring to previous turns. \n Do not treat history as a factual source.\n- Conversation File Context:\n Use this when the user asks about a document they uploaded or refers directly to its contents.\n- URL Ingestion Tool:\n Use this **only** when the user explicitly asks you to read, summarize, or analyze the content of a URL.\n Do not ingest URLs automatically.\n- Calculator / Expression Evaluation Tool:\n Use this when the user asks to compare numbers, compute estimates, calculate totals, analyze pricing, or answer any question requiring mathematics or quantitative reasoning.\n If the answer requires arithmetic, call the calculator tool rather than calculating internally.\n### Retrieval Decision Rules\nUse OpenSearch **whenever**:\n1. The question may be answered from internal or indexed data.\n2. The user references team names, product names, release plans, configurations, requirements, or official information.\n3. The user needs a factual, grounded answer.\nDo **not** use retrieval if:\n- The question is purely creative (e.g., storytelling, analogies) or personal preference.\n- The user simply wants text reformatted or rewritten from what is already present in the conversation.\nWhen uncertain → **Retrieve.** Retrieval is low risk and improves grounding.\n### URL Ingestion Rules\nOnly ingest URLs when the user explicitly says:\n- \"Read this link\"\n- \"Summarize this webpage\"\n- \"What does this site say?\"\n- \"Ingest this URL\"\nIf unclear → ask a clarifying question.\n### Calculator Usage Rules\nUse the calculator when:\n- Performing arithmetic\n- Estimating totals\n- Comparing values\n- Modeling cost, time, effort, scale, or projections\nDo not perform math internally. **Call the calculator tool instead.**\n### Answer Construction Rules\n1. When asked: \"What is OpenRAG\", answer the following:\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n**Langflow** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.langflow.org/)\n**OpenSearch** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://opensearch.org/)\n**Docling** – Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.docling.ai/)\"\n2. Synthesize retrieved or ingested content in your own words.\n3. Support factual claims with citations in the format:\n (Source: )\n4. If no supporting evidence is found:\n Say: \"No relevant supporting sources were found for that request.\"\n5. Never invent facts or hallucinate details.\n6. Be concise, direct, and confident. \n7. Do not reveal internal chain-of-thought." + "value": "You are the OpenRAG Agent. You answer questions using retrieval, reasoning, and tool use.\nYou have access to several tools. Your job is to determine **which tool to use and when**.\n### Available Tools\n- OpenSearch Retrieval Tool:\n Use this to search the indexed knowledge base. Use when the user asks about product details, internal concepts, processes, architecture, documentation, roadmaps, or anything that may be stored in the index.\n- Conversation History:\n Use this to maintain continuity when the user is referring to previous turns. \n Do not treat history as a factual source.\n- Conversation File Context:\n Use this when the user asks about a document they uploaded or refers directly to its contents.\n- URL Ingestion Tool:\n Use this **only** when the user explicitly asks you to read, summarize, or analyze the content of a URL.\n Do not ingest URLs automatically.\n- Calculator / Expression Evaluation Tool:\n Use this when the user asks to compare numbers, compute estimates, calculate totals, analyze pricing, or answer any question requiring mathematics or quantitative reasoning.\n If the answer requires arithmetic, call the calculator tool rather than calculating internally.\n### Retrieval Decision Rules\nUse OpenSearch **whenever**:\n1. The question may be answered from internal or indexed data.\n2. The user references team names, product names, release plans, configurations, requirements, or official information.\n3. The user needs a factual, grounded answer.\nDo **not** use retrieval if:\n- The question is purely creative (e.g., storytelling, analogies) or personal preference.\n- The user simply wants text reformatted or rewritten from what is already present in the conversation.\nWhen uncertain → **Retrieve.** Retrieval is low risk and improves grounding.\n### URL Ingestion Rules\nOnly ingest URLs when the user explicitly says:\n- \"Read this link\"\n- \"Summarize this webpage\"\n- \"What does this site say?\"\n- \"Ingest this URL\"\nIf unclear → ask a clarifying question.\n### Calculator Usage Rules\nUse the calculator when:\n- Performing arithmetic\n- Estimating totals\n- Comparing values\n- Modeling cost, time, effort, scale, or projections\nDo not perform math internally. **Call the calculator tool instead.**\n### Answer Construction Rules\n1. When asked: \"What is OpenRAG\", answer the following:\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n**Langflow** – Langflow is a powerful tool to build and deploy AI agents and MCP servers. [Read more](https://www.langflow.org/)\n**OpenSearch** – OpenSearch is an open source, search and observability suite that brings order to unstructured data at scale. [Read more](https://opensearch.org/)\n**Docling** – Docling simplifies document processing with advanced PDF understanding, OCR support, and seamless AI integrations. Parse PDFs, DOCX, PPTX, images & more. [Read more](https://www.docling.ai/)\"\n2. Synthesize retrieved or ingested content in your own words.\n3. Support factual claims with citations in the format:\n (Source: )\n4. If no supporting evidence is found:\n Say: \"No relevant supporting sources were found for that request.\"\n5. Never invent facts or hallucinate details.\n6. Be concise, direct, and confident. \n7. Do not reveal internal chain-of-thought." }, "temperature": { "_input_type": "SliderInput", diff --git a/frontend/app/onboarding/_components/model-selector.tsx b/frontend/app/onboarding/_components/model-selector.tsx index 147ec034..4939951a 100644 --- a/frontend/app/onboarding/_components/model-selector.tsx +++ b/frontend/app/onboarding/_components/model-selector.tsx @@ -41,6 +41,7 @@ export function ModelSelector({ noOptionsPlaceholder = "No models available", custom = false, hasError = false, + defaultOpen = false, }: { options?: ModelOption[]; groupedOptions?: GroupedModelOption[]; @@ -52,8 +53,9 @@ export function ModelSelector({ custom?: boolean; onValueChange: (value: string, provider?: string) => void; hasError?: boolean; + defaultOpen?: boolean; }) { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(defaultOpen); const [searchValue, setSearchValue] = useState(""); // Flatten grouped options or use regular options @@ -77,6 +79,13 @@ export function ModelSelector({ } }, [allOptions, value, custom, onValueChange]); + // Update open state when defaultOpen changes + useEffect(() => { + if (defaultOpen) { + setOpen(true); + } + }, [defaultOpen]); + return ( diff --git a/frontend/app/settings/_components/anthropic-settings-dialog.tsx b/frontend/app/settings/_components/anthropic-settings-dialog.tsx index caac712f..9501d0e9 100644 --- a/frontend/app/settings/_components/anthropic-settings-dialog.tsx +++ b/frontend/app/settings/_components/anthropic-settings-dialog.tsx @@ -9,150 +9,161 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import AnthropicLogo from "@/components/icons/anthropic-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - AnthropicSettingsForm, - type AnthropicSettingsFormData, + AnthropicSettingsForm, + type AnthropicSettingsFormData, } from "./anthropic-settings-form"; +import { useRouter } from "next/navigation"; const AnthropicSettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - apiKey: "", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + apiKey: "", + }, + }); - const { handleSubmit, watch } = methods; - const apiKey = watch("apiKey"); + const { handleSubmit, watch } = methods; + const apiKey = watch("apiKey"); - const { refetch: validateCredentials } = useGetAnthropicModelsQuery( - { - apiKey: apiKey, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetAnthropicModelsQuery( + { + apiKey: apiKey, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "anthropic", - }; - queryClient.setQueryData(["provider", "health"], healthData); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "anthropic", + }; + queryClient.setQueryData(["provider", "health"], healthData); - toast.success( - "Anthropic credentials saved. Configure models in the Settings page.", - ); - setOpen(false); - }, - }); + toast.message("Anthropic successfully configured", { + description: "You can now access the provided language models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - const onSubmit = async (data: AnthropicSettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + const onSubmit = async (data: AnthropicSettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - // Only validate if a new API key was entered - if (data.apiKey) { - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + // Only validate if a new API key was entered + if (data.apiKey) { + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - if (result.isError) { - setValidationError(result.error); - return; - } - } + if (result.isError) { + setValidationError(result.error); + return; + } + } - const payload: { - anthropic_api_key?: string; - } = {}; + const payload: { + anthropic_api_key?: string; + } = {}; - // Only include api_key if a value was entered - if (data.apiKey) { - payload.anthropic_api_key = data.apiKey; - } + // Only include api_key if a value was entered + if (data.apiKey) { + payload.anthropic_api_key = data.apiKey; + } - // Submit the update - settingsMutation.mutate(payload); - }; + // Submit the update + settingsMutation.mutate(payload); + }; - return ( - - - -
- - -
- -
- Anthropic Setup -
-
+ return ( + + + + + + +
+ +
+ Anthropic Setup +
+
- + - - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default AnthropicSettingsDialog; diff --git a/frontend/app/settings/_components/model-providers.tsx b/frontend/app/settings/_components/model-providers.tsx index 53e6c40e..b5f2cd9a 100644 --- a/frontend/app/settings/_components/model-providers.tsx +++ b/frontend/app/settings/_components/model-providers.tsx @@ -96,20 +96,10 @@ export const ModelProviders = () => { const currentEmbeddingProvider = (settings.knowledge?.embedding_provider as ModelProvider) || "openai"; - // Get all provider keys with active providers first - const activeProviders = new Set([ - currentLlmProvider, - currentEmbeddingProvider, - ]); - const sortedProviderKeys = [ - ...Array.from(activeProviders), - ...allProviderKeys.filter((key) => !activeProviders.has(key)), - ]; - return ( <>
- {sortedProviderKeys.map((providerKey) => { + {allProviderKeys.map((providerKey) => { const { name, logo: Logo, @@ -118,7 +108,6 @@ export const ModelProviders = () => { } = modelProvidersMap[providerKey]; const isLlmProvider = providerKey === currentLlmProvider; const isEmbeddingProvider = providerKey === currentEmbeddingProvider; - const isCurrentProvider = isLlmProvider || isEmbeddingProvider; // Check if this specific provider is unhealthy const hasLlmError = isLlmProvider && health?.llm_error; @@ -161,16 +150,8 @@ export const ModelProviders = () => {
{name} - {isCurrentProvider && ( - + {isProviderUnhealthy && ( + )} diff --git a/frontend/app/settings/_components/ollama-settings-dialog.tsx b/frontend/app/settings/_components/ollama-settings-dialog.tsx index a20ce303..87addcea 100644 --- a/frontend/app/settings/_components/ollama-settings-dialog.tsx +++ b/frontend/app/settings/_components/ollama-settings-dialog.tsx @@ -10,150 +10,162 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import OllamaLogo from "@/components/icons/ollama-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { useAuth } from "@/contexts/auth-context"; import { - OllamaSettingsForm, - type OllamaSettingsFormData, + OllamaSettingsForm, + type OllamaSettingsFormData, } from "./ollama-settings-form"; +import { useRouter } from "next/navigation"; const OllamaSettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const { isAuthenticated, isNoAuthMode } = useAuth(); - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); + const { isAuthenticated, isNoAuthMode } = useAuth(); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const { data: settings = {} } = useGetSettingsQuery({ - enabled: isAuthenticated || isNoAuthMode, - }); + const { data: settings = {} } = useGetSettingsQuery({ + enabled: isAuthenticated || isNoAuthMode, + }); - const isOllamaConfigured = settings.providers?.ollama?.configured === true; + const isOllamaConfigured = settings.providers?.ollama?.configured === true; - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - endpoint: isOllamaConfigured - ? settings.providers?.ollama?.endpoint - : "http://localhost:11434", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + endpoint: isOllamaConfigured + ? settings.providers?.ollama?.endpoint + : "http://localhost:11434", + }, + }); - const { handleSubmit, watch } = methods; - const endpoint = watch("endpoint"); + const { handleSubmit, watch } = methods; + const endpoint = watch("endpoint"); - const { refetch: validateCredentials } = useGetOllamaModelsQuery( - { - endpoint: endpoint, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetOllamaModelsQuery( + { + endpoint: endpoint, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "ollama", - }; - queryClient.setQueryData(["provider", "health"], healthData); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "ollama", + }; + queryClient.setQueryData(["provider", "health"], healthData); - toast.success( - "Ollama endpoint saved. Configure models in the Settings page.", - ); - setOpen(false); - }, - }); + toast.message("Ollama successfully configured", { + description: + "You can now access the provided language and embedding models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - const onSubmit = async (data: OllamaSettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + const onSubmit = async (data: OllamaSettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - // Validate endpoint by fetching models - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + // Validate endpoint by fetching models + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - if (result.isError) { - setValidationError(result.error); - return; - } + if (result.isError) { + setValidationError(result.error); + return; + } - settingsMutation.mutate({ - ollama_endpoint: data.endpoint, - }); - }; + settingsMutation.mutate({ + ollama_endpoint: data.endpoint, + }); + }; - return ( - - - -
- - -
- -
- Ollama Setup -
-
+ return ( + + + + + + +
+ +
+ Ollama Setup +
+
- + - - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default OllamaSettingsDialog; diff --git a/frontend/app/settings/_components/openai-settings-dialog.tsx b/frontend/app/settings/_components/openai-settings-dialog.tsx index 9584379b..099c3474 100644 --- a/frontend/app/settings/_components/openai-settings-dialog.tsx +++ b/frontend/app/settings/_components/openai-settings-dialog.tsx @@ -9,150 +9,162 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import OpenAILogo from "@/components/icons/openai-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - OpenAISettingsForm, - type OpenAISettingsFormData, + OpenAISettingsForm, + type OpenAISettingsFormData, } from "./openai-settings-form"; +import { useRouter } from "next/navigation"; const OpenAISettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - apiKey: "", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + apiKey: "", + }, + }); - const { handleSubmit, watch } = methods; - const apiKey = watch("apiKey"); + const { handleSubmit, watch } = methods; + const apiKey = watch("apiKey"); - const { refetch: validateCredentials } = useGetOpenAIModelsQuery( - { - apiKey: apiKey, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetOpenAIModelsQuery( + { + apiKey: apiKey, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "openai", - }; - queryClient.setQueryData(["provider", "health"], healthData); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "openai", + }; + queryClient.setQueryData(["provider", "health"], healthData); - toast.success( - "OpenAI credentials saved. Configure models in the Settings page.", - ); - setOpen(false); - }, - }); + toast.message("OpenAI successfully configured", { + description: + "You can now access the provided language and embedding models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - const onSubmit = async (data: OpenAISettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + const onSubmit = async (data: OpenAISettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - // Only validate if a new API key was entered - if (data.apiKey) { - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + // Only validate if a new API key was entered + if (data.apiKey) { + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - if (result.isError) { - setValidationError(result.error); - return; - } - } + if (result.isError) { + setValidationError(result.error); + return; + } + } - const payload: { - openai_api_key?: string; - } = {}; + const payload: { + openai_api_key?: string; + } = {}; - // Only include api_key if a value was entered - if (data.apiKey) { - payload.openai_api_key = data.apiKey; - } + // Only include api_key if a value was entered + if (data.apiKey) { + payload.openai_api_key = data.apiKey; + } - // Submit the update - settingsMutation.mutate(payload); - }; + // Submit the update + settingsMutation.mutate(payload); + }; - return ( - - - -
- - -
- -
- OpenAI Setup -
-
+ return ( + + + + + + +
+ +
+ OpenAI Setup +
+
- + - - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default OpenAISettingsDialog; diff --git a/frontend/app/settings/_components/watsonx-settings-dialog.tsx b/frontend/app/settings/_components/watsonx-settings-dialog.tsx index 798e5eb9..5ac7fe01 100644 --- a/frontend/app/settings/_components/watsonx-settings-dialog.tsx +++ b/frontend/app/settings/_components/watsonx-settings-dialog.tsx @@ -9,158 +9,171 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth import IBMLogo from "@/components/icons/ibm-logo"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - WatsonxSettingsForm, - type WatsonxSettingsFormData, + WatsonxSettingsForm, + type WatsonxSettingsFormData, } from "./watsonx-settings-form"; +import { useRouter } from "next/navigation"; const WatsonxSettingsDialog = ({ - open, - setOpen, + open, + setOpen, }: { - open: boolean; - setOpen: (open: boolean) => void; + open: boolean; + setOpen: (open: boolean) => void; }) => { - const queryClient = useQueryClient(); - const [isValidating, setIsValidating] = useState(false); - const [validationError, setValidationError] = useState(null); + const queryClient = useQueryClient(); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const router = useRouter(); - const methods = useForm({ - mode: "onSubmit", - defaultValues: { - endpoint: "https://us-south.ml.cloud.ibm.com", - apiKey: "", - projectId: "", - }, - }); + const methods = useForm({ + mode: "onSubmit", + defaultValues: { + endpoint: "https://us-south.ml.cloud.ibm.com", + apiKey: "", + projectId: "", + }, + }); - const { handleSubmit, watch } = methods; - const endpoint = watch("endpoint"); - const apiKey = watch("apiKey"); - const projectId = watch("projectId"); + const { handleSubmit, watch } = methods; + const endpoint = watch("endpoint"); + const apiKey = watch("apiKey"); + const projectId = watch("projectId"); - const { refetch: validateCredentials } = useGetIBMModelsQuery( - { - endpoint: endpoint, - apiKey: apiKey, - projectId: projectId, - }, - { - enabled: false, - }, - ); + const { refetch: validateCredentials } = useGetIBMModelsQuery( + { + endpoint: endpoint, + apiKey: apiKey, + projectId: projectId, + }, + { + enabled: false, + }, + ); - const settingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - // Update provider health cache to healthy since backend validated the setup - const healthData: ProviderHealthResponse = { - status: "healthy", - message: "Provider is configured and working correctly", - provider: "watsonx", - }; - queryClient.setQueryData(["provider", "health"], healthData); - toast.success( - "watsonx credentials saved. Configure models in the Settings page.", - ); - setOpen(false); - }, - }); + const settingsMutation = useUpdateSettingsMutation({ + onSuccess: () => { + // Update provider health cache to healthy since backend validated the setup + const healthData: ProviderHealthResponse = { + status: "healthy", + message: "Provider is configured and working correctly", + provider: "watsonx", + }; + queryClient.setQueryData(["provider", "health"], healthData); - const onSubmit = async (data: WatsonxSettingsFormData) => { - // Clear any previous validation errors - setValidationError(null); + toast.message("IBM watsonx.ai successfully configured", { + description: + "You can now access the provided language and embedding models.", + duration: Infinity, + closeButton: true, + icon: , + action: { + label: "Settings", + onClick: () => { + router.push("/settings?focusLlmModel=true"); + }, + }, + }); + setOpen(false); + }, + }); - // Validate credentials by fetching models - setIsValidating(true); - const result = await validateCredentials(); - setIsValidating(false); + const onSubmit = async (data: WatsonxSettingsFormData) => { + // Clear any previous validation errors + setValidationError(null); - if (result.isError) { - setValidationError(result.error); - return; - } + // Validate credentials by fetching models + setIsValidating(true); + const result = await validateCredentials(); + setIsValidating(false); - const payload: { - watsonx_endpoint: string; - watsonx_api_key?: string; - watsonx_project_id: string; - } = { - watsonx_endpoint: data.endpoint, - watsonx_project_id: data.projectId, - }; + if (result.isError) { + setValidationError(result.error); + return; + } - // Only include api_key if a value was entered - if (data.apiKey) { - payload.watsonx_api_key = data.apiKey; - } + const payload: { + watsonx_endpoint: string; + watsonx_api_key?: string; + watsonx_project_id: string; + } = { + watsonx_endpoint: data.endpoint, + watsonx_project_id: data.projectId, + }; - // Submit the update - settingsMutation.mutate(payload); - }; + // Only include api_key if a value was entered + if (data.apiKey) { + payload.watsonx_api_key = data.apiKey; + } - return ( - - - -
- - -
- -
- IBM watsonx.ai Setup -
-
+ // Submit the update + settingsMutation.mutate(payload); + }; - + return ( + + + + + + +
+ +
+ IBM watsonx.ai Setup +
+
- - {settingsMutation.isError && ( - -

- {settingsMutation.error?.message} -

-
- )} -
- - - - - -
-
-
- ); + + + + {settingsMutation.isError && ( + +

+ {settingsMutation.error?.message} +

+
+ )} +
+ + + + + +
+
+
+ ); }; export default WatsonxSettingsDialog; diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index d859df03..c122aa65 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -6,10 +6,10 @@ import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { - useGetAnthropicModelsQuery, - useGetIBMModelsQuery, - useGetOllamaModelsQuery, - useGetOpenAIModelsQuery, + useGetAnthropicModelsQuery, + useGetIBMModelsQuery, + useGetOllamaModelsQuery, + useGetOpenAIModelsQuery, } from "@/app/api/queries/useGetModelsQuery"; import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery"; import { ConfirmationDialog } from "@/components/confirmation-dialog"; @@ -17,11 +17,11 @@ import { LabelWrapper } from "@/components/label-wrapper"; import { ProtectedRoute } from "@/components/protected-route"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -30,9 +30,9 @@ import { Textarea } from "@/components/ui/textarea"; import { useAuth } from "@/contexts/auth-context"; import { useTask } from "@/contexts/task-context"; import { - DEFAULT_AGENT_SETTINGS, - DEFAULT_KNOWLEDGE_SETTINGS, - UI_CONSTANTS, + DEFAULT_AGENT_SETTINGS, + DEFAULT_KNOWLEDGE_SETTINGS, + UI_CONSTANTS, } from "@/lib/constants"; import { useDebounce } from "@/lib/debounce"; import GoogleDriveIcon from "../../components/icons/google-drive-logo"; @@ -42,1268 +42,1284 @@ import { useUpdateSettingsMutation } from "../api/mutations/useUpdateSettingsMut import { ModelSelector } from "../onboarding/_components/model-selector"; import ModelProviders from "./_components/model-providers"; import { getModelLogo, type ModelProvider } from "./_helpers/model-helpers"; +import { cn } from "@/lib/utils"; const { MAX_SYSTEM_PROMPT_CHARS } = UI_CONSTANTS; interface GoogleDriveFile { - id: string; - name: string; - mimeType: string; - webViewLink?: string; - iconLink?: string; + 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; - }; + id: string; + name: string; + mimeType?: string; + webUrl?: string; + driveItem?: { + file?: { mimeType: string }; + folder?: unknown; + }; } interface Connector { - id: string; - name: string; - description: string; - icon: React.ReactNode; - status: "not_connected" | "connecting" | "connected" | "error"; - type: string; - connectionId?: string; - access_token?: string; - selectedFiles?: GoogleDriveFile[] | OneDriveFile[]; - available?: boolean; + id: string; + name: string; + description: string; + icon: React.ReactNode; + status: "not_connected" | "connecting" | "connected" | "error"; + type: string; + connectionId?: string; + access_token?: string; + selectedFiles?: GoogleDriveFile[] | OneDriveFile[]; + available?: boolean; } interface SyncResult { - processed?: number; - added?: number; - errors?: number; - skipped?: number; - total?: number; + processed?: number; + added?: number; + errors?: number; + skipped?: number; + total?: number; } interface Connection { - connection_id: string; - is_active: boolean; - created_at: string; - last_sync?: string; + connection_id: string; + is_active: boolean; + created_at: string; + last_sync?: string; } function KnowledgeSourcesPage() { - const { isAuthenticated, isNoAuthMode } = useAuth(); - const { addTask, tasks } = useTask(); - const searchParams = useSearchParams(); - const router = useRouter(); - - // Connectors state - const [connectors, setConnectors] = useState([]); - const [isConnecting, setIsConnecting] = useState(null); - const [isSyncing, setIsSyncing] = useState(null); - const [syncResults, setSyncResults] = useState<{ - [key: string]: SyncResult | null; - }>({}); - const [maxFiles, setMaxFiles] = useState(10); - const [syncAllFiles, setSyncAllFiles] = useState(false); - - // Only keep systemPrompt state since it needs manual save button - const [systemPrompt, setSystemPrompt] = useState(""); - const [chunkSize, setChunkSize] = useState(1024); - const [chunkOverlap, setChunkOverlap] = useState(50); - const [tableStructure, setTableStructure] = useState(true); - const [ocr, setOcr] = useState(false); - const [pictureDescriptions, setPictureDescriptions] = - useState(false); - - // Fetch settings using React Query - const { data: settings = {} } = useGetSettingsQuery({ - enabled: isAuthenticated || isNoAuthMode, - }); - - // Fetch models for each provider - const { data: openaiModels, isLoading: openaiLoading } = - useGetOpenAIModelsQuery( - { apiKey: "" }, - { enabled: settings?.providers?.openai?.configured === true }, - ); - - const { data: anthropicModels, isLoading: anthropicLoading } = - useGetAnthropicModelsQuery( - { apiKey: "" }, - { enabled: settings?.providers?.anthropic?.configured === true }, - ); - - const { data: ollamaModels, isLoading: ollamaLoading } = - useGetOllamaModelsQuery( - { endpoint: settings?.providers?.ollama?.endpoint }, - { - enabled: - settings?.providers?.ollama?.configured === true && - !!settings?.providers?.ollama?.endpoint, - }, - ); - - const { data: watsonxModels, isLoading: watsonxLoading } = - useGetIBMModelsQuery( - { - endpoint: settings?.providers?.watsonx?.endpoint, - apiKey: "", - projectId: settings?.providers?.watsonx?.project_id, - }, - { - enabled: - settings?.providers?.watsonx?.configured === true && - !!settings?.providers?.watsonx?.endpoint && - !!settings?.providers?.watsonx?.project_id, - }, - ); - - // Build grouped LLM model options from all configured providers - const groupedLlmModels = [ - { - group: "OpenAI", - provider: "openai", - icon: getModelLogo("", "openai"), - models: openaiModels?.language_models || [], - configured: settings.providers?.openai?.configured === true, - }, - { - group: "Anthropic", - provider: "anthropic", - icon: getModelLogo("", "anthropic"), - models: anthropicModels?.language_models || [], - configured: settings.providers?.anthropic?.configured === true, - }, - { - group: "Ollama", - provider: "ollama", - icon: getModelLogo("", "ollama"), - models: ollamaModels?.language_models || [], - configured: settings.providers?.ollama?.configured === true, - }, - { - group: "IBM watsonx.ai", - provider: "watsonx", - icon: getModelLogo("", "watsonx"), - models: watsonxModels?.language_models || [], - configured: settings.providers?.watsonx?.configured === true, - }, - ] - .filter((provider) => provider.configured) - .map((provider) => ({ - group: provider.group, - icon: provider.icon, - options: provider.models.map((model) => ({ - ...model, - provider: provider.provider, - })), - })) - .filter((provider) => provider.options.length > 0); - - // Build grouped embedding model options from all configured providers (excluding Anthropic) - const groupedEmbeddingModels = [ - { - group: "OpenAI", - provider: "openai", - icon: getModelLogo("", "openai"), - models: openaiModels?.embedding_models || [], - configured: settings.providers?.openai?.configured === true, - }, - { - group: "Ollama", - provider: "ollama", - icon: getModelLogo("", "ollama"), - models: ollamaModels?.embedding_models || [], - configured: settings.providers?.ollama?.configured === true, - }, - { - group: "IBM watsonx.ai", - provider: "watsonx", - icon: getModelLogo("", "watsonx"), - models: watsonxModels?.embedding_models || [], - configured: settings.providers?.watsonx?.configured === true, - }, - ] - .filter((provider) => provider.configured) - .map((provider) => ({ - group: provider.group, - icon: provider.icon, - options: provider.models.map((model) => ({ - ...model, - provider: provider.provider, - })), - })) - .filter((provider) => provider.options.length > 0); - - const isLoadingAnyLlmModels = - openaiLoading || anthropicLoading || ollamaLoading || watsonxLoading; - const isLoadingAnyEmbeddingModels = - openaiLoading || ollamaLoading || watsonxLoading; - - // Mutations - const updateSettingsMutation = useUpdateSettingsMutation({ - onSuccess: () => { - toast.success("Settings updated successfully"); - }, - onError: (error) => { - toast.error("Failed to update settings", { - description: error.message, - }); - }, - }); - - // Debounced update function - const debouncedUpdate = useDebounce( - (variables: Parameters[0]) => { - updateSettingsMutation.mutate(variables); - }, - 500, - ); - - // Sync system prompt state with settings data - useEffect(() => { - if (settings.agent?.system_prompt) { - setSystemPrompt(settings.agent.system_prompt); - } - }, [settings.agent?.system_prompt]); - - // Sync chunk size and overlap state with settings data - useEffect(() => { - if (settings.knowledge?.chunk_size) { - setChunkSize(settings.knowledge.chunk_size); - } - }, [settings.knowledge?.chunk_size]); - - useEffect(() => { - if (settings.knowledge?.chunk_overlap) { - setChunkOverlap(settings.knowledge.chunk_overlap); - } - }, [settings.knowledge?.chunk_overlap]); - - // Sync docling settings with settings data - useEffect(() => { - if (settings.knowledge?.table_structure !== undefined) { - setTableStructure(settings.knowledge.table_structure); - } - }, [settings.knowledge?.table_structure]); - - useEffect(() => { - if (settings.knowledge?.ocr !== undefined) { - setOcr(settings.knowledge.ocr); - } - }, [settings.knowledge?.ocr]); - - useEffect(() => { - if (settings.knowledge?.picture_descriptions !== undefined) { - setPictureDescriptions(settings.knowledge.picture_descriptions); - } - }, [settings.knowledge?.picture_descriptions]); - - // Update model selection immediately (also updates provider) - const handleModelChange = (newModel: string, provider?: string) => { - if (newModel && provider) { - updateSettingsMutation.mutate({ - llm_model: newModel, - llm_provider: provider, - }); - } else if (newModel) { - updateSettingsMutation.mutate({ llm_model: newModel }); - } - }; - - // Update system prompt with save button - const handleSystemPromptSave = () => { - updateSettingsMutation.mutate({ system_prompt: systemPrompt }); - }; - - // Update embedding model selection immediately (also updates provider) - const handleEmbeddingModelChange = (newModel: string, provider?: string) => { - if (newModel && provider) { - updateSettingsMutation.mutate({ - embedding_model: newModel, - embedding_provider: provider, - }); - } else if (newModel) { - updateSettingsMutation.mutate({ embedding_model: newModel }); - } - }; - - // Update chunk size setting with debounce - const handleChunkSizeChange = (value: string) => { - const numValue = Math.max(0, parseInt(value) || 0); - setChunkSize(numValue); - debouncedUpdate({ chunk_size: numValue }); - }; - - // Update chunk overlap setting with debounce - const handleChunkOverlapChange = (value: string) => { - const numValue = Math.max(0, parseInt(value) || 0); - setChunkOverlap(numValue); - debouncedUpdate({ chunk_overlap: numValue }); - }; - - // Update docling settings - const handleTableStructureChange = (checked: boolean) => { - setTableStructure(checked); - updateSettingsMutation.mutate({ table_structure: checked }); - }; - - const handleOcrChange = (checked: boolean) => { - setOcr(checked); - updateSettingsMutation.mutate({ ocr: checked }); - }; - - const handlePictureDescriptionsChange = (checked: boolean) => { - setPictureDescriptions(checked); - updateSettingsMutation.mutate({ picture_descriptions: checked }); - }; - - // Helper function to get connector icon - const getConnectorIcon = useCallback((iconName: string) => { - const iconMap: { [key: string]: React.ReactElement } = { - "google-drive": , - sharepoint: , - onedrive: , - }; - return ( - iconMap[iconName] || ( -
- ? -
- ) - ); - }, []); - - // Connector functions - const checkConnectorStatuses = useCallback(async () => { - try { - // Fetch available connectors from backend - const connectorsResponse = await fetch("/api/connectors"); - if (!connectorsResponse.ok) { - throw new Error("Failed to load connectors"); - } - - const connectorsResult = await connectorsResponse.json(); - const connectorTypes = Object.keys(connectorsResult.connectors); - - // Initialize connectors list with metadata from backend - const initialConnectors = connectorTypes - .filter((type) => connectorsResult.connectors[type].available) // Only show available connectors - .map((type) => ({ - id: type, - name: connectorsResult.connectors[type].name, - description: connectorsResult.connectors[type].description, - icon: getConnectorIcon(connectorsResult.connectors[type].icon), - status: "not_connected" as const, - type: type, - available: connectorsResult.connectors[type].available, - })); - - setConnectors(initialConnectors); - - // Check status for each connector type - - for (const connectorType of connectorTypes) { - const response = await fetch(`/api/connectors/${connectorType}/status`); - if (response.ok) { - const data = await response.json(); - const connections = data.connections || []; - const activeConnection = connections.find( - (conn: Connection) => conn.is_active, - ); - const isConnected = activeConnection !== undefined; - - setConnectors((prev) => - prev.map((c) => - c.type === connectorType - ? { - ...c, - status: isConnected ? "connected" : "not_connected", - connectionId: activeConnection?.connection_id, - } - : c, - ), - ); - } - } - } catch (error) { - console.error("Failed to check connector statuses:", error); - } - }, [getConnectorIcon]); - - const handleConnect = async (connector: Connector) => { - setIsConnecting(connector.id); - setSyncResults((prev) => ({ ...prev, [connector.id]: null })); - - try { - // Use the shared auth callback URL, same as connectors page - const redirectUri = `${window.location.origin}/auth/callback`; - - const response = await fetch("/api/auth/init", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - connector_type: connector.type, - purpose: "data_source", - name: `${connector.name} Connection`, - redirect_uri: redirectUri, - }), - }); - - if (response.ok) { - const result = await response.json(); - - if (result.oauth_config) { - localStorage.setItem("connecting_connector_id", result.connection_id); - localStorage.setItem("connecting_connector_type", connector.type); - - const authUrl = - `${result.oauth_config.authorization_endpoint}?` + - `client_id=${result.oauth_config.client_id}&` + - `response_type=code&` + - `scope=${result.oauth_config.scopes.join(" ")}&` + - `redirect_uri=${encodeURIComponent( - result.oauth_config.redirect_uri, - )}&` + - `access_type=offline&` + - `prompt=consent&` + - `state=${result.connection_id}`; - - window.location.href = authUrl; - } - } else { - console.error("Failed to initiate connection"); - setIsConnecting(null); - } - } catch (error) { - console.error("Connection error:", error); - setIsConnecting(null); - } - }; - - // const handleSync = async (connector: Connector) => { - // if (!connector.connectionId) return; - - // setIsSyncing(connector.id); - // setSyncResults(prev => ({ ...prev, [connector.id]: null })); - - // 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`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(syncBody), - // }); - - // const result = await response.json(); - - // if (response.status === 201) { - // const taskId = result.task_id; - // if (taskId) { - // addTask(taskId); - // setSyncResults(prev => ({ - // ...prev, - // [connector.id]: { - // processed: 0, - // total: result.total_files || 0, - // }, - // })); - // } - // } else if (response.ok) { - // setSyncResults(prev => ({ ...prev, [connector.id]: result })); - // // Note: Stats will auto-refresh via task completion watcher for async syncs - // } else { - // console.error("Sync failed:", result.error); - // } - // } catch (error) { - // console.error("Sync error:", error); - // } finally { - // setIsSyncing(null); - // } - // }; - - const getStatusBadge = (status: Connector["status"]) => { - switch (status) { - case "connected": - return ( -
- ); - case "connecting": - return ( -
- ); - case "error": - return ( -
- ); - default: - return
; - } - }; - - const navigateToKnowledgePage = (connector: Connector) => { - const provider = connector.type.replace(/-/g, "_"); - router.push(`/upload/${provider}`); - }; - - // Check connector status on mount and when returning from OAuth - useEffect(() => { - if (isAuthenticated) { - checkConnectorStatuses(); - } - - if (searchParams.get("oauth_success") === "true") { - const url = new URL(window.location.href); - url.searchParams.delete("oauth_success"); - window.history.replaceState({}, "", url.toString()); - } - }, [searchParams, isAuthenticated, checkConnectorStatuses]); - - // Track previous tasks to detect new completions - const [prevTasks, setPrevTasks] = useState([]); - - // Watch for task completions and refresh stats - useEffect(() => { - // Find newly completed tasks by comparing with previous state - const newlyCompletedTasks = tasks.filter((task) => { - const wasCompleted = - prevTasks.find((prev) => prev.task_id === task.task_id)?.status === - "completed"; - return task.status === "completed" && !wasCompleted; - }); - - if (newlyCompletedTasks.length > 0) { - // Task completed - could refresh data here if needed - const timeoutId = setTimeout(() => { - // Stats refresh removed - }, 1000); - - // Update previous tasks state - setPrevTasks(tasks); - - return () => clearTimeout(timeoutId); - } else { - // Always update previous tasks state - setPrevTasks(tasks); - } - }, [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" ? settings.ingest_flow_id : settings.flow_id; - const editUrl = - flowType === "ingest" - ? settings.langflow_ingest_edit_url - : settings.langflow_edit_url; - - const derivedFromWindow = - typeof window !== "undefined" - ? `${window.location.protocol}//${window.location.hostname}:7860` - : ""; - const base = ( - settings.langflow_public_url || - 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) => { - if (response.ok) { - return response.json(); - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - }) - .then(() => { - // Only reset form values if the API call was successful - setSystemPrompt(DEFAULT_AGENT_SETTINGS.system_prompt); - // Trigger model update to default model - handleModelChange(DEFAULT_AGENT_SETTINGS.llm_model); - 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) => { - if (response.ok) { - return response.json(); - } - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - }) - .then(() => { - // Only reset form values if the API call was successful - setChunkSize(DEFAULT_KNOWLEDGE_SETTINGS.chunk_size); - setChunkOverlap(DEFAULT_KNOWLEDGE_SETTINGS.chunk_overlap); - setTableStructure(false); - setOcr(false); - setPictureDescriptions(false); - 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 ( -
- {/* Connectors Section */} -
-
-

- Cloud Connectors -

-
- - {/* Conditional Sync Settings or No-Auth Message */} - { - isNoAuthMode ? ( - - - - Cloud connectors require authentication - - - Add the Google OAuth variables below to your .env{" "} - then restart the OpenRAG containers. - - - -
-
-
- - 27 - - # Google OAuth -
-
- - 28 - - # Create credentials here: -
-
- - 29 - - - # https://console.cloud.google.com/apis/credentials - -
-
-
- 30 - GOOGLE_OAUTH_CLIENT_ID= -
-
- 31 - GOOGLE_OAUTH_CLIENT_SECRET= -
-
-
-
- ) : null - //
- //
- //

Sync Settings

- //

- // Configure how many files to sync when manually triggering a sync - //

- //
- //
- //
- // { - // setSyncAllFiles(!!checked); - // if (checked) { - // setMaxFiles(0); - // } else { - // setMaxFiles(10); - // } - // }} - // /> - // - //
- // - //
- // setMaxFiles(parseInt(e.target.value) || 10)} - // disabled={syncAllFiles} - // className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed" - // min="1" - // max="100" - // title={ - // syncAllFiles - // ? "Disabled when 'Sync all files' is checked" - // : "Leave blank or set to 0 for unlimited" - // } - // /> - //
- //
- //
- } - {/* Connectors Grid */} -
- {connectors.map((connector) => { - return ( - - -
-
-
-
- {connector.icon} -
-
- - {connector.name} - {connector && getStatusBadge(connector.status)} - - - {connector?.description - ? `${connector.name} is configured.` - : connector.description} - -
-
-
- - {connector?.available ? ( -
- {connector?.status === "connected" ? ( - <> - - {syncResults[connector.id] && ( -
-
- Processed:{" "} - {syncResults[connector.id]?.processed || 0} -
-
- Added: {syncResults[connector.id]?.added || 0} -
- {syncResults[connector.id]?.errors && ( -
- Errors: {syncResults[connector.id]?.errors} -
- )} -
- )} - - ) : ( - - )} -
- ) : ( -
-

- See our{" "} - - Cloud Connectors installation guide - {" "} - for more detail. -

-
- )} -
-
- ); - })} -
-
- - {/* Model Providers Section */} -
-
-

- Model Providers -

-
- -
- - {/* Agent Behavior Section */} - - -
- Agent -
- - Restore flow - - } - title="Restore default Agent flow" - description="This restores defaults and discards all custom settings and overrides. This can’t be undone." - confirmText="Restore" - variant="destructive" - onConfirm={handleRestoreRetrievalFlow} - /> - - - Langflow icon - - - - - Edit in Langflow - - } - title="Edit Agent flow in Langflow" - 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. -

-

You can restore this flow from Settings.

- - } - confirmText="Proceed" - confirmIcon={} - onConfirm={(closeDialog) => - handleEditInLangflow("chat", closeDialog) - } - variant="warning" - /> -
-
- - This Agent retrieves from your knowledge and generates chat - responses. Edit in Langflow for full control. - -
- -
-
- - - -
-
- -