From cf2786dec4f07d53253f4fa8d7f0f12556b16f00 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sun, 10 May 2026 22:33:35 +0000 Subject: [PATCH] feat: integrate Google Gemini, MiniMax, and GLM providers; fix persistent agent loading toast --- .../admin/settings/admin-settings-form.tsx | 21 +++ memento-note/components/agents/agent-card.tsx | 35 ++++- memento-note/lib/ai/factory.ts | 48 +++++++ memento-note/lib/ai/providers/google.ts | 129 ++++++++++++++++++ memento-note/lib/ai/types.ts | 3 + memento-note/package-lock.json | 46 +++++++ memento-note/package.json | 1 + 7 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 memento-note/lib/ai/providers/google.ts diff --git a/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx index ed5d273..4ab4c1a 100644 --- a/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx +++ b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx @@ -23,6 +23,9 @@ type AIProvider = | 'mistral' | 'zai' | 'lmstudio' + | 'google' + | 'minimax' + | 'glm' /** Providers that cannot be used for embeddings in Memento (no embedding API wired). */ const PROVIDERS_WITHOUT_EMBEDDINGS: AIProvider[] = ['anthropic', 'anthropic_custom'] @@ -45,6 +48,9 @@ const PROVIDER_META: Record = { zai: 'ZAI_API_KEY', lmstudio: 'LMSTUDIO_API_KEY', custom: 'CUSTOM_OPENAI_API_KEY', + google: 'GOOGLE_GENERATIVE_AI_API_KEY', + minimax: 'MINIMAX_API_KEY', + glm: 'GLM_API_KEY', } const BASE_URL_CONFIG: Record = { @@ -72,6 +81,9 @@ const BASE_URL_CONFIG: Record = { zai: '', lmstudio: 'LMSTUDIO_BASE_URL', custom: 'CUSTOM_OPENAI_BASE_URL', + google: '', + minimax: '', + glm: '', } const DEFAULT_BASE_URLS: Record = { @@ -85,6 +97,9 @@ const DEFAULT_BASE_URLS: Record = { zai: 'https://api.zukijourney.com/v1', lmstudio: 'http://localhost:1234/v1', custom: '', + google: '', + minimax: 'https://api.minimax.io/v1', + glm: 'https://open.bigmodel.ai/api/paas/v4', } // Suggested models per provider (shown as hints in Combobox - user can always type a custom name) @@ -112,6 +127,9 @@ const SUGGESTED_MODELS: Record = { deepseek: ['deepseek-chat', 'deepseek-reasoner'], mistral: ['mistral-small-latest', 'mistral-medium-latest', 'mistral-large-latest', 'codestral-latest', 'mistral-embed'], zai: ['gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini', 'claude-sonnet-4', 'gemini-2.5-flash'], + google: ['gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-1.0-pro'], + minimax: ['abab6.5-chat', 'abab6.5s-chat', 'abab5.5-chat'], + glm: ['glm-4', 'glm-4-air', 'glm-4-flash', 'glm-3-turbo'], } const SUGGESTED_EMBEDDINGS: Record = { @@ -605,6 +623,9 @@ export function AdminSettingsForm({ config }: { config: Record } { value: 'mistral', label: t('admin.ai.providerMistralOption') }, { value: 'zai', label: t('admin.ai.providerZAIOption') }, { value: 'lmstudio', label: t('admin.ai.providerLMStudioOption') }, + { value: 'google', label: 'Google Gemini' }, + { value: 'minimax', label: 'MiniMax' }, + { value: 'glm', label: 'Zhipu GLM' }, { value: 'custom', label: t('admin.ai.providerCustomOption') }, ] diff --git a/memento-note/components/agents/agent-card.tsx b/memento-note/components/agents/agent-card.tsx index 0b1502f..c15373c 100644 --- a/memento-note/components/agents/agent-card.tsx +++ b/memento-note/components/agents/agent-card.tsx @@ -93,24 +93,49 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps return } if (pollRef.current) clearInterval(pollRef.current) + + const startTime = Date.now() + const MAX_POLL_TIME = 10 * 60 * 1000 // 10 minutes + pollRef.current = setInterval(async () => { + // Safety timeout + if (Date.now() - startTime > MAX_POLL_TIME) { + if (pollRef.current) clearInterval(pollRef.current) + pollRef.current = null + setIsRunning(false) + toast.error(t('agents.toasts.runError', { error: 'Timeout after 10 minutes' }), { id: toastId, description: '' }) + return + } + try { const res = await fetch(`/api/agents/run-for-note?agentId=${agent.id}`) + if (!res.ok) throw new Error('Poll failed') + const data = await res.json() if (data.status === 'success') { - clearInterval(pollRef.current!) + if (pollRef.current) clearInterval(pollRef.current) pollRef.current = null setIsRunning(false) - toast.success(t('agents.toasts.runSuccess', { name: agent.name }), { id: toastId, duration: 6000 }) + toast.success(t('agents.toasts.runSuccess', { name: agent.name }), { + id: toastId, + duration: 6000, + description: '' // Clear the loading description + }) onRefresh() } else if (data.status === 'failure') { - clearInterval(pollRef.current!) + if (pollRef.current) clearInterval(pollRef.current) pollRef.current = null setIsRunning(false) - toast.error(t('agents.toasts.runError', { error: data.error || t('agents.toasts.runFailed') }), { id: toastId }) + toast.error(t('agents.toasts.runError', { error: data.error || t('agents.toasts.runFailed') }), { + id: toastId, + description: '' // Clear the loading description + }) onRefresh() } - } catch { /* keep polling */ } + } catch (err) { + console.error('Polling error:', err) + // Keep polling until timeout + } }, 3000) } catch { toast.error(t('agents.toasts.runGenericError'), { id: toastId }) diff --git a/memento-note/lib/ai/factory.ts b/memento-note/lib/ai/factory.ts index abd0bc4..aaf1380 100644 --- a/memento-note/lib/ai/factory.ts +++ b/memento-note/lib/ai/factory.ts @@ -2,11 +2,15 @@ import { OpenAIProvider } from './providers/openai'; import { OllamaProvider } from './providers/ollama'; import { CustomOpenAIProvider } from './providers/custom-openai'; import { AnthropicProvider } from './providers/anthropic'; +import { GoogleProvider } from './providers/google'; import { AIProvider } from './types'; type ProviderType = | 'ollama' | 'openai' + | 'google' + | 'minimax' + | 'glm' | 'custom' | 'deepseek' | 'openrouter' @@ -43,6 +47,21 @@ const PROVIDER_DEFAULTS: Record, modelName: string, embeddingModelName: string, baseUrlOverride?: string): OllamaProvider { @@ -156,6 +175,26 @@ function createAnthropicCustomProvider(config: Record, modelName return new AnthropicProvider(apiKey, resolvedModel, baseUrl.trim()); } +function createGoogleProvider(config: Record, modelName: string, embeddingModelName: string): GoogleProvider { + const apiKey = config?.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY || ''; + if (!apiKey) throw new Error('GOOGLE_GENERATIVE_AI_API_KEY is required when using Google provider'); + return new GoogleProvider(apiKey, modelName || PROVIDER_DEFAULTS.google.model, embeddingModelName || PROVIDER_DEFAULTS.google.embeddingModel); +} + +function createMiniMaxProvider(config: Record, modelName: string, embeddingModelName: string): CustomOpenAIProvider { + const apiKey = config?.MINIMAX_API_KEY || process.env.MINIMAX_API_KEY || ''; + if (!apiKey) throw new Error('MINIMAX_API_KEY is required when using MiniMax provider'); + const defaults = PROVIDER_DEFAULTS.minimax; + return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel); +} + +function createGLMProvider(config: Record, modelName: string, embeddingModelName: string): CustomOpenAIProvider { + const apiKey = config?.GLM_API_KEY || process.env.GLM_API_KEY || ''; + if (!apiKey) throw new Error('GLM_API_KEY is required when using GLM provider'); + const defaults = PROVIDER_DEFAULTS.glm; + return new CustomOpenAIProvider(apiKey, defaults.baseUrl, modelName || defaults.model, embeddingModelName || defaults.embeddingModel); +} + function getProviderInstance(providerType: ProviderType, config: Record, modelName: string, embeddingModelName: string, ollamaBaseUrl?: string): AIProvider { switch (providerType) { case 'ollama': @@ -178,6 +217,12 @@ function getProviderInstance(providerType: ProviderType, config: Record { + try { + const { object } = await generateObject({ + model: this.model, + schema: z.object({ + tags: z.array(z.object({ + tag: z.string().describe('Short tag name in lowercase'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') + })) + }), + prompt: `Analyze the following note and suggest 1 to 5 relevant tags. + Note content: "${content}"`, + }); + + return object.tags; + } catch (e) { + console.error('Error generating tags (Google):', e); + return []; + } + } + + async getEmbeddings(text: string): Promise { + try { + const { embedding } = await embed({ + model: this.embeddingModel, + value: text, + }); + return embedding; + } catch (e) { + console.error('Error generating embeddings (Google):', e); + throw e; + } + } + + async generateTitles(prompt: string): Promise { + try { + const { object } = await generateObject({ + model: this.model, + schema: z.object({ + titles: z.array(z.object({ + title: z.string().describe('Suggested title'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') + })) + }), + prompt: prompt, + }); + + return object.titles; + } catch (e) { + console.error('Error generating titles (Google):', e); + return []; + } + } + + async generateText(prompt: string): Promise { + try { + const { text } = await aiGenerateText({ + model: this.model, + prompt: prompt, + }); + + return text.trim(); + } catch (e) { + console.error('Error generating text (Google):', e); + throw e; + } + } + + async chat(messages: any[], systemPrompt?: string): Promise { + try { + const { text } = await aiGenerateText({ + model: this.model, + system: systemPrompt, + messages: messages, + }); + + return { text: text.trim() }; + } catch (e) { + console.error('Error in chat (Google):', e); + throw e; + } + } + + async generateWithTools(options: ToolUseOptions): Promise { + const { tools, maxSteps = 10, systemPrompt, messages, prompt } = options + const opts: Record = { + model: this.model, + tools, + stopWhen: stepCountIs(maxSteps), + } + if (systemPrompt) opts.system = systemPrompt + if (messages) opts.messages = messages + else if (prompt) opts.prompt = prompt + + const result = await aiGenerateText(opts as any) + return { + toolCalls: result.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [], + toolResults: result.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [], + text: result.text, + steps: result.steps?.map((step: any) => ({ + text: step.text, + toolCalls: step.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [], + toolResults: step.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [] + })) || [] + } + } + + getModel() { + return this.model; + } +} diff --git a/memento-note/lib/ai/types.ts b/memento-note/lib/ai/types.ts index 124521f..8bab83f 100644 --- a/memento-note/lib/ai/types.ts +++ b/memento-note/lib/ai/types.ts @@ -70,6 +70,9 @@ export interface AIProvider { export type AIProviderType = | 'openai' | 'ollama' + | 'google' + | 'minimax' + | 'glm' | 'custom' | 'deepseek' | 'openrouter' diff --git a/memento-note/package-lock.json b/memento-note/package-lock.json index a42d8d9..da6fe2e 100644 --- a/memento-note/package-lock.json +++ b/memento-note/package-lock.json @@ -9,6 +9,7 @@ "version": "0.2.0", "dependencies": { "@ai-sdk/anthropic": "^3.0.76", + "@ai-sdk/google": "^3.0.71", "@ai-sdk/openai": "^3.0.7", "@ai-sdk/react": "^3.0.170", "@auth/prisma-adapter": "^2.11.1", @@ -175,6 +176,51 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/google": { + "version": "3.0.71", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.71.tgz", + "integrity": "sha512-G86UtqkCKM8mQcvsA4FQ1WCRN+w1gl/sxuoYl5CJX5DFSUSkrLNKmLcvi3TtnMvKth5li8W/1h3emQSl2K+qnA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.27" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz", + "integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider-utils": { + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", + "integrity": "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/openai": { "version": "3.0.53", "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.53.tgz", diff --git a/memento-note/package.json b/memento-note/package.json index 0189b3e..33a0e1e 100644 --- a/memento-note/package.json +++ b/memento-note/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@ai-sdk/anthropic": "^3.0.76", + "@ai-sdk/google": "^3.0.71", "@ai-sdk/openai": "^3.0.7", "@ai-sdk/react": "^3.0.170", "@auth/prisma-adapter": "^2.11.1",