Skip to content
18 changes: 18 additions & 0 deletions plugins/universal-chat-importer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

## [Unreleased]

### Added
- Support for importing multiple uploaded files in a single tool call.
- Enhanced logging for ZIP and JSON processing, visible in server logs.
- Added explicit trigger instructions to the tool docstring to improve LLM invocation reliability.

### Changed
- Refactored `import_chatgpt_export` to accept a list of file IDs (`file_ids: List[str]`).
- Improved progress reporting with cumulative summaries for multiple files.

### Removed
- Removed direct path support (`import_chatgpt_from_path`) and the associated `ALLOWED_IMPORT_DIRECTORIES` Valve for improved security and simplified workflow.

### Fixed
- Fixed "nothing happened" issue reported by users by making the tool docstring more aggressive and explicit about execution.
43 changes: 43 additions & 0 deletions plugins/universal-chat-importer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Universal Chat Importer

## Quick start

### Method: File Upload
1. **Upload** one or more ChatGPT export files (ZIP or `conversations.json`) to the chat.
2. **Ask** the AI to "import my ChatGPT history from the uploaded files."
3. **Wait** for the process to complete. You will see status updates as the chats are imported.

## Description

The Universal Chat Importer is an Open WebUI Tool that allows users to import chat history from external platforms directly into their Open WebUI instance using natural language.

### Key Features
- **Natural Language Tooling**: Trigger imports by simply asking the AI to process your uploaded export files.
- **Bulk Import**: Support for multiple file uploads in a single request.
- **Hierarchy Preservation**: Maps ChatGPT's complex message branching (DAG) into Open WebUI's chat tree.
- **Progress Reporting**: Real-time status updates via events and detailed logs during the database insertion process.
- **Non-Blocking Execution**: Large imports run in background threads to keep the UI responsive.

## Problem Statement

Migrating existing AI conversations to Open WebUI can be a manual and tedious process. This plugin provides a tool that the AI can use to automatically parse and insert chat history from external exports into your local database, preserving message structure and timestamps.

## Valves (Configuration)

| Valve | Default | Description |
|---|---|---|
| `ENABLE_UPDATE_NOTIFICATIONS` | `True` | If enabled, the plugin will check GitHub for new releases. |
| `CHECK_PREVIEW_RELEASES` | `False` | If enabled, check for preview/dev/pre-release versions. |
| `DEFAULT_MODEL` | `gpt-4o` | The model name to assign to imported chats. |

## Installation

### Via URL (Recommended)
1. Navigate to the [Releases](https://github.com/toolfab-ai/owui-plugins/releases) page.
2. Copy the URL of the `plugin.py` file from the latest release of this plugin.
3. In Open WebUI, navigate to **Admin Settings** -> **Functions** -> **+ New Function** -> **Install from URL**.
4. Paste the URL and click **Install**.

### Via Open WebUI Community
1. Search for **Universal Chat Importer** on the [Open WebUI Community](https://openwebui.com).
2. Click **Install** to import it directly into your instance.
27 changes: 27 additions & 0 deletions plugins/universal-chat-importer/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import sys
from pathlib import Path

# ruff: noqa: I001
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from scripts.build_lib import build


if __name__ == "__main__":
root = Path(__file__).parent
version = None
check_only = False
skip_tests = False
for arg in sys.argv[1:]:
if arg == "--check":
check_only = True
elif arg == "--skip-tests":
skip_tests = True
elif arg.startswith("--version="):
version = arg.split("=", 1)[1]
elif arg.startswith("--"):
print(f"Unknown flag: {arg}")
sys.exit(1)
else:
version = arg
build(root, version=version, check_only=check_only, skip_tests=skip_tests)
122 changes: 122 additions & 0 deletions plugins/universal-chat-importer/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
import asyncio
import logging
import os
import sys
from pathlib import Path

from tests.owui_client import OwuiClient

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("install")

# Configuration Constants
DEFAULT_OWUI_URL = "http://localhost:3000"
PLUGIN_FILE_NAME = "plugin.py"
TOOL_TYPE_KEYWORD = "class Tools"
PTYPE_TOOL = "tool"
PTYPE_FUNCTION = "function"
PUBLIC_ACCESS_GRANTS = [{"principal_type": "user", "principal_id": "*", "permission": "read"}]


def plugin_id_from_path(path: Path) -> str:
return path.parent.name.replace("-", "_").replace(".", "_").lower()


def plugin_name_from_meta(content: str) -> str:
for line in content.splitlines():
line = line.strip()
if line.lower().startswith("title:"):
return line.split(":", 1)[1].strip()
return "Unknown"


def get_plugin_type(content: str) -> str:
if TOOL_TYPE_KEYWORD in content:
return PTYPE_TOOL
return PTYPE_FUNCTION


async def install():
url = os.environ.get("OWUI_URL", DEFAULT_OWUI_URL)
token = os.environ.get("OWUI_TOKEN")

if not token:
logger.error("OWUI_TOKEN environment variable is required")
sys.exit(1)

plugin_dir = Path(__file__).parent
plugin_file = plugin_dir / PLUGIN_FILE_NAME

if not plugin_file.exists():
logger.error(f"{PLUGIN_FILE_NAME} not found in {plugin_dir}. Did you run build.py?")
sys.exit(1)

content = plugin_file.read_text(encoding="utf-8")
pid = plugin_id_from_path(plugin_file)
pname = plugin_name_from_meta(content)
ptype = get_plugin_type(content)

client = OwuiClient(url)
client.token = token

logger.info(f"Installing {ptype} {pname} ({pid})...")

try:
if ptype == PTYPE_TOOL:
try:
await client.create_tool(
tool_id=pid,
name=pname,
content=content,
description=f"Auto-installed from {plugin_dir.name}",
)
logger.info(f"Created tool: {pid}")
except RuntimeError as e:
if "409" in str(e) or "already registered" in str(e):
# We don't have a direct update_tool method in OwuiClient yet that takes content,
# but we can update access. For now, let's assume it's there or handle it.
logger.info(f"Tool {pid} already exists")
else:
raise e

# Make tool public
await client.update_tool_access(pid, PUBLIC_ACCESS_GRANTS)
logger.info(f"Shared tool: {pid}")

else:
try:
await client.create_function(
function_id=pid,
name=pname,
content=content,
description=f"Auto-installed from {plugin_dir.name}",
)
logger.info(f"Created function: {pid}")
except RuntimeError as e:
if "409" in str(e) or "already registered" in str(e):
await client.update_function(
function_id=pid,
name=pname,
content=content,
description=f"Auto-installed from {plugin_dir.name}",
)
logger.info(f"Updated function: {pid}")
else:
raise e

# Activate
try:
res = await client.toggle_function(pid)
if not res.get("is_active"):
await client.toggle_function(pid)
logger.info(f"Activated function: {pid}")
except RuntimeError:
logger.warning(f"Could not activate function: {pid}")

finally:
await client.close()


if __name__ == "__main__":
asyncio.run(install())
6 changes: 6 additions & 0 deletions plugins/universal-chat-importer/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[project]
name = "universal-chat-importer"
version = "0.1.0"
description = "Import ChatGPT exports into Open WebUI"
requires-python = ">=3.11"
dependencies = []
21 changes: 21 additions & 0 deletions plugins/universal-chat-importer/src/_updates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

import logging

from shared.update_notifier.notifier import UpdateMixin

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
logger = logging.getLogger(__name__)


# ========================================================================
# UPDATE CHECK MIXIN
# ========================================================================


class UpdateMixin(UpdateMixin):
"""Mixin for background checking of new releases on GitHub."""

_RELEASE_TAG_PREFIX: str = "universal-chat-importer/"
20 changes: 20 additions & 0 deletions plugins/universal-chat-importer/src/_valves.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from pydantic import BaseModel, Field


class Valves(BaseModel):
"""Admin-configurable settings for the Universal Chat Importer."""

ENABLE_UPDATE_NOTIFICATIONS: bool = Field(
default=True,
description="If enabled, the plugin will check GitHub in the background for new releases and notify the admin.",
)
CHECK_PREVIEW_RELEASES: bool = Field(
default=False,
description="If enabled, check for preview/dev/pre-release versions on GitHub as well.",
)
DEFAULT_MODEL: str = Field(
default="gpt-4o",
description="The model name to assign to imported chats if not specified.",
)
Loading
Loading