diff --git a/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/admin/document-stores/page.tsx b/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/admin/document-stores/page.tsx new file mode 100644 index 00000000000..8bb4da64ab3 --- /dev/null +++ b/apps/web/app/(Main UI)/sidekick-studio/(main-layout)/admin/document-stores/page.tsx @@ -0,0 +1,19 @@ +'use client' + +import React from 'react' +import dynamic from 'next/dynamic' +import { Box, Container } from '@mui/material' + +const AdminDocumentStores = dynamic(() => import('@ui/Admin/DocumentStores'), { ssr: false }) + +const Page = () => { + return ( + + + + + + ) +} + +export default Page diff --git a/packages-answers/assets/aai-models.json b/packages-answers/assets/aai-models.json index 6ec2bb4ce8a..e377c0dc03b 100644 --- a/packages-answers/assets/aai-models.json +++ b/packages-answers/assets/aai-models.json @@ -523,6 +523,13 @@ { "name": "chatGoogleGenerativeAI", "models": [ + { + "label": "gemini-2.5-flash-image-preview", + "name": "gemini-2.5-flash-image-preview", + "description": "Latest Gemini model with image generation and understanding capabilities", + "input_cost": 0.15e-6, + "output_cost": 6e-7 + }, { "label": "gemini-2.5-flash-preview-05-20", "name": "gemini-2.5-flash-preview-05-20", @@ -789,6 +796,24 @@ { "name": "chatOpenAI", "models": [ + { + "label": "gpt-5", + "name": "gpt-5", + "input_cost": 1.25e-6, + "output_cost": 1e-5 + }, + { + "label": "gpt-5-mini", + "name": "gpt-5-mini", + "input_cost": 2.5e-7, + "output_cost": 2e-6 + }, + { + "label": "gpt-5-nano", + "name": "gpt-5-nano", + "input_cost": 5e-8, + "output_cost": 4e-7 + }, { "label": "gpt-4.1", "name": "gpt-4.1", diff --git a/packages-answers/assets/models.json b/packages-answers/assets/models.json index 6ec2bb4ce8a..e377c0dc03b 100644 --- a/packages-answers/assets/models.json +++ b/packages-answers/assets/models.json @@ -523,6 +523,13 @@ { "name": "chatGoogleGenerativeAI", "models": [ + { + "label": "gemini-2.5-flash-image-preview", + "name": "gemini-2.5-flash-image-preview", + "description": "Latest Gemini model with image generation and understanding capabilities", + "input_cost": 0.15e-6, + "output_cost": 6e-7 + }, { "label": "gemini-2.5-flash-preview-05-20", "name": "gemini-2.5-flash-preview-05-20", @@ -789,6 +796,24 @@ { "name": "chatOpenAI", "models": [ + { + "label": "gpt-5", + "name": "gpt-5", + "input_cost": 1.25e-6, + "output_cost": 1e-5 + }, + { + "label": "gpt-5-mini", + "name": "gpt-5-mini", + "input_cost": 2.5e-7, + "output_cost": 2e-6 + }, + { + "label": "gpt-5-nano", + "name": "gpt-5-nano", + "input_cost": 5e-8, + "output_cost": 4e-7 + }, { "label": "gpt-4.1", "name": "gpt-4.1", diff --git a/packages-answers/ui/src/Admin/Chatflows/index.tsx b/packages-answers/ui/src/Admin/Chatflows/index.tsx index 25d36626a7a..3d2f5101a3d 100644 --- a/packages-answers/ui/src/Admin/Chatflows/index.tsx +++ b/packages-answers/ui/src/Admin/Chatflows/index.tsx @@ -161,9 +161,21 @@ const AdminChatflows = () => { } const getCanvasRoute = (chatflow: any) => { - // Check the actual flow type, not the filter selection + // For react-router-dom Link component (relative paths) console.log('🤖id', chatflow.id) console.log('type', chatflow) + if (chatflow.type === 'AGENTFLOW') { + return `/v2/agentcanvas/${chatflow.id}` + } else if (chatflow.type === 'MULTIAGENT') { + return `/agentcanvas/${chatflow.id}` + } else { + // Default to regular chatflow canvas + return `/canvas/${chatflow.id}` + } + } + + const getCanvasFullUrl = (chatflow: any) => { + // For window.open() (full URLs with base path) if (chatflow.type === 'AGENTFLOW') { return `/sidekick-studio/v2/agentcanvas/${chatflow.id}` } else if (chatflow.type === 'MULTIAGENT') { @@ -1371,7 +1383,7 @@ const AdminChatflows = () => { window.open(getCanvasRoute(chatflow), '_blank')} + onClick={() => window.open(getCanvasFullUrl(chatflow), '_blank')} sx={{ color: 'rgba(255, 255, 255, 0.7)', '&:hover': { color: 'rgba(255, 255, 255, 0.9)' } diff --git a/packages-answers/ui/src/Admin/DocumentStores/index.tsx b/packages-answers/ui/src/Admin/DocumentStores/index.tsx new file mode 100644 index 00000000000..861c049d752 --- /dev/null +++ b/packages-answers/ui/src/Admin/DocumentStores/index.tsx @@ -0,0 +1,293 @@ +'use client' +import { useMemo, useState, type ChangeEvent } from 'react' +import { Link } from 'react-router-dom' +import { + Box, + Button, + Chip, + IconButton, + Paper, + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TablePagination, + TableRow, + TableSortLabel, + TextField, + Tooltip, + Typography +} from '@mui/material' +import RefreshIcon from '@mui/icons-material/Refresh' +import LaunchIcon from '@mui/icons-material/Launch' +import documentStoreApi from '@/api/documentstore' +import useApi from '@ui/hooks/useApi' +import { format } from 'date-fns' + +export type AdminDocumentStore = { + id: string + name: string + description?: string + status: string + createdDate: string + updatedDate: string + loaders?: Array<{ id: string }> + totalChunks?: number + totalChars?: number + user?: { + id?: string + name?: string | null + email?: string | null + } + organizationId?: string + userId?: string + isOwner?: boolean +} + +const columns: Array<{ id: keyof AdminDocumentStore | 'owner' | 'loadersCount'; label: string }> = [ + { id: 'name', label: 'Name' }, + { id: 'status', label: 'Status' }, + { id: 'owner', label: 'Owner' }, + { id: 'loadersCount', label: 'Loaders' }, + { id: 'totalChunks', label: 'Chunks' }, + { id: 'updatedDate', label: 'Last Updated' } +] + +type OrderableColumn = 'name' | 'updatedDate' | 'totalChunks' | 'loadersCount' + +type Order = 'asc' | 'desc' + +const AdminDocumentStores = () => { + const { + data: documentStoresData, + isLoading, + isError, + refresh + } = useApi('/api/admin/document-stores', () => documentStoreApi.getAdminDocumentStores()) + + const [orderBy, setOrderBy] = useState('updatedDate') + const [order, setOrder] = useState('desc') + const [page, setPage] = useState(0) + const [rowsPerPage, setRowsPerPage] = useState(25) + const [search, setSearch] = useState('') + + const filteredAndSorted = useMemo(() => { + if (!documentStoresData) return [] + + const normalizedSearch = search.trim().toLowerCase() + const filtered = documentStoresData.filter((store) => { + if (!normalizedSearch) return true + const ownerLabel = store.user?.name || store.user?.email || '' + return ( + store.name.toLowerCase().includes(normalizedSearch) || + (store.description || '').toLowerCase().includes(normalizedSearch) || + ownerLabel.toLowerCase().includes(normalizedSearch) + ) + }) + + const sorted = [...filtered].sort((a, b) => { + let aValue: string | number = '' + let bValue: string | number = '' + + switch (orderBy) { + case 'name': + aValue = a.name.toLowerCase() + bValue = b.name.toLowerCase() + break + case 'totalChunks': + aValue = a.totalChunks ?? 0 + bValue = b.totalChunks ?? 0 + break + case 'loadersCount': + aValue = a.loaders?.length ?? 0 + bValue = b.loaders?.length ?? 0 + break + case 'updatedDate': + default: + aValue = new Date(a.updatedDate).getTime() + bValue = new Date(b.updatedDate).getTime() + break + } + + if (aValue < bValue) { + return order === 'asc' ? -1 : 1 + } + if (aValue > bValue) { + return order === 'asc' ? 1 : -1 + } + return 0 + }) + + return sorted + }, [documentStoresData, order, orderBy, search]) + + const paginatedData = useMemo(() => { + const start = page * rowsPerPage + return filteredAndSorted.slice(start, start + rowsPerPage) + }, [filteredAndSorted, page, rowsPerPage]) + + const handleRequestSort = (property: OrderableColumn) => { + const isAsc = orderBy === property && order === 'asc' + setOrder(isAsc ? 'desc' : 'asc') + setOrderBy(property) + } + + const handleChangePage = (_: unknown, newPage: number) => { + setPage(newPage) + } + + const handleChangeRowsPerPage = (event: ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)) + setPage(0) + } + + const renderSkeleton = () => { + return Array.from({ length: 5 }).map((_, index) => ( + + {columns.map((column) => ( + + + + ))} + + + + + )) + } + + return ( + + + + + + + + + Document Stores + + + Review and manage every document store in your organization. + + + + { + setSearch(event.target.value) + setPage(0) + }} + size='small' + placeholder='Search by name, description, or owner' + sx={{ minWidth: 280 }} + /> + + refresh()} color='primary'> + + + + + + + + + + + {columns.map((column) => { + const isSortable = ['name', 'updatedDate', 'totalChunks', 'loadersCount'].includes(column.id) + return ( + + {isSortable ? ( + handleRequestSort(column.id as OrderableColumn)} + > + {column.label} + + ) : ( + column.label + )} + + ) + })} + Actions + + + + {isLoading && renderSkeleton()} + {!isLoading && paginatedData.length === 0 && ( + + + + {isError ? 'Failed to load document stores.' : 'No document stores found.'} + + + + )} + {!isLoading && + paginatedData.map((store) => { + const ownerLabel = store.user?.name || store.user?.email || 'Unknown owner' + const loadersCount = store.loaders?.length ?? 0 + const updatedAt = format(new Date(store.updatedDate), 'MMM d, yyyy p') + const detailPath = `/sidekick-studio/document-stores/${store.id}` + + return ( + + + + {store.name} + {store.description && ( + + {store.description} + + )} + + + + + + + {ownerLabel} + + + {loadersCount} + + + {store.totalChunks ?? 0} + + + {updatedAt} + + + + + + + + + + ) + })} + +
+
+ +
+ ) +} + +export default AdminDocumentStores diff --git a/packages-answers/ui/src/Admin/index.tsx b/packages-answers/ui/src/Admin/index.tsx index e7c432ee507..9a52c23366f 100644 --- a/packages-answers/ui/src/Admin/index.tsx +++ b/packages-answers/ui/src/Admin/index.tsx @@ -46,6 +46,19 @@ const AdminDashboard = () => { + + + + Document Stores + + View and manage document stores across the organization. + + + + + diff --git a/packages/components/nodes/agents/ToolAgent/AAIToolAgent.ts b/packages/components/nodes/agents/ToolAgent/AAIToolAgent.ts index 8dcea0a3559..df04be84d86 100644 --- a/packages/components/nodes/agents/ToolAgent/AAIToolAgent.ts +++ b/packages/components/nodes/agents/ToolAgent/AAIToolAgent.ts @@ -1,15 +1,154 @@ +import { BaseOutputParser } from '@langchain/core/output_parsers' +import { OutputFixingParser } from 'langchain/output_parsers' +import { ICommonObject, INodeData } from '../../../src/Interface' +import { formatResponse } from '../../outputparsers/OutputParserHelpers' + const { nodeClass: OriginalToolAgent } = require('./ToolAgent') -// AAI-branded clone of ToolAgent for Answer tab -class AAIToolAgent_Agents extends (OriginalToolAgent as any) { +// Create a properly typed base class +const ToolAgentBase = OriginalToolAgent as new (fields?: { sessionId?: string }) => any + +// AAI-branded clone of ToolAgent for Answer tab with structured output support +class AAIToolAgent_Agents extends ToolAgentBase { + outputParser: BaseOutputParser + constructor(fields?: { sessionId?: string }) { super(fields) this.label = 'Tool Agent' this.name = 'aaiToolAgent' this.category = 'Agents' - this.description = 'Tool Agent • Zero configuration required' + this.description = 'Tool Agent • Zero configuration required • Supports structured output' this.tags = ['AAI'] + + // Add output parser input to the existing inputs + this.inputs = [ + ...this.inputs, + { + label: 'Output Parser', + name: 'outputParser', + type: 'BaseLLMOutputParser', + optional: true, + description: 'Parse the agent output into a structured format' + } + ] + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + // Initialize output parser if provided + const llmOutputParser = nodeData.inputs?.outputParser as BaseOutputParser + this.outputParser = llmOutputParser + + if (llmOutputParser) { + let autoFix = (llmOutputParser as any).autoFix + if (autoFix === true) { + // Get the model from nodeData to create OutputFixingParser + const model = nodeData.inputs?.model + if (model) { + this.outputParser = OutputFixingParser.fromLLM(model, llmOutputParser) + } + } + + // Inject format instructions into system message and Chat Prompt Template + this.injectFormatInstructions(nodeData) + } + + // Call the parent init method + return await super.init(nodeData, input, options) + } + + async run(nodeData: INodeData, input: string, options: ICommonObject): Promise { + // Call the parent run method + const result = await super.run(nodeData, input, options) + + // Apply output parser to the final result if parser is available + if (this.outputParser && result) { + return await this.parseOutput(result) + } + + return result + } + + private injectFormatInstructions(nodeData: INodeData) { + if (!this.outputParser || !nodeData.inputs) return + + const formatInstructions = this.outputParser.getFormatInstructions() + if (!formatInstructions) return + + // Escape curly braces to prevent LangChain template parsing issues + const escapedInstructions = formatInstructions.replace(/\{/g, '{{').replace(/\}/g, '}}') + const instructionText = `\n\nIMPORTANT: Your final response must follow this exact format:\n${escapedInstructions}` + + // Handle Chat Prompt Template if it exists + const chatPromptTemplate = nodeData.inputs?.chatPromptTemplate + if (chatPromptTemplate && chatPromptTemplate.promptMessages?.length > 0) { + // Inject into the first system message of the Chat Prompt Template + const systemMessage = chatPromptTemplate.promptMessages[0] + if (systemMessage && (systemMessage as any).prompt?.template) { + const currentTemplate = (systemMessage as any).prompt.template + ;(systemMessage as any).prompt.template = currentTemplate + instructionText + } + } else { + // Handle regular system message + let systemMessage = (nodeData.inputs?.systemMessage as string) || '' + if (systemMessage) { + nodeData.inputs.systemMessage = systemMessage + instructionText + } else { + nodeData.inputs.systemMessage = `You are a helpful assistant.${instructionText}` + } + } + } + + private async parseOutput(result: string | ICommonObject): Promise { + try { + let textToParse: string + + // Extract text from result + if (typeof result === 'string') { + textToParse = result + } else if (result && typeof result === 'object' && 'text' in result) { + textToParse = result.text as string + } else { + // If result is not in expected format, return as-is + return result + } + + // Parse the output using the output parser + const parsedOutput = await this.outputParser.parse(textToParse) + + // Format the response properly for consistency with LLM Chain + const formattedOutput = formatResponse(parsedOutput as string | object) + + // If original result was an object with additional metadata, preserve it + if (typeof result === 'object' && result !== null && 'text' in result) { + // For Tool Agent with metadata, return the structured format like LLM Chain + // Remove the 'text' property and merge the formatted output at the top level + const { text: _text, ...metadata } = result + + // Handle both object and string formatted outputs + let finalResult: ICommonObject + if (typeof formattedOutput === 'object' && formattedOutput !== null) { + finalResult = { + ...formattedOutput, // This adds the 'json' property at top level + ...metadata, // Preserve usedTools, sourceDocuments, artifacts, etc. + parsedOutput: parsedOutput // Include the raw structured parsed output for debugging + } + } else { + // If formattedOutput is a string, create json wrapper like LLM Chain does + finalResult = { + json: parsedOutput, // Direct structured output + ...metadata, // Preserve usedTools, sourceDocuments, artifacts, etc. + parsedOutput: parsedOutput // Include the raw structured parsed output for debugging + } + } + return finalResult + } + + return formattedOutput + } catch (error) { + // If parsing fails, return the original result + return result + } } } -module.exports = { nodeClass: AAIToolAgent_Agents } \ No newline at end of file +module.exports = { nodeClass: AAIToolAgent_Agents } diff --git a/packages/components/nodes/chatmodels/ChatAnthropic/AAIChatAnthropic.ts b/packages/components/nodes/chatmodels/ChatAnthropic/AAIChatAnthropic.ts index 0ff48bb427d..51eb6bc98f8 100644 --- a/packages/components/nodes/chatmodels/ChatAnthropic/AAIChatAnthropic.ts +++ b/packages/components/nodes/chatmodels/ChatAnthropic/AAIChatAnthropic.ts @@ -17,7 +17,7 @@ class AAIChatAnthropic_ChatModels implements INode { baseClasses: string[] inputs: INodeParams[] tags: string[] - + constructor() { this.label = 'Answer ChatAnthropic' this.name = 'aaiChatAnthropic' @@ -39,7 +39,7 @@ class AAIChatAnthropic_ChatModels implements INode { name: 'modelName', type: 'asyncOptions', loadMethod: 'listModels', - default: 'claude-3-sonnet-20240229' + default: 'claude-sonnet-4-0' }, { label: 'Temperature', @@ -170,4 +170,4 @@ class AAIChatAnthropic_ChatModels implements INode { } } -module.exports = { nodeClass: AAIChatAnthropic_ChatModels } \ No newline at end of file +module.exports = { nodeClass: AAIChatAnthropic_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/AAIChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/AAIChatGoogleGenerativeAI.ts index dedff8a66d5..6ef89778c70 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/AAIChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/AAIChatGoogleGenerativeAI.ts @@ -47,7 +47,7 @@ class AAIGoogleGenerativeAI_ChatModels implements INode { name: 'modelName', type: 'asyncOptions', loadMethod: 'listModels', - default: 'gemini-1.5-flash' + default: 'gemini-2.5-flash' }, { label: 'Custom Model Name', @@ -173,6 +173,25 @@ class AAIGoogleGenerativeAI_ChatModels implements INode { 'Allow image input. Refer to the docs for more details.', default: false, optional: true + }, + { + label: 'Response Modalities', + name: 'responseModalities', + type: 'multiOptions', + description: 'Specify output modalities. Enable IMAGE for image generation capabilities.', + options: [ + { + label: 'TEXT', + name: 'TEXT' + }, + { + label: 'IMAGE', + name: 'IMAGE' + } + ], + default: ['TEXT'], + optional: true, + additionalParams: true } ] } @@ -184,7 +203,7 @@ class AAIGoogleGenerativeAI_ChatModels implements INode { } } - async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + async init(nodeData: INodeData, _: string, _options: ICommonObject): Promise { // Use AAI default credentials instead of user-provided credentials const apiKey = process.env.AAI_DEFAULT_GOOGLE_GENERATIVE_AI_API_KEY @@ -204,6 +223,7 @@ class AAIGoogleGenerativeAI_ChatModels implements INode { const contextCache = nodeData.inputs?.contextCache as FlowiseGoogleAICacheManager const streaming = nodeData.inputs?.streaming as boolean const baseUrl = nodeData.inputs?.baseUrl as string | undefined + const responseModalities = nodeData.inputs?.responseModalities as string const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean @@ -219,6 +239,7 @@ class AAIGoogleGenerativeAI_ChatModels implements INode { if (cache) obj.cache = cache if (temperature) obj.temperature = parseFloat(temperature) if (baseUrl) obj.baseUrl = baseUrl + if (responseModalities) obj.responseModalities = convertMultiOptionsToStringArray(responseModalities) // Safety Settings let harmCategories: string[] = convertMultiOptionsToStringArray(harmCategory) @@ -243,8 +264,17 @@ class AAIGoogleGenerativeAI_ChatModels implements INode { model.setMultiModalOption(multiModalOption) if (contextCache) model.setContextCache(contextCache) + // Set user context for image uploads + if (_options?.user) { + model.setUserContext({ + organizationId: _options.user.organizationId, + userId: _options.user.id, + userEmail: _options.user.email || `${_options.user.id}@local` + }) + } + return model } } -module.exports = { nodeClass: AAIGoogleGenerativeAI_ChatModels } \ No newline at end of file +module.exports = { nodeClass: AAIGoogleGenerativeAI_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts index 3f01b8624ba..c02730b5e21 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/ChatGoogleGenerativeAI.ts @@ -248,6 +248,15 @@ class GoogleGenerativeAI_ChatModels implements INode { model.setMultiModalOption(multiModalOption) if (contextCache) model.setContextCache(contextCache) + // Set user context for image uploads + if (options?.user) { + model.setUserContext({ + organizationId: options.user.organizationId, + userId: options.user.id, + userEmail: options.user.email || `${options.user.id}@local` + }) + } + return model } } diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts index 87c7445e896..df6ac027e95 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts @@ -59,6 +59,7 @@ export interface GoogleGenerativeAIChatInput extends BaseChatModelParams, Pick 0) { + modelConfig.config = { responseModalities: this.responseModalities } + } + + this.client = new GenerativeAI(this.apiKey ?? '').getGenerativeModel(modelConfig, { + baseUrl: this.baseUrl + }) if (this.contextCache) { const cachedContent = await this.contextCache.lookup({ contents: prompt ? [{ ...prompt[0], parts: prompt[0].parts.slice(0, 1) }] : [], @@ -231,6 +247,82 @@ class LangchainChatGoogleGenerativeAI this.contextCache = contextCache } + setUserContext(userContext: { organizationId: string; userId: string; userEmail: string }): void { + this.userContext = userContext + } + + /** + * Extract user context from stored instance or use fallbacks + */ + extractUserContext(options?: any): { organizationId: string; userId: string; userEmail: string } { + // First try stored user context (set during initialization) + if (this.userContext) { + return this.userContext + } + + // Try to get user from Flowise run params (from buildAgentflow.ts) + const user = options?.user || options?.runParams?.user + + if (user?.id && user?.organizationId) { + return { + organizationId: user.organizationId, + userId: user.id, + userEmail: user.email || `${user.id}@local` + } + } + + // Fallback to system context for tool calls or when user context is unavailable + return { + organizationId: 'system-org', + userId: 'chat-system', + userEmail: 'system@chat.local' + } + } + + async uploadImageToStorage(imageData: any, userContext: { organizationId: string; userId: string; userEmail: string }) { + try { + // Import and use the storage utility directly + const { addSingleFileToStorage } = await import('../../../src/storageUtils') + const crypto = await import('node:crypto') + + // Convert base64 to Buffer + const buffer = Buffer.from(imageData.data, 'base64') + + // Generate unique identifier for this image generation session + const timestamp = Date.now() + const randomSuffix = crypto.randomBytes(8).toString('hex') + const sessionId = `${timestamp}_${randomSuffix}` + + // Create image filename with session ID + const mimeType = imageData.mimeType || 'image/png' + const fileExtension = mimeType.split('/')[1] || 'png' + const imageFilename = `${sessionId}_chat_generated_image.${fileExtension}` + + // Store the image using organization/user folder structure + const imageStorageUrl = await addSingleFileToStorage( + mimeType, + buffer, + imageFilename, + 'gemini-images', + userContext.organizationId, + userContext.userId + ) + + // Convert FILE-STORAGE:: reference to a full URL with domain + const domain = process.env.DOMAIN || process.env.FLOWISE_DOMAIN || 'http://localhost:4000' + const imageFileName = imageStorageUrl.replace('FILE-STORAGE::', '') + const fullImageUrl = `${domain}/api/v1/get-upload-file?chatflowId=gemini-images&chatId=${userContext.organizationId}%2F${userContext.userId}&fileName=${imageFileName}` + return { + url: fullImageUrl, + success: true, + sessionId + } + } catch (error) { + console.warn('Upload error:', error) + return { success: false, error: String(error) } + } + } + async getNumTokens(prompt: BaseMessage[]) { const contents = convertBaseMessagesToContent(prompt, this._isMultimodalModel) const { totalTokens } = await this.client.countTokens({ contents }) @@ -293,6 +385,30 @@ class LangchainChatGoogleGenerativeAI // Map the API response to a ChatResult object for LangChain const generationResult = mapGenerateContentResultToChatResult(res.response, this.modelName, genAIUsageMetadata) + // Handle image data if present in non-streaming mode + if (generationResult.generations?.[0]?.message?.additional_kwargs?.imageData) { + try { + const imageData = generationResult.generations[0].message.additional_kwargs.imageData + + // Extract user context from LangChain run config or use fallbacks + const userContext = this.extractUserContext(options) + + const uploadResponse = await this.uploadImageToStorage(imageData, userContext) + + if (uploadResponse.success) { + // Update the generation with the uploaded image URL + generationResult.generations[0].message.additional_kwargs.imageUrl = uploadResponse.url + generationResult.generations[0].message.additional_kwargs.imageSessionId = uploadResponse.sessionId + const imageMarkdown = `\n\n![Generated Image](${uploadResponse.url})` + generationResult.generations[0].text += imageMarkdown + // CRITICAL: Also update the message content to include the image markdown + generationResult.generations[0].message.content = generationResult.generations[0].text + } + } catch (error) { + console.warn('Failed to upload generated image:', error) + } + } + // Optionally notify the run manager of the new token (for streaming UI updates) await _runManager?.handleLLMNewToken(generationResult.generations?.length ? generationResult.generations[0].text : '') return generationResult @@ -323,6 +439,8 @@ class LangchainChatGoogleGenerativeAI const stream = this._streamResponseChunks(messages, options, runManager) // Store the final chunks by their index (for multi-candidate support) const finalChunks: Record = {} + // Track custom text modifications (like image markdown) separately + const customTextModifications: Record = {} // Aggregate all streamed chunks by their index for await (const chunk of stream) { @@ -339,10 +457,16 @@ class LangchainChatGoogleGenerativeAI if (finalChunks[index] === undefined) { // First chunk for this index finalChunks[index] = chunk + // Track any custom text modifications + customTextModifications[index] = chunk.text } else { // Concatenate the chunks for the same index const existingChunk = finalChunks[index] const concatenated = existingChunk.concat(chunk) + + // Preserve custom text modifications by manually tracking them + customTextModifications[index] = (customTextModifications[index] || '') + chunk.text + // Use the latest chunk's usage_metadata (which has the correct diff details) // @ts-ignore - Custom metadata structure concatenated.message.usage_metadata = chunk.message.usage_metadata @@ -376,7 +500,18 @@ class LangchainChatGoogleGenerativeAI // Sort and collect all generations in order by index const generations = Object.entries(finalChunks) .sort(([aKey], [bKey]) => parseInt(aKey, 10) - parseInt(bKey, 10)) - .map(([_, value]) => value) + .map(([indexStr, value]) => { + const index = parseInt(indexStr, 10) + // Override the text with our custom modifications to preserve image markdown + if (customTextModifications[index] !== undefined) { + value.text = customTextModifications[index] + // Also update the message content + if (value.message) { + value.message.content = customTextModifications[index] + } + } + return value + }) // Attach aggregated token usage to each generation for (const generation of generations) { @@ -410,6 +545,9 @@ class LangchainChatGoogleGenerativeAI let prompt = convertBaseMessagesToContent(messages, this._isMultimodalModel) prompt = checkIfEmptyContentAndSameRole(prompt) + // Convert any function response parts in the prompt for compatibility + this.convertFunctionResponse(prompt) + // Prepare API request parameters const parameters = this.invocationParams(options) const request = { @@ -520,6 +658,30 @@ class LangchainChatGoogleGenerativeAI index += 1 // Increment chunk index for next chunk if (!chunk) continue // Skip if chunk is null + // Handle image data if present + if (chunk.message.additional_kwargs?.imageData) { + try { + const imageData = chunk.message.additional_kwargs.imageData + + // Extract user context from LangChain run config or use fallbacks + const userContext = this.extractUserContext(options) + + const uploadResponse = await this.uploadImageToStorage(imageData, userContext) + + if (uploadResponse.success) { + // Update the chunk with the uploaded image URL + chunk.message.additional_kwargs.imageUrl = uploadResponse.url + chunk.message.additional_kwargs.imageSessionId = uploadResponse.sessionId + const imageMarkdown = `\n\n![Generated Image](${uploadResponse.url})` + chunk.text += imageMarkdown + // CRITICAL: Also update the message content to include the image markdown + chunk.message.content = chunk.text + } + } catch (error) { + console.warn('Failed to upload generated image:', error) + } + } + // Yield the chunk to the consumer yield chunk // Optionally notify the run manager of the new token (for streaming UI updates) @@ -789,25 +951,44 @@ function mapGenerateContentResultToChatResult( // Extract content and generation info from the candidate const { content, ...generationInfo } = candidate - // Get the generated text (if any) - const text = content?.parts[0]?.text ?? '' + + // Handle both text and image content + let text = '' + let imageData: any = null + + if (content?.parts) { + for (const part of content.parts) { + if (part.text) { + text += part.text + } else if (part.inlineData) { + // Store image data for handling + imageData = part.inlineData + } + } + } // Extract usage metadata if available (for reporting token usage) const usageMetadata: any = extra?.usageMetadata // Build the ChatGeneration object for LangChain + const additionalKwargs: any = { + model_name, + ...generationInfo + } + + if (imageData) { + additionalKwargs.imageData = imageData + } + const generation: ChatGeneration = { text, message: new AIMessage({ content: text, tool_calls: functionCalls, - additional_kwargs: { - model_name, - ...generationInfo - }, + additional_kwargs: additionalKwargs, usage_metadata: { input_tokens: usageMetadata?.promptTokenCount ?? 0, // Number of prompt tokens used - output_tokens: usageMetadata?.candidatesTokenCount ?? 0 + usageMetadata?.thoughtsTokenCount ?? 0, // Output tokens + output_tokens: (usageMetadata?.candidatesTokenCount ?? 0) + (usageMetadata?.thoughtsTokenCount ?? 0), // Output tokens total_tokens: usageMetadata?.totalTokenCount ?? 0, // Total tokens used input_token_details: Array.isArray(usageMetadata?.promptTokensDetails) ? usageMetadata?.promptTokensDetails.reduce((acc: any, curr: any) => { @@ -848,7 +1029,21 @@ function convertResponseContentToChatGenerationChunk( const functionCalls = response.functionCalls() const [candidate] = response.candidates const { content, ...generationInfo } = candidate - const text = content?.parts?.[0]?.text ?? '' + + // Handle both text and image content + let text = '' + let imageData: any = null + + if (content?.parts) { + for (const part of content.parts) { + if (part.text) { + text += part.text + } else if (part.inlineData) { + // Store image data for handling + imageData = part.inlineData + } + } + } const toolCallChunks: ToolCallChunk[] = [] if (functionCalls) { @@ -861,6 +1056,11 @@ function convertResponseContentToChatGenerationChunk( ) } + const additionalKwargs: any = {} + if (imageData) { + additionalKwargs.imageData = imageData + } + return new ChatGenerationChunk({ text, message: new AIMessageChunk({ @@ -869,7 +1069,7 @@ function convertResponseContentToChatGenerationChunk( tool_call_chunks: toolCallChunks, // Each chunk can have unique "generationInfo", and merging strategy is unclear, // so leave blank for now. - additional_kwargs: {}, + additional_kwargs: additionalKwargs, usage_metadata: extra.usageMetadata as any }), generationInfo diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/AAIChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/AAIChatOpenAI.ts index cdfb0925e7f..0fa01e67911 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/AAIChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/AAIChatOpenAI.ts @@ -17,7 +17,7 @@ class AAIChatOpenAI_ChatModels implements INode { baseClasses: string[] inputs: INodeParams[] tags: string[] - + constructor() { this.label = 'Answer ChatOpenAI' this.name = 'aaiChatOpenAI' @@ -193,6 +193,14 @@ class AAIChatOpenAI_ChatModels implements INode { default: 'medium', optional: false, additionalParams: true + }, + { + label: 'Prompt Cache Key', + name: 'promptCacheKey', + type: 'string', + description: 'Cache key for OpenAI prompt caching to optimize cache hit rates and reduce costs', + optional: true, + additionalParams: true } ] } @@ -219,6 +227,7 @@ class AAIChatOpenAI_ChatModels implements INode { const proxyUrl = nodeData.inputs?.proxyUrl as string const baseOptions = nodeData.inputs?.baseOptions const reasoningEffort = nodeData.inputs?.reasoningEffort as OpenAIClient.Chat.ChatCompletionReasoningEffort + const promptCacheKey = nodeData.inputs?.promptCacheKey as string const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean const imageResolution = nodeData.inputs?.imageResolution as string @@ -256,6 +265,7 @@ class AAIChatOpenAI_ChatModels implements INode { obj.stop = stopSequenceArray } if (strictToolCalling) obj.supportsStrictToolCalling = strictToolCalling + if (promptCacheKey) (obj as any).prompt_cache_key = promptCacheKey let parsedBaseOptions: any | undefined = undefined @@ -294,4 +304,4 @@ class AAIChatOpenAI_ChatModels implements INode { } } -module.exports = { nodeClass: AAIChatOpenAI_ChatModels } \ No newline at end of file +module.exports = { nodeClass: AAIChatOpenAI_ChatModels } diff --git a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts index 31b62d91e20..58239028229 100644 --- a/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts +++ b/packages/components/nodes/chatmodels/ChatOpenAI/ChatOpenAI.ts @@ -199,6 +199,14 @@ class ChatOpenAI_ChatModels implements INode { default: 'medium', optional: false, additionalParams: true + }, + { + label: 'Prompt Cache Key', + name: 'promptCacheKey', + type: 'string', + description: 'Cache key for OpenAI prompt caching to optimize cache hit rates and reduce costs', + optional: true, + additionalParams: true } ] } @@ -225,6 +233,7 @@ class ChatOpenAI_ChatModels implements INode { const proxyUrl = nodeData.inputs?.proxyUrl as string const baseOptions = nodeData.inputs?.baseOptions const reasoningEffort = nodeData.inputs?.reasoningEffort as OpenAIClient.Chat.ChatCompletionReasoningEffort + const promptCacheKey = nodeData.inputs?.promptCacheKey as string const allowImageUploads = nodeData.inputs?.allowImageUploads as boolean const imageResolution = nodeData.inputs?.imageResolution as string @@ -261,6 +270,7 @@ class ChatOpenAI_ChatModels implements INode { obj.stop = stopSequenceArray } if (strictToolCalling) obj.supportsStrictToolCalling = strictToolCalling + if (promptCacheKey) (obj as any).prompt_cache_key = promptCacheKey let parsedBaseOptions: any | undefined = undefined diff --git a/packages/components/nodes/tools/CreateGoogleGenerativeAIImage/GoogleGemini.svg b/packages/components/nodes/tools/CreateGoogleGenerativeAIImage/GoogleGemini.svg new file mode 100644 index 00000000000..53b497fa1a0 --- /dev/null +++ b/packages/components/nodes/tools/CreateGoogleGenerativeAIImage/GoogleGemini.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts b/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts index 5d8f5e1c4e2..3da359a5fc1 100644 --- a/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts +++ b/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts @@ -103,13 +103,15 @@ class AnswerAgent_MCP implements INode { // Get API host from environment variable const apiHost = options.user?.chatflowDomain if (!apiHost) { - throw new Error('API_HOST environment variable is not set') + console.error('AnswerAgent MCP: API_HOST environment variable is not set') + return [] } // Get user's API key from database const apiKey = await this.getUserApiKey(nodeData, options) if (!apiKey) { - throw new Error('Unable to retrieve user API key from database') + console.error('AnswerAgent MCP: Unable to retrieve user API key from database') + return [] } // Get the package path for the AnswerAgent MCP server diff --git a/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts b/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts index 11af2a1b7ea..207fa730c29 100644 --- a/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts +++ b/packages/components/nodes/tools/MCP/Atlassian/AtlassianMcp.ts @@ -125,7 +125,8 @@ class Atlassian_MCP implements INode { const credentialData = await getCredentialData(nodeData.credential || '', options) if (!credentialData.access_token) { - throw new Error('Access token not found in credential data') + console.error('Atlassian MCP: Access token not found in credential data') + return [] } if (!process.env.ATLASSIAN_MCP_SERVER_URL) { diff --git a/packages/components/nodes/tools/MCP/Contentful/ContentfulMCP.ts b/packages/components/nodes/tools/MCP/Contentful/ContentfulMCP.ts index 86cedbe3394..1a843e043c2 100644 --- a/packages/components/nodes/tools/MCP/Contentful/ContentfulMCP.ts +++ b/packages/components/nodes/tools/MCP/Contentful/ContentfulMCP.ts @@ -119,7 +119,8 @@ class Contentful_MCP implements INode { const environmentId = nodeData.inputs?.environmentId if (!managementToken || !spaceId || !environmentId) { - throw new Error('Missing Credentials') + console.error('Contentful MCP: Missing Credentials - managementToken, spaceId, or environmentId not provided') + return [] } const packagePath = getNodeModulesPackagePath('@last-rev/contentful-mcp-server/bin/mcp-server.js') diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts index fc49d237ce7..fecd5d811c2 100644 --- a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts +++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts @@ -78,6 +78,7 @@ class Custom_MCP implements INode { description: rest.description || name })) } catch (error) { + console.error('Custom MCP: Error listing actions:', error) return [ { label: 'No Available Actions', @@ -109,7 +110,8 @@ class Custom_MCP implements INode { const mcpServerConfig = nodeData.inputs?.mcpServerConfig as string if (!mcpServerConfig) { - throw new Error('MCP Server Config is required') + console.error('Custom MCP: MCP Server Config is required') + return [] } try { @@ -135,7 +137,8 @@ class Custom_MCP implements INode { return tools as Tool[] } catch (error) { - throw new Error(`Invalid MCP Server Config: ${error}`) + console.error('Custom MCP: Invalid MCP Server Config:', error) + return [] } } } diff --git a/packages/components/nodes/tools/MCP/Github/GithubMCP.ts b/packages/components/nodes/tools/MCP/Github/GithubMCP.ts index cd6928886b0..a0648b0a88a 100644 --- a/packages/components/nodes/tools/MCP/Github/GithubMCP.ts +++ b/packages/components/nodes/tools/MCP/Github/GithubMCP.ts @@ -106,7 +106,8 @@ class Github_MCP implements INode { const accessToken = getCredentialParam('accessToken', credentialData, nodeData) if (!accessToken) { - throw new Error('Missing Github Access Token') + console.error('Github MCP: Missing Github Access Token') + return [] } const packagePath = getNodeModulesPackagePath('@modelcontextprotocol/server-github/dist/index.js') diff --git a/packages/components/nodes/tools/MCP/PostgreSQL/PostgreSQLMCP.ts b/packages/components/nodes/tools/MCP/PostgreSQL/PostgreSQLMCP.ts index 70d010fdd83..a1ffc61959f 100644 --- a/packages/components/nodes/tools/MCP/PostgreSQL/PostgreSQLMCP.ts +++ b/packages/components/nodes/tools/MCP/PostgreSQL/PostgreSQLMCP.ts @@ -106,7 +106,8 @@ class PostgreSQL_MCP implements INode { const postgresUrl = getCredentialParam('postgresUrl', credentialData, nodeData) if (!postgresUrl) { - throw new Error('No postgres url provided') + console.error('PostgreSQL MCP: No postgres url provided') + return [] } const packagePath = getNodeModulesPackagePath('@modelcontextprotocol/server-postgres/dist/index.js') diff --git a/packages/components/nodes/tools/MCP/SalesforceOauthMcp/SalesforceOauthMcp.ts b/packages/components/nodes/tools/MCP/SalesforceOauthMcp/SalesforceOauthMcp.ts index 04b3d219a3f..41fa740f81e 100644 --- a/packages/components/nodes/tools/MCP/SalesforceOauthMcp/SalesforceOauthMcp.ts +++ b/packages/components/nodes/tools/MCP/SalesforceOauthMcp/SalesforceOauthMcp.ts @@ -115,7 +115,8 @@ class SalesforceOauth_MCP implements INode { // Validate refresh token if (!refreshToken) { - throw new Error('Refresh token is required for Salesforce OAuth MCP') + console.error('Salesforce OAuth MCP: Refresh token is required for Salesforce OAuth MCP') + return [] } // Get environment variables for OAuth configuration @@ -125,9 +126,10 @@ class SalesforceOauth_MCP implements INode { // Validate environment variables if (!salesforceClientId || !salesforceClientSecret || !salesforceInstanceUrl) { - throw new Error( - 'Missing required environment variables: SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET, SALESFORCE_INSTANCE_URL' + console.error( + 'Salesforce OAuth MCP: Missing required environment variables: SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET, SALESFORCE_INSTANCE_URL' ) + return [] } const packagePath = getNodeModulesPackagePath('@answerai/salesforce-mcp/dist/index.js') diff --git a/packages/components/nodes/tools/MCP/Slack/SlackMCP.ts b/packages/components/nodes/tools/MCP/Slack/SlackMCP.ts index f117150f6a7..d5d2cc67a00 100644 --- a/packages/components/nodes/tools/MCP/Slack/SlackMCP.ts +++ b/packages/components/nodes/tools/MCP/Slack/SlackMCP.ts @@ -107,7 +107,8 @@ class Slack_MCP implements INode { const teamId = getCredentialParam('teamId', credentialData, nodeData) if (!botToken || !teamId) { - throw new Error('Missing Credentials') + console.error('Slack MCP: Missing Credentials - botToken or teamId not provided') + return [] } const packagePath = getNodeModulesPackagePath('@modelcontextprotocol/server-slack/dist/index.js') diff --git a/packages/components/nodes/tools/MCP/Youtube/YoutubeMCP.ts b/packages/components/nodes/tools/MCP/Youtube/YoutubeMCP.ts index 4d673c984b7..f794f8a6e98 100644 --- a/packages/components/nodes/tools/MCP/Youtube/YoutubeMCP.ts +++ b/packages/components/nodes/tools/MCP/Youtube/YoutubeMCP.ts @@ -106,7 +106,8 @@ class Youtube_MCP implements INode { const apiKey = getCredentialParam('apiKey', credentialData, nodeData) if (!apiKey) { - throw new Error('Missing Youtube API Key') + console.error('Youtube MCP: Missing Youtube API Key') + return [] } const packagePath = getNodeModulesPackagePath('youtube-data-mcp-server/dist/index.js') diff --git a/packages/components/nodes/tools/MCP/core.ts b/packages/components/nodes/tools/MCP/core.ts index 5f48bfc3255..0fbc82dfa5a 100644 --- a/packages/components/nodes/tools/MCP/core.ts +++ b/packages/components/nodes/tools/MCP/core.ts @@ -87,33 +87,53 @@ export class MCPToolkit extends BaseToolkit { async initialize() { if (this._tools === null) { - this.client = await this.createClient() + try { + this.client = await this.createClient() - this._tools = await this.client.request({ method: 'tools/list' }, ListToolsResultSchema) + this._tools = await this.client.request({ method: 'tools/list' }, ListToolsResultSchema) - this.tools = await this.get_tools() + this.tools = await this.get_tools() - // Close the initial client after initialization - await this.client.close() + // Close the initial client after initialization + await this.client.close() + } catch (error) { + console.error('MCP Toolkit: Failed to initialize, setting empty tools:', error) + this._tools = { tools: [] } + this.tools = [] + if (this.client) { + try { + await this.client.close() + } catch (closeError) { + console.error('MCP Toolkit: Error closing client:', closeError) + } + } + } } } async get_tools(): Promise { - if (this._tools === null || this.client === null) { - throw new Error('Must initialize the toolkit first') + if (this._tools === null) { + console.error('MCP Toolkit: get_tools called before initialization, returning empty array') + return [] } - const toolsPromises = this._tools.tools.map(async (tool: any) => { - if (this.client === null) { - throw new Error('Client is not initialized') - } - return await MCPTool({ - toolkit: this, - name: tool.name, - description: tool.description || '', - argsSchema: createSchemaModel(tool.inputSchema) + if (this.client === null) { + console.error('MCP Toolkit: Client not initialized, returning empty array') + return [] + } + try { + const toolsPromises = this._tools.tools.map(async (tool: any) => { + return await MCPTool({ + toolkit: this, + name: tool.name, + description: tool.description || '', + argsSchema: createSchemaModel(tool.inputSchema) + }) }) - }) - return Promise.all(toolsPromises) + return Promise.all(toolsPromises) + } catch (error) { + console.error('MCP Toolkit: Error creating tools, returning empty array:', error) + return [] + } } } diff --git a/packages/components/package.json b/packages/components/package.json index 8423a39ef3a..3bc46d9cffa 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -24,7 +24,7 @@ "dependencies": { "@answerai/answeragent-mcp": "^1.0.1", "@answerai/confluence-mcp": "^1.0.0", - "@answerai/jira-mcp": "^1.0.2", + "@answerai/jira-mcp": "^1.1.0", "@answerai/salesforce-mcp": "^0.1.0", "@apidevtools/json-schema-ref-parser": "^12.0.2", "@arizeai/openinference-instrumentation-langchain": "^2.0.0", diff --git a/packages/server/src/controllers/documentstore/index.ts b/packages/server/src/controllers/documentstore/index.ts index 7f68d659cbd..668250308ec 100644 --- a/packages/server/src/controllers/documentstore/index.ts +++ b/packages/server/src/controllers/documentstore/index.ts @@ -33,6 +33,23 @@ const getAllDocumentStores = async (req: Request, res: Response, next: NextFunct } } +const getAdminDocumentStores = async (req: Request, res: Response, next: NextFunction) => { + try { + const isAdmin = req.user?.roles?.includes('Admin') || req.user?.permissions?.includes('org:manage') + if (!isAdmin) { + throw new InternalFlowiseError( + StatusCodes.FORBIDDEN, + `Error: documentStoreController.getAdminDocumentStores - admin permissions required!` + ) + } + + const apiResponse = await documentStoreService.getAdminDocumentStores(req.user!) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + const deleteLoaderFromDocumentStore = async (req: Request, res: Response, next: NextFunction) => { try { const storeId = req.params.id @@ -532,6 +549,7 @@ export default { deleteDocumentStore, createDocumentStore, getAllDocumentStores, + getAdminDocumentStores, deleteLoaderFromDocumentStore, getDocumentStoreById, getDocumentStoreFileChunks, diff --git a/packages/server/src/routes/admin/index.ts b/packages/server/src/routes/admin/index.ts index 5d33f6e5f6e..1147ac0b1d1 100644 --- a/packages/server/src/routes/admin/index.ts +++ b/packages/server/src/routes/admin/index.ts @@ -1,5 +1,6 @@ import express from 'express' import chatflowsController from '../../controllers/chatflows' +import documentStoreController from '../../controllers/documentstore' import organizationsController from '../../controllers/organizations' import enforceAbility from '../../middlewares/authentication/enforceAbility' const router = express.Router() @@ -13,6 +14,10 @@ router.get('/chatflows/:id/versions', enforceAbility('ChatFlow'), chatflowsContr router.put('/chatflows/bulk-update', enforceAbility('ChatFlow'), chatflowsController.bulkUpdateChatflows) router.post('/chatflows/:id/rollback/:version', enforceAbility('ChatFlow'), chatflowsController.rollbackChatflowToVersion) +// DOCUMENT STORES +// READ +router.get('/document-stores', enforceAbility('DocumentStore'), documentStoreController.getAdminDocumentStores) + // ORGANIZATIONS // READ router.get('/organizations/credentials', enforceAbility('Organization'), organizationsController.getOrganizationCredentials) diff --git a/packages/server/src/services/documentstore/index.ts b/packages/server/src/services/documentstore/index.ts index 7f72a31434d..d3e16adacc0 100644 --- a/packages/server/src/services/documentstore/index.ts +++ b/packages/server/src/services/documentstore/index.ts @@ -69,6 +69,17 @@ import { INPUT_PARAMS_TYPE, OMIT_QUEUE_JOB_DATA } from '../../utils/constants' const DOCUMENT_STORE_BASE_FOLDER = 'docustore' +type AdminDocumentStoreDTO = DocumentStoreDTO & { + user: { + id?: string + name?: string | null + email?: string | null + } + userId?: string + organizationId?: string + isOwner: boolean +} + const createDocumentStore = async (newDocumentStore: DocumentStore, userId: string, organizationId: string) => { try { const appServer = getRunningExpressApp() @@ -101,6 +112,51 @@ const getAllDocumentStores = async (user: IUser) => { } } +const getAdminDocumentStores = async (user: IUser): Promise => { + try { + const appServer = getRunningExpressApp() + const { id: userId, organizationId, permissions, roles } = user + const documentStoreRepository = appServer.AppDataSource.getRepository(DocumentStore) + const queryBuilder = documentStoreRepository + .createQueryBuilder('documentStore') + .leftJoin('User', 'user', 'user.id = documentStore.userId') + .addSelect(['user.id', 'user.name', 'user.email']) + + if (organizationId) { + queryBuilder.where('documentStore.organizationId = :organizationId', { organizationId }) + } + + const isAdmin = Boolean(roles?.includes('Admin') || permissions?.includes('org:manage')) + if (!isAdmin && userId) { + queryBuilder.andWhere('documentStore.userId = :userId', { userId }) + } + + const rawResults = await queryBuilder.orderBy('documentStore.updatedDate', 'DESC').getRawAndEntities() + + return rawResults.entities.map((entity, index) => { + const dto = DocumentStoreDTO.fromEntity(entity) + const raw = rawResults.raw[index] + + return { + ...dto, + user: { + id: raw?.user_id ?? entity.userId, + name: raw?.user_name ?? null, + email: raw?.user_email ?? null + }, + userId: entity.userId, + organizationId: entity.organizationId, + isOwner: entity.userId === userId + } + }) + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: documentStoreServices.getAdminDocumentStores - ${getErrorMessage(error)}` + ) + } +} + const getAllDocumentFileChunks = async (user: IUser) => { try { const appServer = getRunningExpressApp() @@ -2264,6 +2320,7 @@ export default { createDocumentStore, deleteLoaderFromDocumentStore, getAllDocumentStores, + getAdminDocumentStores, getAllDocumentFileChunks, getDocumentStoreById, getUsedChatflowNames, diff --git a/packages/server/src/utils/getUploadsConfig.ts b/packages/server/src/utils/getUploadsConfig.ts index 937fcab02d7..21b94ee07eb 100644 --- a/packages/server/src/utils/getUploadsConfig.ts +++ b/packages/server/src/utils/getUploadsConfig.ts @@ -80,7 +80,7 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise imgUploadAllowedNodes.includes(node.data.name))) { + // Check if any chat model has allowImageUploads enabled + const hasChatModelWithImageUploads = nodes.some((node: IReactFlowNode) => { + return node.data.category === 'Chat Models' && node.data.inputs?.['allowImageUploads'] === true + }) + + if (nodes.some((node) => imgUploadAllowedNodes.includes(node.data.name)) || hasChatModelWithImageUploads) { nodes.forEach((node: IReactFlowNode) => { const data = node.data if (data.category === 'Chat Models' && data.inputs?.['allowImageUploads'] === true) { diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 5a2faa0fe3f..f48e23af06e 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -68,42 +68,6 @@ import { } from '@aws-sdk/client-secrets-manager' import { fetchMCPMetadata, MCPOAuthMetadata } from './mcp-metadata' -/** - * Enhanced error message for MCP connection errors with deep links to credentials - */ -const enhanceMcpInitializationError = (error: any, reactFlowNode: IReactFlowNode, baseURL: string): string => { - const errorMessage = getErrorMessage(error) - - // Check if this is an MCP connection error - if (errorMessage.includes('MCP') && errorMessage.includes('Connection closed')) { - // Extract credential name from input parameters - let credentialType = '' - if (reactFlowNode.data.inputParams) { - const credentialParam = reactFlowNode.data.inputParams.find((param: any) => param.type === 'credential') - if (credentialParam && credentialParam.credentialNames && credentialParam.credentialNames.length > 0) { - credentialType = credentialParam.credentialNames[0] - } - } - - if (credentialType) { - // Generate deep link URL for credential setup - const credentialLink = `/sidekick-studio/credentials?cred=${credentialType}` - const mcpDocsLink = `https://answeragent.ai/docs/sidekick-studio/chatflows/tools-mcp` - - return `**This Sidekick needs an integration setup.** [Click here to set it up](${credentialLink}). [Read more about how to troubleshoot MCP servers and integrations](${mcpDocsLink}).` - } - - // Generic MCP error message if we can't identify the specific credential - const credentialsLink = `/sidekick-studio/credentials` - const mcpDocsLink = `https://answeragent.ai/docs/sidekick-studio/chatflows/tools-mcp` - - return `**This Sidekick needs an integration setup.** [Click here to check your credentials](${credentialsLink}) | [Read more about how to troubleshoot MCP servers and integrations](${mcpDocsLink}).` - } - - // Return original error message if not an MCP connection error - return errorMessage -} - export const QUESTION_VAR_PREFIX = 'question' export const FILE_ATTACHMENT_PREFIX = 'file_attachment' export const CHAT_HISTORY_VAR_PREFIX = 'chat_history' @@ -743,11 +707,7 @@ export const buildFlow = async ({ } } catch (e: any) { logger.error(e) - - // Enhanced error message for MCP connection issues - const enhancedErrorMessage = enhanceMcpInitializationError(e, reactFlowNode, baseURL || 'http://localhost:3000') - - throw new Error(enhancedErrorMessage) + throw e } let neighbourNodeIds = graph[nodeId] diff --git a/packages/ui/src/api/documentstore.js b/packages/ui/src/api/documentstore.js index 16cf05e252a..d1c89c7988a 100644 --- a/packages/ui/src/api/documentstore.js +++ b/packages/ui/src/api/documentstore.js @@ -1,6 +1,7 @@ import client from './client' const getAllDocumentStores = () => client.get('/document-store/store') +const getAdminDocumentStores = () => client.get('/admin/document-stores') const getDocumentLoaders = () => client.get('/document-store/components/loaders') const getSpecificDocumentStore = (id) => client.get(`/document-store/store/${id}`) const createDocumentStore = (body) => client.post(`/document-store/store`, body) @@ -33,6 +34,7 @@ const generateDocStoreToolDesc = (storeId, body) => client.post('/document-store export default { getAllDocumentStores, + getAdminDocumentStores, getSpecificDocumentStore, createDocumentStore, deleteLoaderFromStore, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30bdb2709ef..dda494acb49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,7 +253,7 @@ importers: version: 14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1) next-auth: specifier: ^4.24.11 - version: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) openai: specifier: 4.96.0 version: 4.96.0(encoding@0.1.13)(ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.25.76) @@ -543,7 +543,7 @@ importers: version: 14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1) next-auth: specifier: ^4.24.11 - version: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -730,7 +730,7 @@ importers: version: 6.12.0(@aws-sdk/credential-providers@3.887.0(aws-crt@1.27.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(socks@2.8.7) next-auth: specifier: ^4.24.11 - version: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) node-fetch: specifier: ^3.3.2 version: 3.3.2 @@ -857,8 +857,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 '@answerai/jira-mcp': - specifier: ^1.0.2 - version: 1.0.2 + specifier: ^1.1.0 + version: 1.1.0 '@answerai/salesforce-mcp': specifier: ^0.1.0 version: 0.1.0(@types/node@24.3.1)(encoding@0.1.13) @@ -2351,8 +2351,8 @@ packages: engines: { node: '>=18.0.0' } hasBin: true - '@answerai/jira-mcp@1.0.2': - resolution: { integrity: sha512-nLdQY6Kfcj/X0u/l+ehG+0bUIyOrPLDIJT2F7eg3ceusgOZpmdUEG02G5KjVDpC4q6SiNxFLxIGuGqupC4QNMw== } + '@answerai/jira-mcp@1.1.0': + resolution: { integrity: sha512-LXRITz1f8L+dGoZEPogn2gWIV5Lg5EVc2orxjc0kL8RUAV0ilq2MO/b8XtLT0CyE2Nkihrmkn4mu7rFmYEQHoA== } engines: { node: '>=18.0.0' } hasBin: true @@ -22089,7 +22089,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@answerai/jira-mcp@1.0.2': + '@answerai/jira-mcp@1.1.0': dependencies: '@modelcontextprotocol/sdk': 1.18.0 typescript: 5.9.2 @@ -28367,7 +28367,7 @@ snapshots: '@next-auth/prisma-adapter@1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))': dependencies: '@prisma/client': 5.22.0(prisma@5.22.0) - next-auth: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next-auth: 4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@next/bundle-analyzer@13.5.11(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -35819,8 +35819,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.5.4) eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@7.32.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1)(eslint@7.32.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@7.32.0) eslint-plugin-react: 7.37.5(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.2(eslint@7.32.0) @@ -35883,7 +35883,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@7.32.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1(supports-color@8.1.1) @@ -35894,7 +35894,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1)(eslint@7.32.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0) transitivePeerDependencies: - supports-color @@ -35913,14 +35913,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@7.32.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@5.5.4) eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@7.32.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0) transitivePeerDependencies: - supports-color @@ -35953,7 +35953,7 @@ snapshots: lodash: 4.17.21 string-natural-compare: 3.0.1 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1)(eslint@7.32.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -35964,7 +35964,7 @@ snapshots: doctrine: 2.1.0 eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@7.32.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0))(eslint@7.32.0))(eslint@7.32.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -41790,7 +41790,7 @@ snapshots: netmask@2.0.2: {} - next-auth@4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + next-auth@4.24.11(next@14.2.32(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.55.0)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.92.1))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.28.4 '@panva/hkdf': 1.2.1