diff --git a/backend/open_webui/utils/files.py b/backend/open_webui/utils/files.py new file mode 100644 index 0000000000..f5510eec35 --- /dev/null +++ b/backend/open_webui/utils/files.py @@ -0,0 +1,21 @@ +from open_webui.routers.images import ( + load_b64_image_data, + upload_image, +) + + +def get_image_url_from_base64(request, base64_image_string, metadata, user): + if "data:image/png;base64" in base64_image_string: + image_url = "" + # Extract base64 image data from the line + image_data, content_type = load_b64_image_data(base64_image_string) + if image_data is not None: + image_url = upload_image( + request, + image_data, + content_type, + metadata, + user, + ) + return image_url + return None diff --git a/backend/open_webui/utils/mcp/client.py b/backend/open_webui/utils/mcp/client.py index 9b9d92f10d..875076d1a7 100644 --- a/backend/open_webui/utils/mcp/client.py +++ b/backend/open_webui/utils/mcp/client.py @@ -60,7 +60,16 @@ class MCPClient: raise RuntimeError("MCP client is not connected.") result = await self.session.call_tool(function_name, function_args) - return result.model_dump() + if not result: + raise Exception("No result returned from MCP tool call.") + + result_dict = result.model_dump() + result_content = result_dict.get("content", {}) + + if result.isError: + raise Exception(result_content) + else: + return result_content async def disconnect(self): # Clean up and close the session diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 286e40ebad..3b4fc43858 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -53,6 +53,7 @@ from open_webui.routers.pipelines import ( from open_webui.routers.memories import query_memory, QueryMemoryForm from open_webui.utils.webhook import post_webhook +from open_webui.utils.files import get_image_url_from_base64 from open_webui.models.users import UserModel @@ -1052,9 +1053,6 @@ async def process_chat_payload(request, form_data, user, metadata, model): def make_tool_function(function_name): async def tool_function(**kwargs): - print( - f"Calling MCP tool {function_name} with args {kwargs}" - ) return await mcp_client.call_tool( function_name, function_args=kwargs, @@ -1106,7 +1104,6 @@ async def process_chat_payload(request, form_data, user, metadata, model): metadata["mcp_clients"] = mcp_clients if tools_dict: - log.info(f"tools_dict: {tools_dict}") if metadata.get("params", {}).get("function_calling") == "native": # If the function calling is native, then call the tools function calling handler metadata["tools"] = tools_dict @@ -2487,20 +2484,14 @@ async def process_chat_response( else: tool_function = tool["callable"] - - print("tool_name", tool_name) - print("tool_function", tool_function) - print("tool_function_params", tool_function_params) tool_result = await tool_function( **tool_function_params ) - print("tool_result", tool_result) except Exception as e: tool_result = str(e) tool_result_embeds = [] - if isinstance(tool_result, HTMLResponse): content_disposition = tool_result.headers.get( "Content-Disposition", "" @@ -2573,9 +2564,58 @@ async def process_chat_response( for item in tool_result: # check if string if isinstance(item, str) and item.startswith("data:"): - tool_result_files.append(item) + tool_result_files.append( + { + "type": "data", + "content": item, + } + ) tool_result.remove(item) + if tool.get("type") == "mcp": + if ( + isinstance(item, dict) + and item.get("type") == "image" + ): + image_url = get_image_url_from_base64( + request, + f"data:{item.get('mimeType', 'image/png')};base64,{item.get('data', '')}", + { + "chat_id": metadata.get( + "chat_id", None + ), + "message_id": metadata.get( + "message_id", None + ), + "session_id": metadata.get( + "session_id", None + ), + }, + user, + ) + + tool_result_files.append( + { + "type": "image", + "url": image_url, + } + ) + tool_result.remove(item) + + if tool_result_files: + if not isinstance(tool_result, list): + tool_result = [ + tool_result, + ] + + for file in tool_result_files: + tool_result.append( + { + "type": file.get("type", "data"), + "content": "Displayed", + } + ) + if isinstance(tool_result, dict) or isinstance( tool_result, list ): @@ -2742,23 +2782,18 @@ async def process_chat_response( if isinstance(stdout, str): stdoutLines = stdout.split("\n") for idx, line in enumerate(stdoutLines): + if "data:image/png;base64" in line: - image_url = "" - # Extract base64 image data from the line - image_data, content_type = ( - load_b64_image_data(line) + image_url = get_image_url_from_base64( + request, + line, + metadata, + user, ) - if image_data is not None: - image_url = upload_image( - request, - image_data, - content_type, - metadata, - user, + if image_url: + stdoutLines[idx] = ( + f"![Output Image]({image_url})" ) - stdoutLines[idx] = ( - f"![Output Image]({image_url})" - ) output["stdout"] = "\n".join(stdoutLines) @@ -2768,19 +2803,12 @@ async def process_chat_response( resultLines = result.split("\n") for idx, line in enumerate(resultLines): if "data:image/png;base64" in line: - image_url = "" - # Extract base64 image data from the line - image_data, content_type = ( - load_b64_image_data(line) + image_url = get_image_url_from_base64( + request, + line, + metadata, + user, ) - if image_data is not None: - image_url = upload_image( - request, - image_data, - content_type, - metadata, - user, - ) resultLines[idx] = ( f"![Output Image]({image_url})" ) diff --git a/backend/requirements.txt b/backend/requirements.txt index 81fc0beb45..990aaf7d26 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -46,6 +46,7 @@ anthropic google-genai==1.32.0 google-generativeai==0.8.5 tiktoken +mcp==1.14.1 langchain==0.3.26 langchain-community==0.3.27 diff --git a/pyproject.toml b/pyproject.toml index bed62ee4e7..20f977530f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,8 @@ dependencies = [ "asgiref==3.8.1", "tiktoken", + "mcp==1.14.1", + "openai", "anthropic", "google-genai==1.32.0", diff --git a/src/lib/components/common/Collapsible.svelte b/src/lib/components/common/Collapsible.svelte index 4f4f6448be..8f788c6d89 100644 --- a/src/lib/components/common/Collapsible.svelte +++ b/src/lib/components/common/Collapsible.svelte @@ -191,20 +191,30 @@ {/if} {/if} + {/if} + {/if} - {#if attributes?.done === 'true'} - {#if typeof files === 'object'} - {#each files ?? [] as file, idx} - {#if file.startsWith('data:image/')} - Image - {/if} - {/each} + {#if attributes?.done === 'true'} + {#if typeof files === 'object'} + {#each files ?? [] as file, idx} + {#if typeof file === 'string'} + {#if file.startsWith('data:image/')} + Image + {/if} + {:else if typeof file === 'object'} + {#if file.type === 'image' && file.url} + Image + {/if} {/if} - {/if} + {/each} {/if} {/if} {:else}