diff --git a/frontend/src/components/feature/settings/ai/bots/AIFeaturesBotForm.tsx b/frontend/src/components/feature/settings/ai/bots/AIFeaturesBotForm.tsx index 18a2064ca..ab4274514 100644 --- a/frontend/src/components/feature/settings/ai/bots/AIFeaturesBotForm.tsx +++ b/frontend/src/components/feature/settings/ai/bots/AIFeaturesBotForm.tsx @@ -139,15 +139,38 @@ const AIFeaturesBotForm = (props: Props) => { )} {isLocalLLM && ( - - - - - - Currently, code interpreter features are not available for Local LLM providers. - These features require OpenAI's infrastructure. - - + <> + + + + + + Currently, code interpreter features are not available for Local LLM providers. + These features require OpenAI's infrastructure. + + + + + + ( + field.onChange(v ? 1 : 0)} + /> + )} + /> + Enable vision (base64 image encoding) + + + + When images are sent to this agent, encode them as base64 and include them directly + in the request. Enable this for vision-capable models (Ollama, LM Studio, etc.). + + + )} Advanced diff --git a/frontend/src/types/RavenBot/RavenBot.ts b/frontend/src/types/RavenBot/RavenBot.ts index 9e551ca07..b3b0fd97f 100644 --- a/frontend/src/types/RavenBot/RavenBot.ts +++ b/frontend/src/types/RavenBot/RavenBot.ts @@ -66,4 +66,6 @@ File search enables the assistant with knowledge from files that you upload. Onc use_google_document_parser?: 0 | 1 /** Google Document Processor ID : Data */ google_document_processor_id?: string + /** Enable base64 image encoding for Local LLM vision : Check - When images are sent to a Local LLM agent, encode them as base64 and include them directly in the request. */ + enable_vision_base64?: 0 | 1 } \ No newline at end of file diff --git a/packages/types/RavenBot/RavenBot.ts b/packages/types/RavenBot/RavenBot.ts index afca165f5..9c3535567 100644 --- a/packages/types/RavenBot/RavenBot.ts +++ b/packages/types/RavenBot/RavenBot.ts @@ -45,4 +45,6 @@ File search enables the assistant with knowledge from files that you upload. Onc dynamic_instructions?: 0 | 1 /** Bot Functions : Table - Raven Bot Functions */ bot_functions?: RavenBotFunctions[] + /** Enable base64 image encoding for Local LLM vision : Check - When images are sent to a Local LLM agent, encode them as base64 and include them directly in the request. */ + enable_vision_base64?: 0 | 1 } \ No newline at end of file diff --git a/raven/ai/agents_integration.py b/raven/ai/agents_integration.py index 5611c0eb3..ecd896e9c 100644 --- a/raven/ai/agents_integration.py +++ b/raven/ai/agents_integration.py @@ -570,8 +570,11 @@ async def handle_ai_request_async( {"role": msg["role"], "content": [{"type": "text", "text": msg["content"]}]} ) - # Add current user message - messages.append({"role": "user", "content": [{"type": "text", "text": message}]}) + # Add current user message (may be a list for vision content with images) + if isinstance(message, list): + messages.append({"role": "user", "content": message}) + else: + messages.append({"role": "user", "content": [{"type": "text", "text": message}]}) # Create the API call with or without tools api_params = { @@ -620,7 +623,10 @@ async def handle_ai_request_async( messages = [ {"role": "system", "content": [{"type": "text", "text": agent.instructions}]}, - {"role": "user", "content": [{"type": "text", "text": str(full_input)}]}, + { + "role": "user", + "content": message if isinstance(message, list) else [{"type": "text", "text": str(full_input)}], + }, assistant_message, ] diff --git a/raven/ai/ai.py b/raven/ai/ai.py index b7926edf0..598e16c27 100644 --- a/raven/ai/ai.py +++ b/raven/ai/ai.py @@ -353,6 +353,55 @@ def extract_file_content_for_agent(file_url: str, file_extension: str, bot, file return extracted_content +def _build_vision_content(file_url: str, user_text: str = "") -> list: + """ + Build an OpenAI-compatible vision content list with a base64-encoded image. + Used for local LLMs with vision capability (Ollama, LM Studio, etc.) that accept + the OpenAI chat completions format but cannot fetch server-side file paths. + """ + import base64 + import mimetypes + + content = [] + + if user_text: + content.append({"type": "text", "text": user_text}) + + try: + file_doc = frappe.get_doc("File", {"file_url": file_url}) + file_path = file_doc.get_full_path() + + with open(file_path, "rb") as f: + image_data = base64.b64encode(f.read()).decode("utf-8") + + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type: + ext = file_path.rsplit(".", 1)[-1].lower() if "." in file_path else "jpeg" + mime_map = { + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "png": "image/png", + "gif": "image/gif", + "webp": "image/webp", + } + mime_type = mime_map.get(ext, "image/jpeg") + + content.append( + { + "type": "image_url", + "image_url": {"url": f"data:{mime_type};base64,{image_data}"}, + } + ) + except Exception as e: + frappe.log_error( + f"Error encoding image for Local LLM vision: {str(e)}\nFile: {file_url}", + "Image Vision Encoding Error", + ) + content.append({"type": "text", "text": f"[Image: {file_url}]"}) + + return content + + def process_message_with_agent( message, bot, channel_id: str, is_new_conversation: bool, channel=None ): @@ -428,11 +477,21 @@ def process_message_with_agent( if message.text or message.content: content += f"\n\nUser's question: {message.text or message.content}" else: - content = f"[User uploaded an image: {file_url}]" + user_text = "" if extracted_content: - content += f"\n\nExtracted content from the image:\n{extracted_content}" + user_text = f"Extracted content from the image:\n{extracted_content}\n\n" if message.text or message.content: - content += f"\n\nUser's question: {message.text or message.content}" + user_text += message.text or message.content + + if bot.model_provider == "Local LLM" and getattr(bot, "enable_vision_base64", 1): + # Encode image as base64 so local vision models (Ollama etc.) can see it + content = _build_vision_content(file_url, user_text or None) + else: + content = f"[User uploaded an image: {file_url}]" + if extracted_content: + content += f"\n\nExtracted content from the image:\n{extracted_content}" + if message.text or message.content: + content += f"\n\nUser's question: {message.text or message.content}" else: content = message.text or message.content or "" @@ -446,13 +505,20 @@ def process_message_with_agent( file_extension = file_url.split(".")[-1].lower() if "." in file_url else "" extracted_content = extract_file_content_for_agent(file_url, file_extension, bot, file_handler) - file_prefix = f"[User uploaded a {'file' if recent_file_message.message_type == 'File' else 'image'}: {file_url}]" - if extracted_content: - file_prefix += f"\n\nExtracted content from the file:\n{extracted_content}\n\n" + if recent_file_message.message_type == "Image" and bot.model_provider == "Local LLM" and getattr(bot, "enable_vision_base64", 1): + # Encode image as base64 for local vision models, combining with the user's text + vision_text = "" + if extracted_content: + vision_text = f"Extracted content from the image:\n{extracted_content}\n\n" + vision_text += content + content = _build_vision_content(file_url, vision_text) else: - file_prefix += "\n" - - content = file_prefix + content + file_prefix = f"[User uploaded a {'file' if recent_file_message.message_type == 'File' else 'image'}: {file_url}]" + if extracted_content: + file_prefix += f"\n\nExtracted content from the file:\n{extracted_content}\n\n" + else: + file_prefix += "\n" + content = file_prefix + content # Get conversation history if this is an existing thread conversation_history = [] diff --git a/raven/raven_bot/doctype/raven_bot/raven_bot.json b/raven/raven_bot/doctype/raven_bot/raven_bot.json index 878b03751..59970e9e0 100644 --- a/raven/raven_bot/doctype/raven_bot/raven_bot.json +++ b/raven/raven_bot/doctype/raven_bot/raven_bot.json @@ -244,6 +244,13 @@ "fieldtype": "Data", "label": "Google Document Processor ID", "length": 400 + }, + { + "default": "0", + "description": "When images are sent to a Local LLM agent, encode them as base64 and include them directly in the request. Enable this for vision-capable local models (Ollama, LM Studio, etc.).", + "fieldname": "enable_vision_base64", + "fieldtype": "Check", + "label": "Enable base64 image encoding for Local LLM vision" } ], "grid_page_length": 50,