From 2fae50a39f8f6e3d8cc4c4ccec497091952e1a03 Mon Sep 17 00:00:00 2001 From: fujie Date: Sun, 22 Mar 2026 22:20:34 +0800 Subject: [PATCH] feat: remove iflow-sdk-pipe plugin, its READMEs, and associated Python code --- plugins/pipes/iflow-sdk-pipe/README.md | 52 -- plugins/pipes/iflow-sdk-pipe/README_CN.md | 50 -- .../pipes/iflow-sdk-pipe/iflow_sdk_pipe.py | 544 ------------------ 3 files changed, 646 deletions(-) delete mode 100644 plugins/pipes/iflow-sdk-pipe/README.md delete mode 100644 plugins/pipes/iflow-sdk-pipe/README_CN.md delete mode 100644 plugins/pipes/iflow-sdk-pipe/iflow_sdk_pipe.py diff --git a/plugins/pipes/iflow-sdk-pipe/README.md b/plugins/pipes/iflow-sdk-pipe/README.md deleted file mode 100644 index 09b9d51..0000000 --- a/plugins/pipes/iflow-sdk-pipe/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# iFlow Official SDK Pipe - -This plugin integrates the [iFlow SDK](https://platform.iflow.cn/cli/sdk/sdk-python) into OpenWebUI as a `Pipe`. - -## Install with Batch Install Plugins - -If you already use [Batch Install Plugins from GitHub](https://github.com/Fu-Jie/openwebui-extensions/tree/main/plugins/tools/batch-install-plugins), you can install or update this plugin with: - -```text -Install plugin from Fu-Jie/openwebui-extensions -``` - -When the selection dialog opens, search for this plugin, check it, and continue. - -> [!IMPORTANT] -> If the official OpenWebUI Community version is already installed, remove it first. After that, Batch Install Plugins can keep this plugin updated in future runs. - -## Features - -- **Standard iFlow Integration**: Connects to the iFlow CLI process via WebSocket (ACP). -- **Auto-Process Management**: Automatically starts the iFlow process if it's not running. -- **Streaming Support**: Direct streaming from iFlow to the chat interface. -- **Status Updates**: Real-time status updates in the UI (thinking, tool usage, etc.). -- **Tool Execution Visibility**: See when iFlow is calling and completing tools. - -## Configuration - -Set the following `Valves`: - -- `IFLOW_PORT`: The port for the iFlow CLI process (default: `8090`). -- `IFLOW_URL`: The WebSocket URL (default: `ws://localhost:8090/acp`). -- `AUTO_START`: Automatically start the process (default: `True`). -- `TIMEOUT`: Request timeout in seconds. -- `LOG_LEVEL`: SDK logging level (DEBUG, INFO, etc.). - -## Installation - -This plugin requires both the **iFlow CLI** binary and the **iflow-cli-sdk** Python package. - -### 1. Install iFlow CLI (System level) - -Run the following command in your terminal (Linux/macOS): - -```bash -bash -c "$(curl -fsSL https://platform.iflow.cn/cli/install.sh)" -``` - -### 2. Install Python SDK (OpenWebUI environment) - -```bash -pip install iflow-cli-sdk -``` diff --git a/plugins/pipes/iflow-sdk-pipe/README_CN.md b/plugins/pipes/iflow-sdk-pipe/README_CN.md deleted file mode 100644 index 6aa532d..0000000 --- a/plugins/pipes/iflow-sdk-pipe/README_CN.md +++ /dev/null @@ -1,50 +0,0 @@ -# iFlow 官方 SDK Pipe 插件 - -此插件将 [iFlow SDK](https://platform.iflow.cn/cli/sdk/sdk-python) 集成到 OpenWebUI 中。 - -## 使用 Batch Install Plugins 安装 - -如果你已经安装了 [Batch Install Plugins from GitHub](https://github.com/Fu-Jie/openwebui-extensions/tree/main/plugins/tools/batch-install-plugins),可以用下面这句来安装或更新当前插件: - -```text -从 Fu-Jie/openwebui-extensions 安装插件 -``` - -当选择弹窗打开后,搜索当前插件,勾选后继续安装即可。 - -> [!IMPORTANT] -> 如果你已经安装了 OpenWebUI 官方社区里的同名版本,请先删除旧版本,否则重新安装时可能报错。删除后,Batch Install Plugins 后续就可以继续负责更新这个插件。 - -## 功能特性 - -- **标准 iFlow 集成**:通过 WebSocket (ACP) 连接到 iFlow CLI 进程。 -- **自动进程管理**:如果 iFlow 进程未运行,将自动启动。 -- **流式输出支持**:支持从 iFlow 到聊天界面的实时流式输出。 -- **实时状态更新**:在 UI 中实时显示助手状态(思考中、工具调用等)。 -- **工具调用可视化**:实时反馈 iFlow 调用及完成工具的过程。 - -## 配置项 (Valves) - -- `IFLOW_PORT`:iFlow CLI 进程端口(默认:`8090`)。 -- `IFLOW_URL`:WebSocket 地址(默认:`ws://localhost:8090/acp`)。 -- `AUTO_START`:是否自动启动进程(默认:`True`)。 -- `TIMEOUT`:请求超时时间(秒)。 -- `LOG_LEVEL`:SDK 日志级别(DEBUG, INFO 等)。 - -## 安装说明 - -此插件同时依赖 **iFlow CLI** 二进制文件和 **iflow-cli-sdk** Python 包。 - -### 1. 安装 iFlow CLI (系统层级) - -在系统中执行以下命令(适用于 Linux/macOS): - -```bash -bash -c "$(curl -fsSL https://gitee.com/iflow-ai/iflow-cli/raw/main/install.sh)" -``` - -### 2. 安装 Python SDK (OpenWebUI 环境) - -```bash -pip install iflow-cli-sdk -``` diff --git a/plugins/pipes/iflow-sdk-pipe/iflow_sdk_pipe.py b/plugins/pipes/iflow-sdk-pipe/iflow_sdk_pipe.py deleted file mode 100644 index e1cd824..0000000 --- a/plugins/pipes/iflow-sdk-pipe/iflow_sdk_pipe.py +++ /dev/null @@ -1,544 +0,0 @@ -""" -title: iFlow Official SDK Pipe -author: Fu-Jie -author_url: https://github.com/Fu-Jie/openwebui-extensions -funding_url: https://github.com/open-webui -description: Integrate iFlow SDK. Supports dynamic models, multi-turn conversation, streaming, tool execution, and task planning. -version: 0.1.2 -requirements: iflow-cli-sdk==0.1.11 -""" - -import shutil -import subprocess - -import os -import json -import asyncio -import logging -from typing import Optional, Union, AsyncGenerator, List, Any, Dict, Literal -from pydantic import BaseModel, Field - -# Setup logger -logger = logging.getLogger(__name__) - -# Import iflow SDK modules with safety -IFlowClient = None -IFlowOptions = None -AssistantMessage = None -TaskFinishMessage = None -ToolCallMessage = None -PlanMessage = None -TaskStatusMessage = None -ApprovalMode = None -StopReason = None - -try: - from iflow_sdk import ( - IFlowClient, - IFlowOptions, - AssistantMessage, - TaskFinishMessage, - ToolCallMessage, - PlanMessage, - TaskStatusMessage, - ApprovalMode, - StopReason, - ) -except ImportError: - logger.error( - "iflow-cli-sdk not found. Please install it with 'pip install iflow-cli-sdk'." - ) - -# Base guidelines for all users, adapted for iFlow -BASE_GUIDELINES = ( - "\n\n[Environment & Capabilities Context]\n" - "You are an AI assistant operating within a high-capability Linux container environment (OpenWebUI) powered by **iFlow CLI**.\n" - "\n" - "**System Environment & User Privileges:**\n" - "- **Output Environment**: You are rendering in the **OpenWebUI Chat Page**. Optimize your output format to leverage Markdown for the best UI experience.\n" - "- **Root Access**: You are running as **root**. You have **READ access to the entire container file system**. You **MUST ONLY WRITE** to your designated persistent workspace directory.\n" - "- **STRICT FILE CREATION RULE**: You are **PROHIBITED** from creating or editing files outside of your specific workspace path. Never place files in `/root`, `/tmp`, or `/app`. All operations must use the absolute path provided in your session context.\n" - "- **iFlow Task Planning**: You possess **Task Planning** capabilities. When faced with complex requests, you SHOULD generate a structured plan. The iFlow SDK will visualize this plan as a task list for the user.\n" - "- **Tool Execution (ACP)**: You interact with tools via the **Agent Control Protocol (ACP)**. Depending on the `ApprovalMode`, your tool calls may be executed automatically or require user confirmation.\n" - "- **Rich Python Environment**: You can natively import and use any installed OpenWebUI dependencies.\n" - "\n" - "**Formatting & Presentation Directives:**\n" - "1. **Markdown Excellence**: Leverage headers, tables, and lists to structure your response professionally.\n" - "2. **Advanced Visualization**: Use **Mermaid** for diagrams and **LaTeX** for math. Always wrap Mermaid in standard ```mermaid blocks.\n" - "3. **Interactive Artifacts (HTML)**: **Premium Delivery Protocol**: For web applications, you MUST:\n" - " - 1. **Persist**: Create the file in the workspace (e.g., `index.html`).\n" - " - 2. **Publish**: Call `publish_file_from_workspace(filename='your_file.html')` (via provided tools if available). This triggers the premium embedded experience.\n" - " - **CRITICAL**: Never output raw HTML source code directly in the chat. Persist and publish.\n" - "4. **Media & Files**: ALWAYS embed generated media using `![caption](url)`. Never provide plain text links for images/videos.\n" - "5. **Dual-Channel Delivery**: Always aim to provide both an instant visual Insight in the chat AND a persistent downloadable file.\n" - "6. **Active & Autonomous**: Analyze the user's request -> Formulate a plan -> **EXECUTE** the plan immediately. Minimize user friction.\n" -) - -# Sensitive extensions only for Administrators -ADMIN_EXTENSIONS = ( - "\n**[ADMINISTRATOR PRIVILEGES - CONFIDENTIAL]**\n" - "Current user is an **ADMINISTRATOR**. Restricted access is lifted:\n" - "- **Full OS Interaction**: You can use shell tools to analyze any container process or system configuration.\n" - "- **Database Access**: You can connect to the **OpenWebUI Database** using credentials in environment variables.\n" - "- **iFlow CLI Debugging**: You can inspect iFlow configuration and logs for diagnostic purposes.\n" - "**SECURITY NOTE**: Protect sensitive internal details.\n" -) - -# Strict restrictions for regular Users -USER_RESTRICTIONS = ( - "\n**[USER ACCESS RESTRICTIONS - STRICT]**\n" - "Current user is a **REGULAR USER**. Adhere to boundaries:\n" - "- **NO Environment Access**: FORBIDDEN from accessing environment variables (e.g., via `env` or `os.environ`).\n" - "- **NO Database Access**: MUST NOT attempt to connect to OpenWebUI database.\n" - "- **NO Writing Outside Workspace**: All artifacts MUST be saved strictly inside the isolated workspace path provided.\n" - "- **Restricted Shell**: Use shell tools ONLY for operations within your isolated workspace. Do NOT explore system secrets.\n" -) - - -class Pipe: - class Valves(BaseModel): - IFLOW_PORT: int = Field( - default=8090, - description="Port for iFlow CLI process.", - ) - IFLOW_URL: str = Field( - default="ws://localhost:8090/acp", - description="WebSocket URL for iFlow ACP.", - ) - AUTO_START: bool = Field( - default=True, - description="Whether to automatically start the iFlow process.", - ) - TIMEOUT: float = Field( - default=300.0, - description="Timeout for the message request (seconds).", - ) - LOG_LEVEL: str = Field( - default="INFO", - description="Log level for iFlow SDK (DEBUG, INFO, WARNING, ERROR).", - ) - CWD: str = Field( - default="", - description="CLI operation working directory. Empty for default.", - ) - APPROVAL_MODE: Literal["DEFAULT", "AUTO_EDIT", "YOLO", "PLAN"] = Field( - default="YOLO", - description="Tool execution permission mode.", - ) - FILE_ACCESS: bool = Field( - default=False, - description="Enable file system access (disabled by default for security).", - ) - AUTO_INSTALL_CLI: bool = Field( - default=True, - description="Automatically install iFlow CLI if not found in PATH.", - ) - IFLOW_BIN_DIR: str = Field( - default="/app/backend/data/bin", - description="Fixed path for iFlow CLI binary (recommended for persistence in Docker).", - ) - - # Auth Config - SELECTED_AUTH_TYPE: Literal["iflow", "openai-compatible"] = Field( - default="iflow", - description="Authentication type. 'iflow' for native, 'openai-compatible' for others.", - ) - AUTH_API_KEY: str = Field( - default="", - description="API Key for the model provider.", - ) - AUTH_BASE_URL: str = Field( - default="", - description="Base URL for the model provider.", - ) - AUTH_MODEL: str = Field( - default="", - description="Model name to use.", - ) - SYSTEM_PROMPT: str = Field( - default="", - description="System prompt to guide the AI's behavior.", - ) - - def __init__(self): - self.type = "pipe" - self.id = "iflow_sdk" - self.name = "iflow" - self.valves = self.Valves() - - def _get_user_role(self, __user__: dict) -> str: - """Determine if the user is an admin.""" - return __user__.get("role", "user") - - def _get_system_prompt(self, role: str) -> str: - """Construct the dynamic system prompt based on user role.""" - prompt = self.valves.SYSTEM_PROMPT if self.valves.SYSTEM_PROMPT else "" - prompt += BASE_GUIDELINES - if role == "admin": - prompt += ADMIN_EXTENSIONS - else: - prompt += USER_RESTRICTIONS - return prompt - - async def _ensure_cli(self, _emit_status) -> bool: - """Check for iFlow CLI and attempt installation if missing.""" - - async def _check_binary(name: str) -> Optional[str]: - # 1. Check in system PATH - path = shutil.which(name) - if path: - return path - - # 2. Compile potential search paths - search_paths = [] - - # Try to resolve NPM global prefix - try: - proc = await asyncio.create_subprocess_exec( - "npm", - "config", - "get", - "prefix", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, _ = await proc.communicate() - if proc.returncode == 0: - prefix = stdout.decode().strip() - search_paths.extend( - [ - os.path.join(prefix, "bin"), - os.path.join(prefix, "node_modules", ".bin"), - prefix, - ] - ) - except: - pass - - if self.valves.IFLOW_BIN_DIR: - search_paths.extend( - [ - self.valves.IFLOW_BIN_DIR, - os.path.join(self.valves.IFLOW_BIN_DIR, "bin"), - ] - ) - - # Common/default locations - search_paths.extend( - [ - os.path.expanduser("~/.iflow/bin"), - os.path.expanduser("~/.npm-global/bin"), - os.path.expanduser("~/.local/bin"), - "/usr/local/bin", - "/usr/bin", - "/bin", - os.path.expanduser("~/bin"), - ] - ) - - for p in search_paths: - full_path = os.path.join(p, name) - if os.path.exists(full_path) and os.access(full_path, os.X_OK): - return full_path - return None - - # Initial check - binary_path = await _check_binary("iflow") - if binary_path: - logger.info(f"iFlow CLI found at: {binary_path}") - bin_dir = os.path.dirname(binary_path) - if bin_dir not in os.environ["PATH"]: - os.environ["PATH"] = f"{bin_dir}:{os.environ['PATH']}" - return True - - if not self.valves.AUTO_INSTALL_CLI: - return False - - try: - install_loc_msg = ( - self.valves.IFLOW_BIN_DIR - if self.valves.IFLOW_BIN_DIR - else "default location" - ) - await _emit_status( - f"iFlow CLI not found. Attempting auto-installation to {install_loc_msg}..." - ) - - # Detection for package managers and official script - env = os.environ.copy() - has_npm = shutil.which("npm") is not None - has_curl = shutil.which("curl") is not None - - if has_npm: - if self.valves.IFLOW_BIN_DIR: - os.makedirs(self.valves.IFLOW_BIN_DIR, exist_ok=True) - install_cmd = f"npm i -g --prefix {self.valves.IFLOW_BIN_DIR} @iflow-ai/iflow-cli@latest" - else: - install_cmd = "npm i -g @iflow-ai/iflow-cli@latest" - elif has_curl: - await _emit_status( - "npm not found. Attempting to use official shell installer via curl..." - ) - # Official installer script from gitee/github as fallback - # We try gitee first as it's more reliable in some environments - install_cmd = 'bash -c "$(curl -fsSL https://gitee.com/iflow-ai/iflow-cli/raw/main/install.sh)"' - # If we have a custom bin dir, try to tell the installer (though it might not support it) - if self.valves.IFLOW_BIN_DIR: - env["IFLOW_BIN_DIR"] = self.valves.IFLOW_BIN_DIR - else: - await _emit_status( - "Error: Neither 'npm' nor 'curl' found. Cannot proceed with auto-installation." - ) - return False - - process = await asyncio.create_subprocess_shell( - install_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env=env, - ) - stdout_data, stderr_data = await process.communicate() - - # Even if the script returns non-zero (which it might if it tries to - # start an interactive shell at the end), we check if the binary exists. - await _emit_status( - "Installation script finished. Finalizing verification..." - ) - binary_path = await _check_binary("iflow") - - if binary_path: - try: - os.chmod(binary_path, 0o755) - except: - pass - await _emit_status(f"iFlow CLI confirmed at {binary_path}.") - bin_dir = os.path.dirname(binary_path) - if bin_dir not in os.environ["PATH"]: - os.environ["PATH"] = f"{bin_dir}:{os.environ['PATH']}" - return True - else: - # Script failed and no binary - error_msg = ( - stderr_data.decode().strip() or "Binary not found in search paths" - ) - logger.error( - f"Installation failed with code {process.returncode}: {error_msg}" - ) - await _emit_status(f"Installation failed: {error_msg}") - return False - except Exception as e: - logger.error(f"Error during installation: {str(e)}") - await _emit_status(f"Installation error: {str(e)}") - return False - - async def _ensure_sdk(self, _emit_status) -> bool: - """Check for iflow-cli-sdk Python package and attempt installation if missing.""" - global IFlowClient, IFlowOptions, AssistantMessage, TaskFinishMessage, ToolCallMessage, PlanMessage, TaskStatusMessage, ApprovalMode, StopReason - - if IFlowClient is not None: - return True - - await _emit_status("iflow-cli-sdk not found. Attempting auto-installation...") - try: - # Use sys.executable to ensure we use the same Python environment - import sys - - process = await asyncio.create_subprocess_exec( - sys.executable, - "-m", - "pip", - "install", - "iflow-cli-sdk", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - - if process.returncode == 0: - await _emit_status("iflow-cli-sdk installed successfully. Loading...") - # Try to import again - from iflow_sdk import ( - IFlowClient as C, - IFlowOptions as O, - AssistantMessage as AM, - TaskFinishMessage as TM, - ToolCallMessage as TC, - PlanMessage as P, - TaskStatusMessage as TS, - ApprovalMode as AP, - StopReason as SR, - ) - - # Update global pointers - IFlowClient, IFlowOptions = C, O - AssistantMessage, TaskFinishMessage = AM, TM - ToolCallMessage, PlanMessage = TC, P - TaskStatusMessage, ApprovalMode, StopReason = TS, AP, SR - return True - else: - error_msg = stderr.decode().strip() - logger.error(f"SDK installation failed: {error_msg}") - await _emit_status(f"SDK installation failed: {error_msg}") - return False - except Exception as e: - logger.error(f"Error during SDK installation: {str(e)}") - await _emit_status(f"SDK installation error: {str(e)}") - return False - - async def pipe( - self, body: dict, __user__: dict, __event_emitter__=None - ) -> Union[str, AsyncGenerator[str, None]]: - """Main entry point for the pipe.""" - - async def _emit_status(description: str, done: bool = False): - if __event_emitter__: - await __event_emitter__( - { - "type": "status", - "data": { - "description": description, - "done": done, - }, - } - ) - - # 0. Ensure SDK and CLI are available - if not await self._ensure_sdk(_emit_status): - return "Error: iflow-cli-sdk (Python package) missing and auto-installation failed. Please install it with `pip install iflow-cli-sdk` manually." - - # 1. Update PATH to include custom bin dir - if self.valves.IFLOW_BIN_DIR not in os.environ["PATH"]: - os.environ["PATH"] = f"{self.valves.IFLOW_BIN_DIR}:{os.environ['PATH']}" - - # 2. Ensure CLI is installed and path is updated - if not await self._ensure_cli(_emit_status): - return f"Error: iFlow CLI not found and auto-installation failed. Please install it to {self.valves.IFLOW_BIN_DIR} manually." - - messages = body.get("messages", []) - if not messages: - return "No messages provided." - - # Get the last user message - last_message = messages[-1] - content = last_message.get("content", "") - - # Determine user role and construct prompt - role = self._get_user_role(__user__) - dynamic_prompt = self._get_system_prompt(role) - - # Prepare Auth Info - auth_info = None - if self.valves.AUTH_API_KEY: - auth_info = { - "api_key": self.valves.AUTH_API_KEY, - "base_url": self.valves.AUTH_BASE_URL, - "model_name": self.valves.AUTH_MODEL, - } - - # Prepare Session Settings - session_settings = None - try: - from iflow_sdk import SessionSettings - - session_settings = SessionSettings(system_prompt=dynamic_prompt) - except ImportError: - session_settings = {"system_prompt": dynamic_prompt} - - # 2. Configure iFlow Options - # Use local references to ensure we're using the freshly imported SDK components - from iflow_sdk import ( - IFlowOptions as SDKOptions, - ApprovalMode as SDKApprovalMode, - ) - - # Get approval mode with a safe fallback - try: - target_mode = getattr(SDKApprovalMode, self.valves.APPROVAL_MODE) - except (AttributeError, TypeError): - target_mode = ( - SDKApprovalMode.YOLO if hasattr(SDKApprovalMode, "YOLO") else None - ) - - options = SDKOptions( - url=self.valves.IFLOW_URL, - auto_start_process=self.valves.AUTO_START, - process_start_port=self.valves.IFLOW_PORT, - timeout=self.valves.TIMEOUT, - log_level=self.valves.LOG_LEVEL, - cwd=self.valves.CWD or None, - approval_mode=target_mode, - file_access=self.valves.FILE_ACCESS, - auth_method_id=self.valves.SELECTED_AUTH_TYPE if auth_info else None, - auth_method_info=auth_info, - session_settings=session_settings, - ) - - async def _emit_status(description: str, done: bool = False): - if __event_emitter__: - await __event_emitter__( - { - "type": "status", - "data": { - "description": description, - "done": done, - }, - } - ) - - # 3. Stream from iFlow - async def stream_generator(): - try: - await _emit_status("Initializing iFlow connection...") - - async with IFlowClient(options) as client: - await client.send_message(content) - - await _emit_status("iFlow is processing...") - - async for message in client.receive_messages(): - if isinstance(message, AssistantMessage): - yield message.chunk.text - if message.agent_info and message.agent_info.agent_id: - logger.debug( - f"Message from agent: {message.agent_info.agent_id}" - ) - - elif isinstance(message, PlanMessage): - plan_str = "\n".join( - [ - f"{'✅' if e.status == 'completed' else '⏳'} [{e.priority}] {e.content}" - for e in message.entries - ] - ) - await _emit_status(f"Execution Plan updated:\n{plan_str}") - - elif isinstance(message, TaskStatusMessage): - await _emit_status(f"iFlow: {message.status}") - - elif isinstance(message, ToolCallMessage): - tool_desc = ( - f"Calling tool: {message.tool_name}" - if message.tool_name - else "Invoking tool" - ) - await _emit_status( - f"{tool_desc}... (Status: {message.status})" - ) - - elif isinstance(message, TaskFinishMessage): - reason_msg = "Task completed." - if message.stop_reason == StopReason.MAX_TOKENS: - reason_msg = "Task stopped: Max tokens reached." - elif message.stop_reason == StopReason.END_TURN: - reason_msg = "Task completed successfully." - - await _emit_status(reason_msg, done=True) - break - - except Exception as e: - logger.error(f"Error in iFlow pipe: {str(e)}", exc_info=True) - error_msg = f"iFlow Error: {str(e)}" - yield error_msg - await _emit_status(error_msg, done=True) - - return stream_generator()