feat: ollama check (#803)
This commit is contained in:
@@ -1,24 +1,38 @@
|
|||||||
import { Provider, ProviderResponse } from './types';
|
import { Provider, ProviderResponse } from './types';
|
||||||
import { getApiUrl, getSecretKey } from '../../../config';
|
import { getApiUrl, getSecretKey } from '../../../config';
|
||||||
|
import { special_provider_cases } from '../providers/utils';
|
||||||
|
|
||||||
export async function getActiveProviders(): Promise<string[]> {
|
export async function getActiveProviders(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
// Fetch the secrets settings
|
// Fetch the secrets settings
|
||||||
const secretsSettings = await getSecretsSettings();
|
const secretsSettings = await getSecretsSettings();
|
||||||
|
|
||||||
|
// Check for special provider cases (e.g. ollama needs to be installed in Applications folder)
|
||||||
|
const specialCasesResults = await Promise.all(
|
||||||
|
Object.entries(special_provider_cases).map(async ([providerName, checkFunction]) => {
|
||||||
|
const isActive = await checkFunction(); // Dynamically re-check status
|
||||||
|
console.log(`Special case result for ${providerName}:`, isActive);
|
||||||
|
return isActive ? providerName : null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Extract active providers based on `is_set` in `secret_status` or providers with no keys
|
// Extract active providers based on `is_set` in `secret_status` or providers with no keys
|
||||||
const activeProviders = Object.values(secretsSettings) // Convert object to array
|
const activeProviders = Object.values(secretsSettings) // Convert object to array
|
||||||
.filter((provider) => {
|
.filter((provider) => {
|
||||||
const apiKeyStatus = Object.values(provider.secret_status || {}); // Get all key statuses
|
const apiKeyStatus = Object.values(provider.secret_status || {}); // Get all key statuses
|
||||||
|
|
||||||
// Include providers if:
|
// Include providers if:
|
||||||
// - They have at least one key set (`is_set: true`), OR
|
// - They have at least one key set (`is_set: true`)
|
||||||
// - They have no keys (`secret_status` is empty or undefined)
|
return apiKeyStatus.some((key) => key.is_set);
|
||||||
return apiKeyStatus.some((key) => key.is_set) || apiKeyStatus.length === 0;
|
|
||||||
})
|
})
|
||||||
.map((provider) => provider.name || 'Unknown Provider'); // Extract provider name
|
.map((provider) => provider.name || 'Unknown Provider'); // Extract provider name
|
||||||
|
|
||||||
return activeProviders;
|
// Combine active providers from secrets settings and special cases
|
||||||
|
const allActiveProviders = [
|
||||||
|
...activeProviders,
|
||||||
|
...specialCasesResults.filter((provider) => provider !== null), // Filter out null results
|
||||||
|
];
|
||||||
|
return allActiveProviders;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get active providers:', error);
|
console.error('Failed to get active providers:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -109,15 +109,3 @@ export const provider_aliases = [
|
|||||||
{ provider: 'OpenRouter', alias: 'openrouter' },
|
{ provider: 'OpenRouter', alias: 'openrouter' },
|
||||||
{ provider: 'Google', alias: 'google' },
|
{ provider: 'Google', alias: 'google' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const recent_models = [
|
|
||||||
{ name: 'gpt-4o', provider: 'OpenAI', lastUsed: '2 hours ago' },
|
|
||||||
{ name: 'claude-3-5-sonnet-latest', provider: 'Anthropic', lastUsed: 'Yesterday' },
|
|
||||||
{ name: 'o1-mini', provider: 'OpenAI', lastUsed: '3 days ago' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const model_env_vars = {
|
|
||||||
OpenAI: 'OPENAI_MODEL',
|
|
||||||
Anthropic: 'ANTHROPIC_MODEL',
|
|
||||||
Databricks: 'DATABRICKS_MODEL',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Check, Plus, Settings, X, Rocket } from 'lucide-react';
|
import { Check, Plus, Settings, X, Rocket, RefreshCw, AlertCircle } from 'lucide-react';
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from '../../ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip';
|
||||||
import { Portal } from '@radix-ui/react-portal';
|
import { Portal } from '@radix-ui/react-portal';
|
||||||
import { required_keys } from '../models/hardcoded_stuff';
|
import { required_keys } from '../models/hardcoded_stuff';
|
||||||
|
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||||
|
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
|
||||||
|
import { getActiveProviders } from '../api_keys/utils';
|
||||||
|
|
||||||
// Common interfaces and helper functions
|
// Common interfaces and helper functions
|
||||||
interface Provider {
|
interface Provider {
|
||||||
@@ -27,6 +30,7 @@ interface BaseProviderCardProps {
|
|||||||
showDelete?: boolean;
|
showDelete?: boolean;
|
||||||
hasRequiredKeys?: boolean;
|
hasRequiredKeys?: boolean;
|
||||||
onTakeoff?: () => void;
|
onTakeoff?: () => void;
|
||||||
|
showTakeoff?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArticle(word: string): string {
|
function getArticle(word: string): string {
|
||||||
@@ -60,9 +64,11 @@ function BaseProviderCard({
|
|||||||
showDelete = false,
|
showDelete = false,
|
||||||
hasRequiredKeys = false,
|
hasRequiredKeys = false,
|
||||||
onTakeoff,
|
onTakeoff,
|
||||||
|
showTakeoff,
|
||||||
}: BaseProviderCardProps) {
|
}: BaseProviderCardProps) {
|
||||||
const numRequiredKeys = required_keys[name]?.length || 0;
|
const numRequiredKeys = required_keys[name]?.length || 0;
|
||||||
const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`;
|
const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`;
|
||||||
|
const { activeKeys, setActiveKeys } = useActiveKeys();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle hover:bg-transparent hover:duration-300">
|
<div className="relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle hover:bg-transparent hover:duration-300">
|
||||||
@@ -85,6 +91,7 @@ function BaseProviderCard({
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h3 className="text-base font-medium text-textStandard truncate mr-2">{name}</h3>
|
<h3 className="text-base font-medium text-textStandard truncate mr-2">{name}</h3>
|
||||||
|
|
||||||
|
{/* Configured state: Green check */}
|
||||||
{isConfigured && (
|
{isConfigured && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -98,7 +105,36 @@ function BaseProviderCard({
|
|||||||
<p>
|
<p>
|
||||||
{hasRequiredKeys
|
{hasRequiredKeys
|
||||||
? `You have ${getArticle(name)} ${name} API Key set in your environment`
|
? `You have ${getArticle(name)} ${name} API Key set in your environment`
|
||||||
: `${name} has no required API keys`}
|
: `${name} is installed and running on your machine`}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Not Configured state: Red exclamation mark for Ollama */}
|
||||||
|
{!isConfigured && name === 'Ollama' && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-bgApp hover:bg-bgApp shadow-none text-textSubtle border border-borderSubtle hover:border-borderStandard hover:text-textStandard transition-colors">
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<Portal>
|
||||||
|
<TooltipContent side="top" align="center" className="z-[9999]">
|
||||||
|
<p>
|
||||||
|
To use, the{' '}
|
||||||
|
<a
|
||||||
|
href="https://ollama.com/download"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 underline hover:text-blue-800"
|
||||||
|
>
|
||||||
|
Ollama app
|
||||||
|
</a>{' '}
|
||||||
|
must be installed on your machine and open.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Portal>
|
</Portal>
|
||||||
@@ -106,7 +142,6 @@ function BaseProviderCard({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-textSubtle mt-1.5 mb-3 leading-normal overflow-y-auto max-h-[54px] ">
|
<p className="text-xs text-textSubtle mt-1.5 mb-3 leading-normal overflow-y-auto max-h-[54px] ">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
@@ -114,6 +149,42 @@ function BaseProviderCard({
|
|||||||
|
|
||||||
<div className="space-x-2 text-center flex items-center justify-between">
|
<div className="space-x-2 text-center flex items-center justify-between">
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
|
{!isConfigured && name === 'Ollama' && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Trigger a refresh of active keys
|
||||||
|
const refreshActiveKeys = async () => {
|
||||||
|
try {
|
||||||
|
const providers = await getActiveProviders(); // Re-fetch active providers
|
||||||
|
setActiveKeys(providers); // Update the context state
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing active providers:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshActiveKeys(); // Call the refresh function
|
||||||
|
}}
|
||||||
|
className="rounded-full h-7 w-7 p-0 bg-bgApp hover:bg-bgApp shadow-none text-textSubtle border border-borderSubtle hover:border-borderStandard hover:text-textStandard transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw className="!size-4" /> {/* Refresh icon */}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<Portal>
|
||||||
|
<TooltipContent side="top" align="center" className="z-[9999]">
|
||||||
|
<p>Re-check for active Ollama app running in the background.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Default "Add Keys" Button for other providers */}
|
||||||
{!isConfigured && onAddKeys && hasRequiredKeys && (
|
{!isConfigured && onAddKeys && hasRequiredKeys && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -187,7 +258,7 @@ function BaseProviderCard({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isConfigured && onTakeoff && (
|
{isConfigured && onTakeoff && showTakeoff !== false && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -228,6 +299,7 @@ interface BaseProviderGridProps {
|
|||||||
onConfigure?: (provider: Provider) => void;
|
onConfigure?: (provider: Provider) => void;
|
||||||
onDelete?: (provider: Provider) => void;
|
onDelete?: (provider: Provider) => void;
|
||||||
onTakeoff?: (provider: Provider) => void;
|
onTakeoff?: (provider: Provider) => void;
|
||||||
|
showTakeoff?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BaseProviderGrid({
|
export function BaseProviderGrid({
|
||||||
@@ -240,6 +312,7 @@ export function BaseProviderGrid({
|
|||||||
onAddKeys,
|
onAddKeys,
|
||||||
onConfigure,
|
onConfigure,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
showTakeoff,
|
||||||
onTakeoff,
|
onTakeoff,
|
||||||
}: BaseProviderGridProps) {
|
}: BaseProviderGridProps) {
|
||||||
return (
|
return (
|
||||||
@@ -262,6 +335,7 @@ export function BaseProviderGrid({
|
|||||||
showSettings={showSettings}
|
showSettings={showSettings}
|
||||||
showDelete={showDelete}
|
showDelete={showDelete}
|
||||||
hasRequiredKeys={hasRequiredKeys}
|
hasRequiredKeys={hasRequiredKeys}
|
||||||
|
showTakeoff={showTakeoff}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export function ConfigureProvidersGrid() {
|
|||||||
onAddKeys={handleAddKeys}
|
onAddKeys={handleAddKeys}
|
||||||
onConfigure={handleConfigure}
|
onConfigure={handleConfigure}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
showTakeoff={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showSetupModal && selectedForSetup && (
|
{showSetupModal && selectedForSetup && (
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const special_provider_cases = {
|
||||||
|
Ollama: async () => await checkForOllama(), // Dynamically re-check
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function checkForOllama() {
|
||||||
|
console.log('Invoking check-ollama IPC handler...');
|
||||||
|
try {
|
||||||
|
const ollamaInstalled = await window.electron.checkForOllama();
|
||||||
|
return ollamaInstalled;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error invoking check-ollama:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
saveSettings,
|
saveSettings,
|
||||||
updateEnvironmentVariables,
|
updateEnvironmentVariables,
|
||||||
} from './utils/settings';
|
} from './utils/settings';
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
if (started) app.quit();
|
if (started) app.quit();
|
||||||
@@ -347,6 +348,37 @@ ipcMain.handle('select-file-or-directory', async () => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('check-ollama', async () => {
|
||||||
|
try {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Run `ps` and filter for "ollama"
|
||||||
|
exec('ps aux | grep -iw "[o]llama"', (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Error executing ps command:', error);
|
||||||
|
return resolve(false); // Process is not running
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error('Standard error output from ps command:', stderr);
|
||||||
|
return resolve(false); // Process is not running
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Raw stdout from ps command:', stdout);
|
||||||
|
|
||||||
|
// Trim and check if output contains a match
|
||||||
|
const trimmedOutput = stdout.trim();
|
||||||
|
console.log('Trimmed stdout:', trimmedOutput);
|
||||||
|
|
||||||
|
const isRunning = trimmedOutput.length > 0; // True if there's any output
|
||||||
|
resolve(isRunning); // Resolve true if running, false otherwise
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error checking for Ollama:', err);
|
||||||
|
return false; // Return false on error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
// Test error feature - only enabled with GOOSE_TEST_ERROR=true
|
// Test error feature - only enabled with GOOSE_TEST_ERROR=true
|
||||||
if (process.env.GOOSE_TEST_ERROR === 'true') {
|
if (process.env.GOOSE_TEST_ERROR === 'true') {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
openInChrome: (url) => ipcRenderer.send('open-in-chrome', url),
|
openInChrome: (url) => ipcRenderer.send('open-in-chrome', url),
|
||||||
fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url),
|
fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url),
|
||||||
reloadApp: () => ipcRenderer.send('reload-app'),
|
reloadApp: () => ipcRenderer.send('reload-app'),
|
||||||
|
checkForOllama: () => ipcRenderer.invoke('check-ollama'),
|
||||||
selectFileOrDirectory: () => ipcRenderer.invoke('select-file-or-directory'),
|
selectFileOrDirectory: () => ipcRenderer.invoke('select-file-or-directory'),
|
||||||
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
|
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
|
||||||
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
||||||
|
|||||||
Reference in New Issue
Block a user