Merge branch 'main' into feat/refactor-folder-structure
This commit is contained in:
commit
16d49c5f2d
11 changed files with 800 additions and 52 deletions
|
|
@ -56,8 +56,13 @@ export const useDoclingHealthQuery = (
|
|||
queryKey: ["docling-health"],
|
||||
queryFn: checkDoclingHealth,
|
||||
retry: 1,
|
||||
refetchInterval: 30000, // Check every 30 seconds
|
||||
staleTime: 25000, // Consider data stale after 25 seconds
|
||||
refetchInterval: (query) => {
|
||||
// If healthy, check every 30 seconds; otherwise check every 3 seconds
|
||||
return query.state.data?.status === "healthy" ? 30000 : 3000;
|
||||
},
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnMount: true,
|
||||
staleTime: 30000, // Consider data stale after 25 seconds
|
||||
...options,
|
||||
},
|
||||
queryClient,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,13 @@ export const useProviderHealthQuery = (
|
|||
queryKey: ["provider", "health"],
|
||||
queryFn: checkProviderHealth,
|
||||
retry: false, // Don't retry health checks automatically
|
||||
refetchInterval: (query) => {
|
||||
// If healthy, check every 30 seconds; otherwise check every 3 seconds
|
||||
return query.state.data?.status === "healthy" ? 30000 : 3000;
|
||||
},
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnMount: true,
|
||||
staleTime: 30000, // Consider data stale after 25 seconds
|
||||
enabled: !!settings?.edited && options?.enabled !== false, // Only run after onboarding is complete
|
||||
...options,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -365,4 +365,22 @@
|
|||
width: 100%;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.thinking-dots::after {
|
||||
content: ".";
|
||||
animation: thinking-dots 1.4s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes thinking-dots {
|
||||
0% {
|
||||
content: ".";
|
||||
}
|
||||
33.33% {
|
||||
content: "..";
|
||||
}
|
||||
66.66%,
|
||||
100% {
|
||||
content: "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
97
frontend/src/app/chat/components/assistant-message.tsx
Normal file
97
frontend/src/app/chat/components/assistant-message.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { GitBranch } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import DogIcon from "@/components/logo/dog-icon";
|
||||
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { FunctionCall } from "../types";
|
||||
import { FunctionCalls } from "./function-calls";
|
||||
import { Message } from "./message";
|
||||
|
||||
interface AssistantMessageProps {
|
||||
content: string;
|
||||
functionCalls?: FunctionCall[];
|
||||
messageIndex?: number;
|
||||
expandedFunctionCalls: Set<string>;
|
||||
onToggle: (functionCallId: string) => void;
|
||||
isStreaming?: boolean;
|
||||
showForkButton?: boolean;
|
||||
onFork?: (e: React.MouseEvent) => void;
|
||||
isCompleted?: boolean;
|
||||
isInactive?: boolean;
|
||||
animate?: boolean;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function AssistantMessage({
|
||||
content,
|
||||
functionCalls = [],
|
||||
messageIndex,
|
||||
expandedFunctionCalls,
|
||||
onToggle,
|
||||
isStreaming = false,
|
||||
showForkButton = false,
|
||||
onFork,
|
||||
isCompleted = false,
|
||||
isInactive = false,
|
||||
animate = true,
|
||||
delay = 0.2,
|
||||
}: AssistantMessageProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={animate ? { opacity: 0, y: -20 } : { opacity: 1, y: 0 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={
|
||||
animate
|
||||
? { duration: 0.4, delay: delay, ease: "easeOut" }
|
||||
: { duration: 0 }
|
||||
}
|
||||
className={isCompleted ? "opacity-50" : ""}
|
||||
>
|
||||
<Message
|
||||
icon={
|
||||
<div className="w-8 h-8 flex items-center justify-center flex-shrink-0 select-none">
|
||||
<DogIcon
|
||||
className="h-6 w-6 transition-colors duration-300"
|
||||
disabled={isCompleted || isInactive}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
showForkButton && onFork ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onFork}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-accent rounded text-muted-foreground hover:text-foreground"
|
||||
title="Fork conversation from here"
|
||||
>
|
||||
<GitBranch className="h-3 w-3" />
|
||||
</button>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<FunctionCalls
|
||||
functionCalls={functionCalls}
|
||||
messageIndex={messageIndex}
|
||||
expandedFunctionCalls={expandedFunctionCalls}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
<div className="relative">
|
||||
<MarkdownRenderer
|
||||
className={cn(
|
||||
"text-sm py-1.5 transition-colors duration-300",
|
||||
isCompleted ? "text-placeholder-foreground" : "text-foreground",
|
||||
)}
|
||||
chatMessage={
|
||||
isStreaming
|
||||
? (content.trim()
|
||||
? content +
|
||||
' <span class="inline-block w-1 h-4 bg-primary ml-1 animate-pulse"></span>'
|
||||
: '<span class="text-muted-foreground italic">Thinking<span class="thinking-dots"></span></span>')
|
||||
: content
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Message>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
210
frontend/src/app/onboarding/components/ibm-onboarding.tsx
Normal file
210
frontend/src/app/onboarding/components/ibm-onboarding.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LabelInput } from "@/components/label-input";
|
||||
import { LabelWrapper } from "@/components/label-wrapper";
|
||||
import IBMLogo from "@/components/logo/ibm-logo";
|
||||
import { useDebouncedValue } from "@/lib/debounce";
|
||||
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
||||
import { useGetIBMModelsQuery } from "../../api/queries/useGetModelsQuery";
|
||||
import { useModelSelection } from "../hooks/useModelSelection";
|
||||
import { useUpdateSettings } from "../hooks/useUpdateSettings";
|
||||
import { AdvancedOnboarding } from "./advanced";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
|
||||
export function IBMOnboarding({
|
||||
isEmbedding = false,
|
||||
setSettings,
|
||||
sampleDataset,
|
||||
setSampleDataset,
|
||||
setIsLoadingModels,
|
||||
alreadyConfigured = false,
|
||||
}: {
|
||||
isEmbedding?: boolean;
|
||||
setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
|
||||
sampleDataset: boolean;
|
||||
setSampleDataset: (dataset: boolean) => void;
|
||||
setIsLoadingModels?: (isLoading: boolean) => void;
|
||||
alreadyConfigured?: boolean;
|
||||
}) {
|
||||
const [endpoint, setEndpoint] = useState(alreadyConfigured ? "" : "https://us-south.ml.cloud.ibm.com");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [projectId, setProjectId] = useState("");
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "https://us-south.ml.cloud.ibm.com",
|
||||
label: "https://us-south.ml.cloud.ibm.com",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
value: "https://eu-de.ml.cloud.ibm.com",
|
||||
label: "https://eu-de.ml.cloud.ibm.com",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
value: "https://eu-gb.ml.cloud.ibm.com",
|
||||
label: "https://eu-gb.ml.cloud.ibm.com",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
value: "https://au-syd.ml.cloud.ibm.com",
|
||||
label: "https://au-syd.ml.cloud.ibm.com",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
value: "https://jp-tok.ml.cloud.ibm.com",
|
||||
label: "https://jp-tok.ml.cloud.ibm.com",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
value: "https://ca-tor.ml.cloud.ibm.com",
|
||||
label: "https://ca-tor.ml.cloud.ibm.com",
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
const debouncedEndpoint = useDebouncedValue(endpoint, 500);
|
||||
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
||||
const debouncedProjectId = useDebouncedValue(projectId, 500);
|
||||
|
||||
// Fetch models from API when all credentials are provided
|
||||
const {
|
||||
data: modelsData,
|
||||
isLoading: isLoadingModels,
|
||||
error: modelsError,
|
||||
} = useGetIBMModelsQuery(
|
||||
{
|
||||
endpoint: debouncedEndpoint ? debouncedEndpoint : undefined,
|
||||
apiKey: debouncedApiKey ? debouncedApiKey : undefined,
|
||||
projectId: debouncedProjectId ? debouncedProjectId : undefined,
|
||||
},
|
||||
{ enabled: !!debouncedEndpoint || !!debouncedApiKey || !!debouncedProjectId || alreadyConfigured },
|
||||
);
|
||||
|
||||
// Use custom hook for model selection logic
|
||||
const {
|
||||
languageModel,
|
||||
embeddingModel,
|
||||
setLanguageModel,
|
||||
setEmbeddingModel,
|
||||
languageModels,
|
||||
embeddingModels,
|
||||
} = useModelSelection(modelsData, isEmbedding);
|
||||
const handleSampleDatasetChange = (dataset: boolean) => {
|
||||
setSampleDataset(dataset);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingModels?.(isLoadingModels);
|
||||
}, [isLoadingModels, setIsLoadingModels]);
|
||||
|
||||
// Update settings when values change
|
||||
useUpdateSettings(
|
||||
"watsonx",
|
||||
{
|
||||
endpoint,
|
||||
apiKey,
|
||||
projectId,
|
||||
languageModel,
|
||||
embeddingModel,
|
||||
},
|
||||
setSettings,
|
||||
isEmbedding,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<LabelWrapper
|
||||
label="watsonx.ai API Endpoint"
|
||||
helperText="Base URL of the API"
|
||||
id="api-endpoint"
|
||||
required
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<ModelSelector
|
||||
options={alreadyConfigured ? [] : options}
|
||||
value={endpoint}
|
||||
custom
|
||||
onValueChange={alreadyConfigured ? () => {} : setEndpoint}
|
||||
searchPlaceholder="Search endpoint..."
|
||||
noOptionsPlaceholder={
|
||||
alreadyConfigured
|
||||
? "https://•••••••••••••••••••••••••••••••••••••••••"
|
||||
: "No endpoints available"
|
||||
}
|
||||
placeholder="Select endpoint..."
|
||||
/>
|
||||
{alreadyConfigured && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Reusing endpoint from model provider selection.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</LabelWrapper>
|
||||
|
||||
<div className="space-y-1">
|
||||
<LabelInput
|
||||
label="watsonx Project ID"
|
||||
helperText="Project ID for the model"
|
||||
id="project-id"
|
||||
required
|
||||
placeholder={
|
||||
alreadyConfigured ? "••••••••••••••••••••••••" : "your-project-id"
|
||||
}
|
||||
value={projectId}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
disabled={alreadyConfigured}
|
||||
/>
|
||||
{alreadyConfigured && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Reusing project ID from model provider selection.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<LabelInput
|
||||
label="watsonx API key"
|
||||
helperText="API key to access watsonx.ai"
|
||||
id="api-key"
|
||||
type="password"
|
||||
required
|
||||
placeholder={
|
||||
alreadyConfigured
|
||||
? "•••••••••••••••••••••••••••••••••••••••••"
|
||||
: "your-api-key"
|
||||
}
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
disabled={alreadyConfigured}
|
||||
/>
|
||||
{alreadyConfigured && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Reusing API key from model provider selection.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isLoadingModels && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Validating configuration...
|
||||
</p>
|
||||
)}
|
||||
{modelsError && (
|
||||
<p className="text-mmd text-accent-amber-foreground">
|
||||
Connection failed. Check your configuration.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<AdvancedOnboarding
|
||||
icon={<IBMLogo className="w-4 h-4" />}
|
||||
languageModels={languageModels}
|
||||
embeddingModels={embeddingModels}
|
||||
languageModel={languageModel}
|
||||
embeddingModel={embeddingModel}
|
||||
sampleDataset={sampleDataset}
|
||||
setLanguageModel={setLanguageModel}
|
||||
setEmbeddingModel={setEmbeddingModel}
|
||||
setSampleDataset={handleSampleDatasetChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
174
frontend/src/app/onboarding/components/ollama-onboarding.tsx
Normal file
174
frontend/src/app/onboarding/components/ollama-onboarding.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LabelInput } from "@/components/label-input";
|
||||
import { LabelWrapper } from "@/components/label-wrapper";
|
||||
import OllamaLogo from "@/components/logo/ollama-logo";
|
||||
import { useDebouncedValue } from "@/lib/debounce";
|
||||
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
||||
import { useGetOllamaModelsQuery } from "../../api/queries/useGetModelsQuery";
|
||||
import { useModelSelection } from "../hooks/useModelSelection";
|
||||
import { useUpdateSettings } from "../hooks/useUpdateSettings";
|
||||
import { ModelSelector } from "./model-selector";
|
||||
|
||||
export function OllamaOnboarding({
|
||||
setSettings,
|
||||
sampleDataset,
|
||||
setSampleDataset,
|
||||
setIsLoadingModels,
|
||||
isEmbedding = false,
|
||||
alreadyConfigured = false,
|
||||
}: {
|
||||
setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
|
||||
sampleDataset: boolean;
|
||||
setSampleDataset: (dataset: boolean) => void;
|
||||
setIsLoadingModels?: (isLoading: boolean) => void;
|
||||
isEmbedding?: boolean;
|
||||
alreadyConfigured?: boolean;
|
||||
}) {
|
||||
const [endpoint, setEndpoint] = useState(alreadyConfigured ? undefined : `http://localhost:11434`);
|
||||
const [showConnecting, setShowConnecting] = useState(false);
|
||||
const debouncedEndpoint = useDebouncedValue(endpoint, 500);
|
||||
|
||||
// Fetch models from API when endpoint is provided (debounced)
|
||||
const {
|
||||
data: modelsData,
|
||||
isLoading: isLoadingModels,
|
||||
error: modelsError,
|
||||
} = useGetOllamaModelsQuery(
|
||||
debouncedEndpoint ? { endpoint: debouncedEndpoint } : undefined,
|
||||
{ enabled: !!debouncedEndpoint || alreadyConfigured },
|
||||
);
|
||||
|
||||
// Use custom hook for model selection logic
|
||||
const {
|
||||
languageModel,
|
||||
embeddingModel,
|
||||
setLanguageModel,
|
||||
setEmbeddingModel,
|
||||
languageModels,
|
||||
embeddingModels,
|
||||
} = useModelSelection(modelsData, isEmbedding);
|
||||
|
||||
// Handle delayed display of connecting state
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
if (debouncedEndpoint && isLoadingModels) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setIsLoadingModels?.(true);
|
||||
setShowConnecting(true);
|
||||
}, 500);
|
||||
} else {
|
||||
setShowConnecting(false);
|
||||
setIsLoadingModels?.(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [debouncedEndpoint, isLoadingModels, setIsLoadingModels]);
|
||||
|
||||
// Update settings when values change
|
||||
useUpdateSettings(
|
||||
"ollama",
|
||||
{
|
||||
endpoint,
|
||||
languageModel,
|
||||
embeddingModel,
|
||||
},
|
||||
setSettings,
|
||||
isEmbedding,
|
||||
);
|
||||
|
||||
// Check validation state based on models query
|
||||
const hasConnectionError = debouncedEndpoint && modelsError;
|
||||
const hasNoModels =
|
||||
modelsData &&
|
||||
!modelsData.language_models?.length &&
|
||||
!modelsData.embedding_models?.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<LabelInput
|
||||
label="Ollama Base URL"
|
||||
helperText="Base URL of your Ollama server"
|
||||
id="api-endpoint"
|
||||
required
|
||||
placeholder={
|
||||
alreadyConfigured
|
||||
? "http://••••••••••••••••••••"
|
||||
: "http://localhost:11434"
|
||||
}
|
||||
value={endpoint}
|
||||
onChange={(e) => setEndpoint(e.target.value)}
|
||||
disabled={alreadyConfigured}
|
||||
/>
|
||||
{alreadyConfigured && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Reusing endpoint from model provider selection.
|
||||
</p>
|
||||
)}
|
||||
{showConnecting && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Connecting to Ollama server...
|
||||
</p>
|
||||
)}
|
||||
{hasConnectionError && (
|
||||
<p className="text-mmd text-accent-amber-foreground">
|
||||
Can't reach Ollama at {debouncedEndpoint}. Update the base URL or
|
||||
start the server.
|
||||
</p>
|
||||
)}
|
||||
{hasNoModels && (
|
||||
<p className="text-mmd text-accent-amber-foreground">
|
||||
No models found. Install embedding and agent models on your Ollama
|
||||
server.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isEmbedding && setEmbeddingModel && (
|
||||
<LabelWrapper
|
||||
label="Embedding model"
|
||||
helperText="Model used for knowledge ingest and retrieval"
|
||||
id="embedding-model"
|
||||
required={true}
|
||||
>
|
||||
<ModelSelector
|
||||
options={embeddingModels}
|
||||
icon={<OllamaLogo className="w-4 h-4" />}
|
||||
noOptionsPlaceholder={
|
||||
isLoadingModels
|
||||
? "Loading models..."
|
||||
: "No embedding models detected. Install an embedding model to continue."
|
||||
}
|
||||
value={embeddingModel}
|
||||
onValueChange={setEmbeddingModel}
|
||||
/>
|
||||
</LabelWrapper>
|
||||
)}
|
||||
{!isEmbedding && setLanguageModel && (
|
||||
<LabelWrapper
|
||||
label="Language model"
|
||||
helperText="Model used for chat"
|
||||
id="embedding-model"
|
||||
required={true}
|
||||
>
|
||||
<ModelSelector
|
||||
options={languageModels}
|
||||
icon={<OllamaLogo className="w-4 h-4" />}
|
||||
noOptionsPlaceholder={
|
||||
isLoadingModels
|
||||
? "Loading models..."
|
||||
: "No language models detected. Install a language model to continue."
|
||||
}
|
||||
value={languageModel}
|
||||
onValueChange={setLanguageModel}
|
||||
/>
|
||||
</LabelWrapper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
168
frontend/src/app/onboarding/components/openai-onboarding.tsx
Normal file
168
frontend/src/app/onboarding/components/openai-onboarding.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LabelInput } from "@/components/label-input";
|
||||
import { LabelWrapper } from "@/components/label-wrapper";
|
||||
import OpenAILogo from "@/components/logo/openai-logo";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useDebouncedValue } from "@/lib/debounce";
|
||||
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
|
||||
import { useGetOpenAIModelsQuery } from "../../api/queries/useGetModelsQuery";
|
||||
import { useModelSelection } from "../hooks/useModelSelection";
|
||||
import { useUpdateSettings } from "../hooks/useUpdateSettings";
|
||||
import { AdvancedOnboarding } from "./advanced";
|
||||
|
||||
export function OpenAIOnboarding({
|
||||
setSettings,
|
||||
sampleDataset,
|
||||
setSampleDataset,
|
||||
setIsLoadingModels,
|
||||
isEmbedding = false,
|
||||
hasEnvApiKey = false,
|
||||
alreadyConfigured = false,
|
||||
}: {
|
||||
setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
|
||||
sampleDataset: boolean;
|
||||
setSampleDataset: (dataset: boolean) => void;
|
||||
setIsLoadingModels?: (isLoading: boolean) => void;
|
||||
isEmbedding?: boolean;
|
||||
hasEnvApiKey?: boolean;
|
||||
alreadyConfigured?: boolean;
|
||||
}) {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [getFromEnv, setGetFromEnv] = useState(hasEnvApiKey && !alreadyConfigured);
|
||||
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
||||
|
||||
// Fetch models from API when API key is provided
|
||||
const {
|
||||
data: modelsData,
|
||||
isLoading: isLoadingModels,
|
||||
error: modelsError,
|
||||
} = useGetOpenAIModelsQuery(
|
||||
getFromEnv
|
||||
? { apiKey: "" }
|
||||
: debouncedApiKey
|
||||
? { apiKey: debouncedApiKey }
|
||||
: undefined,
|
||||
{ enabled: debouncedApiKey !== "" || getFromEnv || alreadyConfigured },
|
||||
);
|
||||
// Use custom hook for model selection logic
|
||||
const {
|
||||
languageModel,
|
||||
embeddingModel,
|
||||
setLanguageModel,
|
||||
setEmbeddingModel,
|
||||
languageModels,
|
||||
embeddingModels,
|
||||
} = useModelSelection(modelsData, isEmbedding);
|
||||
const handleSampleDatasetChange = (dataset: boolean) => {
|
||||
setSampleDataset(dataset);
|
||||
};
|
||||
|
||||
const handleGetFromEnvChange = (fromEnv: boolean) => {
|
||||
setGetFromEnv(fromEnv);
|
||||
if (fromEnv) {
|
||||
setApiKey("");
|
||||
}
|
||||
setEmbeddingModel?.("");
|
||||
setLanguageModel?.("");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingModels?.(isLoadingModels);
|
||||
}, [isLoadingModels, setIsLoadingModels]);
|
||||
|
||||
// Update settings when values change
|
||||
useUpdateSettings(
|
||||
"openai",
|
||||
{
|
||||
apiKey,
|
||||
languageModel,
|
||||
embeddingModel,
|
||||
},
|
||||
setSettings,
|
||||
isEmbedding,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-5">
|
||||
{!alreadyConfigured && (
|
||||
<LabelWrapper
|
||||
label="Use environment OpenAI API key"
|
||||
id="get-api-key"
|
||||
description="Reuse the key from your environment config. Turn off to enter a different key."
|
||||
flex
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Switch
|
||||
checked={getFromEnv}
|
||||
onCheckedChange={handleGetFromEnvChange}
|
||||
disabled={!hasEnvApiKey}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!hasEnvApiKey && (
|
||||
<TooltipContent>
|
||||
OpenAI API key not detected in the environment.
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</LabelWrapper>
|
||||
)}
|
||||
{(!getFromEnv || alreadyConfigured) && (
|
||||
<div className="space-y-1">
|
||||
<LabelInput
|
||||
label="OpenAI API key"
|
||||
helperText="The API key for your OpenAI account."
|
||||
className={modelsError ? "!border-destructive" : ""}
|
||||
id="api-key"
|
||||
type="password"
|
||||
required
|
||||
placeholder={
|
||||
alreadyConfigured
|
||||
? "sk-•••••••••••••••••••••••••••••••••••••••••"
|
||||
: "sk-..."
|
||||
}
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
disabled={alreadyConfigured}
|
||||
/>
|
||||
{alreadyConfigured && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Reusing key from model provider selection.
|
||||
</p>
|
||||
)}
|
||||
{isLoadingModels && (
|
||||
<p className="text-mmd text-muted-foreground">
|
||||
Validating API key...
|
||||
</p>
|
||||
)}
|
||||
{modelsError && (
|
||||
<p className="text-mmd text-destructive">
|
||||
Invalid OpenAI API key. Verify or replace the key.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AdvancedOnboarding
|
||||
icon={<OpenAILogo className="w-4 h-4" />}
|
||||
languageModels={languageModels}
|
||||
embeddingModels={embeddingModels}
|
||||
languageModel={languageModel}
|
||||
embeddingModel={embeddingModel}
|
||||
sampleDataset={sampleDataset}
|
||||
setLanguageModel={setLanguageModel}
|
||||
setSampleDataset={handleSampleDatasetChange}
|
||||
setEmbeddingModel={setEmbeddingModel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
|
|||
}
|
||||
|
||||
# Simple tool calling test
|
||||
payload = {
|
||||
base_payload = {
|
||||
"model": llm_model,
|
||||
"messages": [
|
||||
{"role": "user", "content": "What tools do you have available?"}
|
||||
|
|
@ -136,10 +136,11 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
|
|||
}
|
||||
}
|
||||
],
|
||||
"max_tokens": 50,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Try with max_tokens first
|
||||
payload = {**base_payload, "max_tokens": 50}
|
||||
response = await client.post(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
headers=headers,
|
||||
|
|
@ -147,6 +148,17 @@ async def _test_openai_completion_with_tools(api_key: str, llm_model: str) -> No
|
|||
timeout=30.0,
|
||||
)
|
||||
|
||||
# If max_tokens doesn't work, try with max_completion_tokens
|
||||
if response.status_code != 200:
|
||||
logger.info("max_tokens parameter failed, trying max_completion_tokens instead")
|
||||
payload = {**base_payload, "max_completion_tokens": 50}
|
||||
response = await client.post(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"OpenAI completion test failed: {response.status_code} - {response.text}")
|
||||
raise Exception(f"OpenAI API error: {response.status_code}")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import httpx
|
||||
from typing import Dict, List
|
||||
from api.provider_validation import test_embedding
|
||||
from utils.container_utils import transform_localhost_url
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
|
|
@ -229,20 +228,14 @@ class ModelsService:
|
|||
f"Model: {model_name}, Capabilities: {capabilities}"
|
||||
)
|
||||
|
||||
# Check if model has required capabilities
|
||||
# Check if model has embedding capability
|
||||
has_embedding = "embedding" in capabilities
|
||||
# Check if model has required capabilities for language models
|
||||
has_completion = DESIRED_CAPABILITY in capabilities
|
||||
has_tools = TOOL_CALLING_CAPABILITY in capabilities
|
||||
|
||||
# Check if it's an embedding model
|
||||
try:
|
||||
await test_embedding("ollama", endpoint=endpoint, embedding_model=model_name)
|
||||
is_embedding = True
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to test embedding for model {model_name}: {str(e)}")
|
||||
is_embedding = False
|
||||
|
||||
if is_embedding:
|
||||
# Embedding models only need completion capability
|
||||
if has_embedding:
|
||||
# Embedding models have embedding capability
|
||||
embedding_models.append(
|
||||
{
|
||||
"value": model_name,
|
||||
|
|
@ -250,7 +243,7 @@ class ModelsService:
|
|||
"default": "nomic-embed-text" in model_name.lower(),
|
||||
}
|
||||
)
|
||||
elif not is_embedding and has_completion and has_tools:
|
||||
if has_completion and has_tools:
|
||||
# Language models need both completion and tool calling
|
||||
language_models.append(
|
||||
{
|
||||
|
|
@ -333,34 +326,6 @@ class ModelsService:
|
|||
if project_id:
|
||||
headers["Project-ID"] = project_id
|
||||
|
||||
# Validate credentials with a minimal completion request
|
||||
async with httpx.AsyncClient() as client:
|
||||
validation_url = f"{watson_endpoint}/ml/v1/text/generation"
|
||||
validation_params = {"version": "2024-09-16"}
|
||||
validation_payload = {
|
||||
"input": "test",
|
||||
"model_id": "ibm/granite-3-2b-instruct",
|
||||
"project_id": project_id,
|
||||
"parameters": {
|
||||
"max_new_tokens": 1,
|
||||
},
|
||||
}
|
||||
|
||||
validation_response = await client.post(
|
||||
validation_url,
|
||||
headers=headers,
|
||||
params=validation_params,
|
||||
json=validation_payload,
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
if validation_response.status_code != 200:
|
||||
raise Exception(
|
||||
f"Invalid credentials or endpoint: {validation_response.status_code} - {validation_response.text}"
|
||||
)
|
||||
|
||||
logger.info("IBM Watson credentials validated successfully")
|
||||
|
||||
# Fetch foundation models using the correct endpoint
|
||||
models_url = f"{watson_endpoint}/ml/v1/foundation_model_specs"
|
||||
|
||||
|
|
@ -424,6 +389,39 @@ class ModelsService:
|
|||
}
|
||||
)
|
||||
|
||||
# Validate credentials with the first available LLM model
|
||||
if language_models:
|
||||
first_llm_model = language_models[0]["value"]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
validation_url = f"{watson_endpoint}/ml/v1/text/generation"
|
||||
validation_params = {"version": "2024-09-16"}
|
||||
validation_payload = {
|
||||
"input": "test",
|
||||
"model_id": first_llm_model,
|
||||
"project_id": project_id,
|
||||
"parameters": {
|
||||
"max_new_tokens": 1,
|
||||
},
|
||||
}
|
||||
|
||||
validation_response = await client.post(
|
||||
validation_url,
|
||||
headers=headers,
|
||||
params=validation_params,
|
||||
json=validation_payload,
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
if validation_response.status_code != 200:
|
||||
raise Exception(
|
||||
f"Invalid credentials or endpoint: {validation_response.status_code} - {validation_response.text}"
|
||||
)
|
||||
|
||||
logger.info(f"IBM Watson credentials validated successfully using model: {first_llm_model}")
|
||||
else:
|
||||
logger.warning("No language models available to validate credentials")
|
||||
|
||||
if not language_models and not embedding_models:
|
||||
raise Exception("No IBM models retrieved from API")
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class DoclingManager:
|
|||
# Bind to all interfaces by default (can be overridden with DOCLING_BIND_HOST env var)
|
||||
self._host = os.getenv('DOCLING_BIND_HOST', '0.0.0.0')
|
||||
self._running = False
|
||||
self._starting = False
|
||||
self._external_process = False
|
||||
|
||||
# PID file to track docling-serve across sessions (in current working directory)
|
||||
|
|
@ -126,6 +127,7 @@ class DoclingManager:
|
|||
if self._process is not None and self._process.poll() is None:
|
||||
self._running = True
|
||||
self._external_process = False
|
||||
self._starting = False # Clear starting flag if service is running
|
||||
return True
|
||||
|
||||
# Check if we have a PID from file
|
||||
|
|
@ -133,6 +135,7 @@ class DoclingManager:
|
|||
if pid is not None and self._is_process_running(pid):
|
||||
self._running = True
|
||||
self._external_process = True
|
||||
self._starting = False # Clear starting flag if service is running
|
||||
return True
|
||||
|
||||
# No running process found
|
||||
|
|
@ -142,6 +145,19 @@ class DoclingManager:
|
|||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get current status of docling serve."""
|
||||
# Check for starting state first
|
||||
if self._starting:
|
||||
display_host = "localhost" if self._host == "0.0.0.0" else self._host
|
||||
return {
|
||||
"status": "starting",
|
||||
"port": self._port,
|
||||
"host": self._host,
|
||||
"endpoint": None,
|
||||
"docs_url": None,
|
||||
"ui_url": None,
|
||||
"pid": None
|
||||
}
|
||||
|
||||
if self.is_running():
|
||||
# Try to get PID from process handle first, then from PID file
|
||||
pid = None
|
||||
|
|
@ -196,6 +212,9 @@ class DoclingManager:
|
|||
except Exception as e:
|
||||
self._add_log_entry(f"Error checking port availability: {e}")
|
||||
|
||||
# Set starting flag to show "Starting" status in UI
|
||||
self._starting = True
|
||||
|
||||
# Clear log buffer when starting
|
||||
self._log_buffer = []
|
||||
self._add_log_entry("Starting docling serve as external process...")
|
||||
|
|
@ -261,6 +280,8 @@ class DoclingManager:
|
|||
|
||||
if result == 0:
|
||||
self._add_log_entry(f"Docling-serve is now listening on {self._host}:{self._port}")
|
||||
# Service is now running, clear starting flag
|
||||
self._starting = False
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
|
@ -294,16 +315,24 @@ class DoclingManager:
|
|||
self._add_log_entry(f"Error reading final output: {e}")
|
||||
|
||||
self._running = False
|
||||
self._starting = False
|
||||
return False, f"Docling serve process exited immediately (code: {return_code})"
|
||||
|
||||
# If we get here and the process is still running but not listening yet,
|
||||
# clear the starting flag anyway (it's running, just not ready)
|
||||
if self._process.poll() is None:
|
||||
self._starting = False
|
||||
|
||||
display_host = "localhost" if self._host == "0.0.0.0" else self._host
|
||||
return True, f"Docling serve starting on http://{display_host}:{port}"
|
||||
|
||||
except FileNotFoundError:
|
||||
self._starting = False
|
||||
return False, "docling-serve not available. Please install: uv add docling-serve"
|
||||
except Exception as e:
|
||||
self._running = False
|
||||
self._process = None
|
||||
self._starting = False
|
||||
return False, f"Error starting docling serve: {str(e)}"
|
||||
|
||||
def _start_output_capture(self):
|
||||
|
|
|
|||
|
|
@ -206,10 +206,21 @@ class MonitorScreen(Screen):
|
|||
|
||||
# Add docling serve to its own table
|
||||
docling_status = self.docling_manager.get_status()
|
||||
docling_running = docling_status["status"] == "running"
|
||||
docling_status_text = "running" if docling_running else "stopped"
|
||||
docling_style = "bold green" if docling_running else "bold red"
|
||||
docling_port = f"{docling_status['host']}:{docling_status['port']}" if docling_running else "N/A"
|
||||
docling_status_value = docling_status["status"]
|
||||
docling_running = docling_status_value == "running"
|
||||
docling_starting = docling_status_value == "starting"
|
||||
|
||||
if docling_running:
|
||||
docling_status_text = "running"
|
||||
docling_style = "bold green"
|
||||
elif docling_starting:
|
||||
docling_status_text = "starting"
|
||||
docling_style = "bold yellow"
|
||||
else:
|
||||
docling_status_text = "stopped"
|
||||
docling_style = "bold red"
|
||||
|
||||
docling_port = f"{docling_status['host']}:{docling_status['port']}" if (docling_running or docling_starting) else "N/A"
|
||||
docling_pid = str(docling_status.get("pid")) if docling_status.get("pid") else "N/A"
|
||||
|
||||
if self.docling_table:
|
||||
|
|
@ -375,15 +386,25 @@ class MonitorScreen(Screen):
|
|||
"""Start docling serve."""
|
||||
self.operation_in_progress = True
|
||||
try:
|
||||
success, message = await self.docling_manager.start()
|
||||
# Start the service (this sets _starting = True internally at the start)
|
||||
# Create task and let it begin executing (which sets the flag)
|
||||
start_task = asyncio.create_task(self.docling_manager.start())
|
||||
# Give it a tiny moment to set the _starting flag
|
||||
await asyncio.sleep(0.1)
|
||||
# Refresh immediately to show "Starting" status
|
||||
await self._refresh_services()
|
||||
# Now wait for start to complete
|
||||
success, message = await start_task
|
||||
if success:
|
||||
self.notify(message, severity="information")
|
||||
else:
|
||||
self.notify(f"Failed to start docling serve: {message}", severity="error")
|
||||
# Refresh the services table to show updated status
|
||||
# Refresh again to show final status (running or stopped)
|
||||
await self._refresh_services()
|
||||
except Exception as e:
|
||||
self.notify(f"Error starting docling serve: {str(e)}", severity="error")
|
||||
# Refresh on error to clear starting status
|
||||
await self._refresh_services()
|
||||
finally:
|
||||
self.operation_in_progress = False
|
||||
|
||||
|
|
@ -646,7 +667,11 @@ class MonitorScreen(Screen):
|
|||
suffix = f"-{random.randint(10000, 99999)}"
|
||||
|
||||
# Add docling serve controls
|
||||
docling_running = self.docling_manager.is_running()
|
||||
docling_status = self.docling_manager.get_status()
|
||||
docling_status_value = docling_status["status"]
|
||||
docling_running = docling_status_value == "running"
|
||||
docling_starting = docling_status_value == "starting"
|
||||
|
||||
if docling_running:
|
||||
docling_controls.mount(
|
||||
Button("Stop", variant="error", id=f"docling-stop-btn{suffix}")
|
||||
|
|
@ -654,6 +679,11 @@ class MonitorScreen(Screen):
|
|||
docling_controls.mount(
|
||||
Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}")
|
||||
)
|
||||
elif docling_starting:
|
||||
# Show disabled button or no button when starting
|
||||
start_btn = Button("Starting...", variant="warning", id=f"docling-start-btn{suffix}")
|
||||
start_btn.disabled = True
|
||||
docling_controls.mount(start_btn)
|
||||
else:
|
||||
docling_controls.mount(
|
||||
Button("Start", variant="success", id=f"docling-start-btn{suffix}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue