首页 教程 Web前端 Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

        感谢您点开这篇文章:D,鼠鼠我是一个代码小白,下文是学习开源项目Open WebUI过程中的一点笔记记录,希望能帮助到你~

        本人菜鸟,持续成长,能力不足有疏漏的地方欢迎一起探讨指正,比心心~

通过本文,您可以了解:

  • Open WebUI项目的基本信息和架构

  • 通过ollama部署大模型、通过docker镜像和源码运行Open WebUI项目的方法

  • 项目后端代码在多情景(普通提问、联网搜索提问、上传PDF文件且联网提问、上传PDF文件非联网提问)下的相关代码实现逻辑

  • RAG模块实现逻辑流程


目录

一、项目基本信息

二、运行项目源码

1、通过ollama部署大模型

1.1、安装ollma

1.2、配置ollama

1.3、下载模型

1.4、运行服务

命令行直接对话

REST API

2、搭建Open WebUI

2.1、通过docker部署

2.2、通过源码构建

​编辑

三、项目结构

1、backend目录(后端代码)

1.1、start.sh

1.2、data目录

1.3、open_webui目录

1.3.1、main.py

中间件(应用于FastAPI应用中)

Task Endpoints

Pipelines Endpoints

Config Endpoints

OAuth Login & Callback

1.3.2、apps目录

1.3.2.1、webui/main.py

1.3.2.2、webui/models(重点,数据库实体)

1.3.2.3、openai/main.py

1.3.2.4、openai/chat_interceptor

1.3.3、retrieval目录

main.py

utils.py

2、src目录(前端代码)

四、特定情景下代码链路逻辑

情景1:用户在界面发送消息时,代码调用逻辑:

情景2:用户进行联网搜索提问“武汉今天天气如何”时,代码调用逻辑:

情景3.1:用户上传PDF文件,让其帮忙总结(联网搜索功能关闭),代码逻辑:

情景3.2:用户上传PDF文件,让其帮忙总结,(联网搜索功能开启),代码逻辑:

五、总结



一、项目基本信息

  • Github:https://github.com/open-webui/open-webui

  • 官方文档:https://docs.openwebui.com/getting-started/

  • 代码版本:v0.3.32(2024.10.6)——本文学习版本,目前最新版本已更新至v0.3.35(截至2024.10.28)


二、运行项目源码

作者本地环境:Ubuntu24.04,纯CPU

通过ollama部署大模型qwen2:7b作为模型端,通过Open WebUI提供用户chat服务。

1、通过ollama部署大模型

ollama是大模型部署方案,对应docker,本质也是基于docker的容器化技术

1.1、安装ollma

官方地址:https://ollama.com/

开源地址:https://github.com/ollama/ollama

打开官网,点击Downloard,根据操作系统选择对应下载方式。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

以Ubuntu24.04为例,通过下述命令下载:

curl -fsSL https://ollama.com/install.sh | sh #下载完成后查询版本信息 ollama -v #查看状态

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

如上,ollama已经成功安装。

1.2、配置ollama

通过编辑ollama.service进行配置:

sudo vim /etc/systemd/system/ollama.service

  • 更改HOST

由于Ollama的默认参数配置,启动时设置了仅本地访问,因此需要对HOST进行配置,开启监听任何来源IP。

[Service] # 配置远程访问 Environment="OLLAMA_HOST=0.0.0.0"

  • 更改模型存储路径

默认情况下,不同操作系统大模型存储的路径如下:

macOS: ~/.ollama/models Linux: /usr/share/ollama/.ollama/models Windows: C:\Users.ollama\models

如果要修改模型文件的存储路径,设置如下:

[Service] # 配置OLLAMA的模型存放路径 Environment="OLLAMA_MODELS=/data/ollama/models"

如果因为指定的目录ollama用户及用户组没有相应权限,导致服务不能启动。可以通过授权给相应的目录权限解决问题:

chown ollama:ollama ollama/models

  • 应用配置

重载systemd并重启Ollama

systemctl daemon-reload systemctl restart ollama

配置完成后,访问测试。浏览器访问http://IP:11434/,出现Ollama is running代表成功。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

1.3、下载模型

ollama的命令和docker操作命令非常相似。可通过shell窗口输入ollama查看相关命令:

******:~/work# ollama Usage: ollama [flags] ollama [command] Available Commands: serve Start ollama create Create a model from a Modelfile show Show information for a model run Run a model pull Pull a model from a registry push Push a model to a registry list List models ps List running models cp Copy a model rm Remove a model help Help about any command Flags: -h, --help help for ollama -v, --version Show version information Use "ollama [command] --help" for more information about a command.

由上可知ollama相关命令:

ollama serve # 启动ollama ollama create # 从模型文件创建模型 ollama show # 显示模型信息 ollama run # 运行模型 ollama pull # 从注册仓库中拉取模型 ollama push # 将模型推送到注册仓库 ollama list # 列出已下载模型 ollama cp # 复制模型 ollama rm # 删除模型 ollama help # 获取有关任何命令的帮助信息

  • 拉取qwen2-7b模型

ollama pull qwen2:7b #下载成功查看模型 ollama list

可见,已成功拉取:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

也可以自定义模型,所谓自定义模型就是不适用Ollama官方模型库中的模型,理论可以使用其他各类经过转换处理的模型,有从GGUF导入和从PyTorch或Safetensors导入两种方式。

所谓从从PyTorch或Safetensors导入Ollama,其实就是使用llama.cpp项目,对PyTorch或Safetensors类型的模型进行转换、量化处理成GGUF格式的模型,然后再用Ollama加载使用 。

参考:Ollama:一个在本地部署、运行大型语言模型的工具-CSDN博客

  • 运行模型

运行模型并进行对话:

ollama run qwen2:7b

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

1.4、运行服务

命令行直接对话

如上,运行模型可以直接与模型进行对话。

REST API

运行模型后,执行ollama serve命令启动Ollama服务,然后就可以通过API形式进行模型调用。ollama serve会自动启动一个http服务,可以通过http请求模型服务。

参考官方API文档:https://github.com/ollama/ollama/blob/main/docs/api.md

生成回复

curl http://localhost:11434/api/generate -d '{ "model": "qwen2:7b", "prompt":"你是谁?为什么天空是蓝色的?" }'

上述localhost也可以换成ip。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

若要禁用流式,如下操作:

curl http://ip:11434/api/generate -d '{ "model": "qwen2:7b", "prompt":"你是谁?为什么天空是蓝色的?", "stream":false }'

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

与模型聊天

curl http://localhost:11434/api/chat -d '{ "model": "qwen2:7b", "messages": [ { "role": "user", "content": "天空为什么是蓝色的?" } ] }'

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

也可以带历史记录

curl http://localhost:11434/api/chat -d '{ "model": "qwen2:7b", "messages": [ { "role": "user", "content": "why is the sky blue?" }, { "role": "assistant", "content": "due to rayleigh scattering." }, { "role": "user", "content": "how is that different than mie scattering?" } ] }'

2、搭建Open WebUI

Open WebUI 是一个可扩展、功能丰富且用户友好的自托管 WebUI,旨在完全离线操作。它支持各种 LLM 运行程序,包括 Ollama 和 OpenAI 兼容的 API。

  • Github:https://github.com/open-webui/open-webui

  • Open WebUI:https://docs.openwebui.com/

  • 社区:https://openwebui.com/

2.1、通过docker部署

使用Docker部署安装Open WebUI。计算机已有ollama,使用以下命令:

docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

访问http://IP:3000,创建一个账号(管理员)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

登陆账号:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

进入Open WebUI后,界面如下。在Settings中进行相关设置

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

管理员设置,设置外部连接:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

设置连接后,在选择模型部分可见上面下载下来的千问模型:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

选择模型即可进行对话:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

2.2、通过源码构建

也可以通过本地运行项目源码进行搭建。(以Linux为例)

# Copying required .env file cp -RPp .env.example .env # Building Frontend Using Node npm install npm run build cd ./backend # Optional: To install using Conda as your development environment, follow these instructions: # Create and activate a Conda environment conda create --name open-webui-env python=3.11 conda activate open-webui-env # Install dependencies pip install -r requirements.txt -U # Start the application bash start.sh

在鼠鼠我多次构建的过程中,有次有遇到一个错误,报错如下:

(venv) ******:~/PycharmProjects/openwebui(v0.3.32)/open-webui$ npm run build > open-webui@0.3.32 build > npm run pyodide:fetch && vite build > open-webui@0.3.32 pyodide:fetch > node scripts/prepare-pyodide.js Setting up pyodide + micropip Failed to load Pyodide: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/***/PycharmProjects/openwebuiv0.3.32/open-webui/node_modules/pyodide/pyodide.asm.js' imported from /home/***/PycharmProjects/openwebui(v0.3.32)/open-webui/node_modules/pyodide/pyodide.mjs at new NodeError (node:internal/errors:405:5) at finalizeResolution (node:internal/modules/esm/resolve:327:11) at moduleResolve (node:internal/modules/esm/resolve:980:10) at defaultResolve (node:internal/modules/esm/resolve:1193:11) at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:403:12) at ModuleLoader.resolve (node:internal/modules/esm/loader:372:25) at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:249:38) at ModuleLoader.import (node:internal/modules/esm/loader:335:34) at importModuleDynamically (node:internal/modules/esm/translators:143:35) at importModuleDynamicallyCallback (node:internal/modules/esm/utils:112:14) { url: 'file:///home/***/PycharmProjects/openwebuiv0.3.32/open-webui/node_modules/pyodide/pyodide.asm.js', code: 'ERR_MODULE_NOT_FOUND' } Copying Pyodide files into static directory node:internal/process/promises:288 triggerUncaughtException(err, true /* fromPromise */); ^ [Error: ENOENT: no such file or directory, open '/home/***/PycharmProjects/openwebuiv0.3.32/open-webui/node_modules/pyodide/python_stdlib.zip'] { errno: -2, code: 'ENOENT', syscall: 'open', path: '/home/***/PycharmProjects/openwebuiv0.3.32/open-webui/node_modules/pyodide/python_stdlib.zip' } Node.js v18.19.1

原因:涉及两方面,一是node.js 版本问题,如下图(官方最新文档要求),目前版本低于要求版本。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

另一个方面——文件夹命名问题。原来的项目处于的一个文件夹为“openwebui(v0.3.32)”,其中包含括号和".",在后续代码执行中,由于路径中包含特殊字符(在这个案例中是括号 ()),导致命令解释错误或者文件系统路径解析出现问题。在文件或目录名称中使用特殊字符,如括号、星号、问号、波浪线等,经常会导致这类问题,因为这些字符在 Unix 和 Linux 命令行中可能有特殊含义。

解决方法:将项目所处的目录进行重命名为“openwebui_v0_3_32”,即可解决。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Q:点击“+”功能只有“上传文件”,没有“联网搜索”,如何解决?

A:需要管理员在面板中进行设置搜索引擎,本质上是通过api调用:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)


三、项目结构

整个代码语言构成分布如下,其中,Svelte 是一种现代的前端框架,用于构建高性能的Web应用程序。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

项目文件如下所示,主要分为前端、后端、测试和部署脚本:

  • backend目录:后端代码目录,包含API服务、数据库操作等

  • cypress 目录:包含Cypress测试框架的配置和测试脚本,用于端到端测试

  • docs目录:文档目录,包含项目说明、安全指南等。

  • kubernetes : 包含Kubernetes部署配置文件。

  • scripts : 包含各种脚本文件,用于自动化部署、测试或其他任务的脚本。

  • src :前端代码目录,存放Svelte组件和相关资源的地方。

  • static : 静态文件目录,如图片、CSS、客户端JavaScript等。

  • test/test_files/image_gen : 测试目录下的子目录,包含用于测试的图像生成器。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

1、backend目录(后端代码)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

  • data文件夹:用于存储后端服务需要的数据文件,如数据库、文档等

  • open-webui文件夹:包含后端服务的主要代码和配置文件

  • dev.sh:用于本地开发环境的启动脚本

  • start.shstart_windows.bat - 用于启动后端服务的脚本,分别适用于类Unix系统和Windows系统。

1.1、start.sh

启动脚本,最后会启动一个 Uvicorn 服务器,并通过这个命令来运行 open-webui/backend/open_webui/main.py 文件中的FastAPI的 app 应用对象,监听在指定的主机和端口上,并允许所有的转发 IP 地址。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

1.2、data目录

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

  • cache - 用于存储应用程序的缓存数据。

  • functions - 包含一些后端服务使用的函数或脚本。

  • tools - 包含一些用于后端服务的工具或脚本。

  • uploads - 用于存储用户上传的文件。

  • vector_db - 用于存储向量数据库或类似的数据结构。

  • readme.txt - 包含文件夹的说明或使用指南。

  • webui.db - 后端服务使用的数据库文件。

1.3、open_webui目录

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

  • apps - 包含后端服务的应用程序逻辑。

  • data - 与主data文件夹类似,用于存储后端服务需要的数据文件。

  • migrations - 包含数据库迁移脚本,用于数据库结构的版本控制。

  • static - 包含静态文件,如图片、CSS、JavaScript等。

  • test - 包含测试代码和测试用例。

  • utils - 包含一些后端服务使用的实用工具或函数。

  • init.py - Python模块初始化文件。

  • alembic.ini - Alembic数据库迁移工具的配置文件。

  • config.py - 后端服务的配置文件。

  • constants.py - 包含后端服务使用的常量。

  • env.py - 包含环境变量的配置。

  • main.py - 是后端服务的入口点或主程序。

1.3.1、main.py
中间件(应用于FastAPI应用中)
  • ChatCompletionMiddleware类 :用于处理与聊天补全相关的请求,包括模型选择、过滤函数、工具函数调用和文件处理。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

  • PipelineMiddleware类:对请求进行预处理和后处理。处理管道中的过滤器调用

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

  • 也添加中间件CORSMiddleware、SecurityHeadersMiddleware 以及 PipelineMiddleware 本身,分别负责处理跨域资源共享(CORS)、安全头部设置以及自定义的业务逻辑处理。

设置相关路由

  • @app.get("/api/models") : 用于获取模型的列表

  • @app.post("/api/chat/completed"):完整的聊天补全请求处理流程,包括模型验证、外部API调用、事件处理、全局和本地过滤器调用。

  • @app.post("/api/chat/actions/{action_id}"):用于处理特定动作的请求。它通过执行与动作ID关联的功能来响应聊天中的动作请求

Task Endpoints

路由

作用

GET请求端点/api/task/config

它返回当前应用的状态配置信息

POST请求端点/api/task/config/update

用于更新任务配置。只有管理员用户可以访问此端点,并且需要提供一个符合TaskConfigForm模型的JSON数据体来进行更新操作。

POST请求端点/api/task/title/completions

用于根据给定的提示生成标题

POST请求端点/api/task/query/completions

用于根据用户的对话历史生成搜索查询

POST请求端点/api/task/emoji/completions

用于根据文本内容生成相应的表情符号

POST请求端点/api/task/moa/completions

用于综合多个模型的响应生成最终答案

Pipelines Endpoints

@app.get("/api/pipelines/list"):通过调用get_openai_models函数获取模型列表,然后筛选出包含“pipelines”字段的响应,并返回相应的API URL和索引

Config Endpoints

@app.post("/api/pipelines/upload"):允许用户上传Python脚本文件作为管道。

接下来的几个段落分别定义了添加、删除管道以及获取管道详情等的端点;

并且定义了一系列API端点,用于管理和获取应用程序的配置信息。

OAuth Login & Callback

实现完整的OAuth登录和注册流程,包括客户端注册、会话管理、用户认证和JWT令牌生成等功能。

1.3.2、apps目录

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

1.3.2.1、webui/main.py

注册了多个路由处理器,处理不同类型api请求,如用户认证、文件上传、模型管理等。

定义相关核心函数:

  • get_status:根路由处理函数,返回应用的状态信息。

  • get_function_module:根据管道ID加载函数模块。

  • get_pipe_models:获取管道模型的详细信息。

  • execute_pipe:执行管道函数。

  • get_message_content:从不同的响应类型中获取消息内容。

  • process_line:处理聊天消息的每一行。

  • get_pipe_id:从表单数据中获取管道ID。

  • get_function_params:获取函数参数。

最后定义函数generate_function_chat_completion,实现聊天补全处理相关逻辑。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

1.3.2.2、webui/models(重点,数据库实体)

(未完待续。。。。。。待整理)

1.3.2.3、openai/main.py
  • 设置FastAPI应用、Middleware和依赖注入(中间件会在每次请求前执行,确保在访问模型端点之前已经加载了模型数据)。

  • 设置api路由

    • /config:提供了一个GET方法来返回当前的应用程序配置,包括是否启用OpenAI API的功能。

    • /config/update:接受一个POST请求,更新应用程序的配置,特别是启用或禁用OpenAI API的功能。

    • /urls和/keys:分别提供了GET方法来显示当前的OpenAI API URLs和Keys列表,以及POST方法来更新这些列表。

    • /audio/speech:这是一个音频处理的端点,接受用户的语音输入并生成对应的音频文件响应。

  • 设置异步函数,例如fetch_url, cleanup_response, merge_models_lists, get_all_models_raw, get_all_models等。这些函数主要负责与外部API通信、处理JSON数据、合并模型列表等工作。

1.3.2.4、openai/chat_interceptor

实现一个简单的聊天系统拦截器,可以用于检查和处理特定的情况,例如不支持的URL或过长的上下文文本。

模块

实现

解析用户输入

get_message_text函数:从用户输入中提取文本内容

生成聊天响应

generate_chat_response函数:生成聊天响应,包括生成一个唯一的ID、创建时间、模型名称、选择内容和使用情况

拦截器列表

包含了一系列的拦截器实例

chat_interceptor_before_lark_doc_content和

chat_interceptor_after_lark_doc_content:

分别在处理飞书文档内容之前和之后使用的拦截器列表。

拦截器入口

遍历拦截器列表,并调用每个拦截器的 intercept 方法

intercept_chat_completion_before_lark_doc_content和intercept_chat_completion_after_lark_doc_content:

分别是在处理飞书文档内容之前和之后调用的拦截器入口函数。

拦截器类型

  • UnsupportedUrlChatCompletionInterceptor:

检查用户输入中是否包含不支持的URL,如果是,则返回默认回答。

  • LongContextTextChatCompletionInterceptor:

      检查用户输入的文本是否过长,如果是,则返回默认回答。

拦截器调用

在发送聊天请求之前或之后,调用拦截器列表中的拦截器,每个拦截器都会检查请求,并决定是否拦截请求。

1.3.3、retrieval目录

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

  • loaders:从各种来源加载和处理文档内容,适用于需要跨多种文件格式工作的应用场景。

  • models:定义了用于检索任务的模型

  • vector:

    • 包含与向量相关的文件,如dbsconnector.py,用于处理向量数据库的连接和交互,以及向量化文本数据以用于相似性搜索。

    • main.py文件可能包含与向量检索相关的主要逻辑。

  • web:

    • 包含多个与Web相关的Python文件,如brave.pyduckduckgo.py等,这些文件用于实现与不同搜索引擎(如Brave Search、DuckDuckGo)的交互,以便从这些搜索引擎获取数据。

    • main.pyutils.py文件可能包含Web应用的主要逻辑和辅助功能。

    • testdata目录可能包含用于测试的示例数据。

  • utils.py:一个通用的工具文件,包含在整个应用中使用的辅助函数和类。

  • main.py:后端服务入口点,主要用于处理文档检索和向量数据库操作。

(下面的内容是旧版本v0.3.21中rag目录,即对应v0.3.32中retrieval目录,两版本肯定有差异,下面是之前学习旧版本的笔记,仅供参考)

main.py

配置和模型更新

  • 定义 update_embedding_modelupdate_reranking_model 函数来更新嵌入和重排模型。

  • 使用 get_embedding_function 获取嵌入函数,用于将文本转换为向量表示

API 路由和处理函数

  • 定义了多个 API 路由和处理函数,例如:

    • /:根路由,返回应用状态。

    • /embedding:返回嵌入模型的配置。

    • /reranking:返回重排模型的配置。

    • /embedding/update/reranking/update:更新嵌入和重排模型的配置。

    • /config:返回 RAG 应用的配置。

    • /config/update:更新 RAG 应用的配置。

    • /template/query/settings:获取和更新查询模板和设置。

    • /query/doc/query/collection:处理文档和集合的查询请求。

    • /youtube/web:处理 YouTube 视频和网页内容的存储请求。

    • /web/search:处理网页搜索请求。

文档和网页处理

  • 定义了 get_loader 函数,根据文件类型选择适当的加载器(如 TikaLoader、TextLoader 等)。

  • 定义了 store_data_in_vector_dbstore_text_in_vector_db 函数,用于将数据存储到向量数据库中。

错误处理

  • 使用 HTTPException 处理错误情况,并返回错误信息。

辅助函数

  • 定义 get_web_loadervalidate_urlresolve_hostname 等辅助函数,用于加载和验证网页内容。

搜索功能

  • 定义 search_web 函数,用于通过不同的搜索引擎进行搜索。

安全加载器

  • 定义 SafeWebBaseLoader 类,用于增强错误处理,确保即使某些 URL 无法访问,系统仍然可以正常工作。

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

rag模块的作用流程:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

utils.py

处理检索增强生成(RAG)任务的函数,主要涉及从不同数据源中提取和查询信息

  • query_doc 函数:用于从一个指定的集合中查询与给定查询最相关的文档。

  • query_doc_with_hybrid_search 函数:扩展了基本的查询功能,引入了混合搜索的概念。它不仅使用BM25Retriever进行初步筛选,还结合了ChromaRetriever进行更精确的搜索,并通过EnsembleRetriever组合两者的结果。此外,它还包括一个重排序步骤,通过RerankCompressor对结果进行进一步优化。

  • merge_and_sort_query_results 函数:用于合并多个查询结果,并对它们按相关性进行排序。它会将所有结果的距离、文档和元数据合并在一起,然后根据距离进行降序或升序排列,最后只保留前K个结果。

  • query_collection 和 query_collection_with_hybrid_search 函数:这两个函数分别实现了基于普通搜索和混合搜索的多集合查询。它们遍历一组集合名称,对每个集合执行相应的查询操作,并将结果合并和排序后返回。

  • rag_template 函数:用于替换模板字符串中的占位符,以便在生成的上下文中插入具体的查询和上下文内容。

  • get_embedding_function 函数:根据不同的嵌入引擎和模型生成对应的嵌入函数。

  • get_rag_context 函数:从文件列表和消息记录中提取与当前查询最相关的上下文。

  • get_model_path 函数:用于确定Hugging Face模型的本地路径。

  • generate_openai_embeddings 和 generate_openai_batch_embeddings 函数:用于调用OpenAI API生成文本的嵌入向量。前者处理单个文本输入,后者则可以处理一批文本输入,适用于批量处理的场景。

  • ChromaRetriever 类和 RerankCompressor 类:分别是LangChain库中原有的Retriever和DocumentCompressor的具体实现。ChromaRetriever负责从Chroma数据库中检索文档,而RerankCompressor则在检索到的文档基础上进行进一步的重排序。

。。。。。。(未完待续,鼠鼠后面有空会继续更新的惹)

2、src目录(前端代码)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

  • lib:包含可重用的JavaScript或Svelte组件、工具函数、实用程序等

  • routes:包含Svelte路由文件,用于定义应用程序的页面路由。

  • app.css:包含全局样式表,定义了样式重置、通用样式或主题。

  • app.d.ts:TypeScript的声明文件,用于为项目提供类型定义。

  • app.html:项目的HTML模板文件,通常是应用程序的入口点。

  • tailwind.css:使用Tailwind CSS时的全局样式文件。

(由于前端不是鼠鼠我学习的重点,所以没在看前端部分了)

四、特定情景下代码链路逻辑

情景1:用户在界面发送消息时,代码调用逻辑:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

核心部分:

1、构建prompt和调用大模型:

""" [open_webui.apps.ollama.main] """ @app.post("/api/chat/{url_idx}") async def generate_chat_completion( form_data: GenerateChatCompletionForm, url_idx: Optional[int] = None, user=Depends(get_verified_user), ): log.info(f"/api/chat或/api/chat/{url_idx}") payload = {**form_data.model_dump(exclude_none=True)} log.debug(f"{payload = }") if "metadata" in payload: del payload["metadata"] model_id = form_data.model if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST: raise HTTPException( status_code=403, detail="Model not found", ) model_info = Models.get_model_by_id(model_id) if model_info: if model_info.base_model_id: payload["model"] = model_info.base_model_id params = model_info.params.model_dump() if params: if payload.get("options") is None: payload["options"] = {} payload["options"] = apply_model_params_to_body_ollama( params, payload["options"] ) #构建prompt payload = apply_model_system_prompt_to_body(params, payload, user) if ":" not in payload["model"]: payload["model"] = f"{payload['model']}:latest" url = get_ollama_url(url_idx, payload["model"]) log.info(f"url: {url}") log.debug(payload) #调用大模型 return await post_streaming_url( f"{url}/api/chat", json.dumps(payload), stream=form_data.stream, content_type="application/x-ndjson", )

构建prompt的apply_model_system_prompt_to_body函数细节:

""" backend/open_webui/utils/payload.py """ # inplace function: form_data is modified def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict: system = params.get("system", None) if not system: return form_data if user: template_params = { "user_name": user.name, "user_location": user.info.get("location") if user.info else None, } else: template_params = {} system = prompt_template(system, **template_params) form_data["messages"] = add_or_update_system_message( system, form_data.get("messages", []) ) return form_data

调用大模型的post_streaming_url函数细节:

""" backend/open_webui/apps/ollama/main.py """ async def post_streaming_url( url: str, payload: Union[str, bytes], stream: bool = True, content_type=None ): log.info("post_streaming_url") r = None try: session = aiohttp.ClientSession( trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) ) r = await session.post( url, data=payload, headers={"Content-Type": "application/json"}, ) r.raise_for_status() if stream: headers = dict(r.headers) if content_type: headers["Content-Type"] = content_type return StreamingResponse( r.content, status_code=r.status, headers=headers, background=BackgroundTask( cleanup_response, response=r, session=session ), ) else: res = await r.json() await cleanup_response(r, session) return res except Exception as e: error_detail = "Open WebUI: Server Connection Error" if r is not None: try: res = await r.json() if "error" in res: error_detail = f"Ollama: {res['error']}" except Exception: error_detail = f"Ollama: {e}" raise HTTPException( status_code=r.status if r else 500, detail=error_detail, )

情景2:用户进行联网搜索提问“武汉今天天气如何”时,代码调用逻辑:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

核心代码:

1、生成搜索查询:

""" backend/open_webui/main.py """ @app.post("/api/task/query/completions") async def generate_search_query(form_data: dict, user=Depends(get_verified_user)): log.info("/api/task/query/completions") print("generate_search_query") if not app.state.config.ENABLE_SEARCH_QUERY: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Search query generation is disabled", ) model_id = form_data["model"] if model_id not in app.state.MODELS: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Model not found", ) # Check if the user has a custom task model # If the user has a custom task model, use that model task_model_id = get_task_model_id(model_id) print(task_model_id) model = app.state.MODELS[task_model_id] if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "": template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE else: template = """Given the user's message and interaction history, decide if a web search is necessary. You must be concise and exclusively provide a search query if one is necessary. Refrain from verbose responses or any additional commentary. Prefer suggesting a search if uncertain to provide comprehensive or updated information. If a search isn't needed at all, respond with an empty string. Default to a search query when in doubt. Today's date is {{CURRENT_DATE}}. User Message: {{prompt:end:4000}} Interaction History: {{MESSAGES:END:6}} Search Query:""" content = search_query_generation_template( template, form_data["messages"], {"name": user.name} ) print("content", content) payload = { "model": task_model_id, "messages": [{"role": "user", "content": content}], "stream": False, **( {"max_tokens": 30} if app.state.MODELS[task_model_id]["owned_by"] == "ollama" else { "max_completion_tokens": 30, } ), "metadata": {"task": str(TASKS.QUERY_GENERATION), "task_body": form_data}, } log.debug(payload) # Handle pipeline filters try: payload = filter_pipeline(payload, user) except Exception as e: if len(e.args) > 1: return JSONResponse( status_code=e.args[0], content={"detail": e.args[1]}, ) else: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={"detail": str(e)}, ) if "chat_id" in payload: del payload["chat_id"] return await generate_chat_completions(form_data=payload, user=user)

2、执行搜索查询

""" backend/open_webui/apps/retrieval/main.py """ @app.post("/process/web/search") def process_web_search(form_data: SearchForm, user=Depends(get_verified_user)): log.info("调用函数process_web_search") try: logging.info( f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}" ) web_results = search_web( app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query ) except Exception as e: log.exception(e) print(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e), ) try: collection_name = form_data.collection_name if collection_name == "": collection_name = calculate_sha256_string(form_data.query)[:63] urls = [result.link for result in web_results] loader = get_web_loader(urls) docs = loader.load() save_docs_to_vector_db(docs, collection_name, overwrite=True) return { "status": True, "collection_name": collection_name, "filenames": urls, } except Exception as e: log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), )

情景3.1:用户上传PDF文件,让其帮忙总结(联网搜索功能关闭),代码逻辑:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

核心代码:

1、接收处理保存用户上传的PDF文件

""" backend/open_webui/apps/webui/routers/files.py """ @router.post("/") def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)): log.info("调用函数:upload_file") log.info(f"file.content_type: {file.content_type}") try: unsanitized_filename = file.filename filename = os.path.basename(unsanitized_filename) # replace filename with uuid id = str(uuid.uuid4()) name = filename filename = f"{id}_{filename}" file_path = f"{UPLOAD_DIR}/{filename}" contents = file.file.read() with open(file_path, "wb") as f: f.write(contents) f.close() file = Files.insert_new_file( user.id, FileForm( **{ "id": id, "filename": filename, "meta": { "name": name, "content_type": file.content_type, "size": len(contents), "path": file_path, }, } ), ) try: process_file(ProcessFileForm(file_id=id)) file = Files.get_file_by_id(id=id) except Exception as e: log.exception(e) log.error(f"Error processing file: {file.id}") if file: return file else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT("Error uploading file"), ) except Exception as e: log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), )

2、上述调用的处理文件的函数

""" backend/open_webui/apps/retrieval/main.py """ @app.post("/process/file") def process_file( form_data: ProcessFileForm, user=Depends(get_verified_user), ): log.info("调用函数:process_file") try: file = Files.get_file_by_id(form_data.file_id) collection_name = form_data.collection_name if collection_name is None: collection_name = f"file-{file.id}" if form_data.content: # Update the content in the file # Usage: /files/{file_id}/data/content/update VECTOR_DB_CLIENT.delete( collection_name=f"file-{file.id}", filter={"file_id": file.id}, ) docs = [ Document( page_content=form_data.content, metadata={ "name": file.meta.get("name", file.filename), "created_by": file.user_id, "file_id": file.id, **file.meta, }, ) ] text_content = form_data.content elif form_data.collection_name: # Check if the file has already been processed and save the content # Usage: /knowledge/{id}/file/add, /knowledge/{id}/file/update result = VECTOR_DB_CLIENT.query( collection_name=f"file-{file.id}", filter={"file_id": file.id} ) if len(result.ids[0]) > 0: docs = [ Document( page_content=result.documents[0][idx], metadata=result.metadatas[0][idx], ) for idx, id in enumerate(result.ids[0]) ] else: docs = [ Document( page_content=file.data.get("content", ""), metadata={ "name": file.meta.get("name", file.filename), "created_by": file.user_id, "file_id": file.id, **file.meta, }, ) ] text_content = file.data.get("content", "") else: # Process the file and save the content # Usage: /files/ file_path = file.meta.get("path", None) if file_path: loader = Loader( engine=app.state.config.CONTENT_EXTRACTION_ENGINE, TIKA_SERVER_URL=app.state.config.TIKA_SERVER_URL, PDF_EXTRACT_IMAGES=app.state.config.PDF_EXTRACT_IMAGES, ) docs = loader.load( file.filename, file.meta.get("content_type"), file_path ) else: docs = [ Document( page_content=file.data.get("content", ""), metadata={ "name": file.filename, "created_by": file.user_id, "file_id": file.id, **file.meta, }, ) ] text_content = " ".join([doc.page_content for doc in docs]) log.debug(f"text_content: {text_content}") Files.update_file_data_by_id( file.id, {"content": text_content}, ) hash = calculate_sha256_string(text_content) Files.update_file_hash_by_id(file.id, hash) try: result = save_docs_to_vector_db( docs=docs, collection_name=collection_name, metadata={ "file_id": file.id, "name": file.meta.get("name", file.filename), "hash": hash, }, add=(True if form_data.collection_name else False), ) if result: Files.update_file_metadata_by_id( file.id, { "collection_name": collection_name, }, ) return { "status": True, "collection_name": collection_name, "filename": file.meta.get("name", file.filename), "content": text_content, } except Exception as e: raise e except Exception as e: log.exception(e) if "No pandoc was found" in str(e): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED, ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), )

情景3.2:用户上传PDF文件,让其帮忙总结,(联网搜索功能开启),代码逻辑:

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)


五、总结

开源项目目前还在不断更新迭代,相信后面会有更好的功能体验。本人水平有限,有错轻喷谢谢~

评论(0)条

提示:请勿发布广告垃圾评论,否则封号处理!!

    猜你喜欢
    【MySQL】用户管理

    【MySQL】用户管理

     服务器/数据库  4个月前  3.7k

    我们推荐使用普通用户对数据的访问。而root作为管理员可以对普通用户对应的权限进行设置和管理。如给张三和李四这样的普通用户权限设定后。就只能操作给你权限的库了。

    Cursor Rules 让开发效率变成10倍速

    Cursor Rules 让开发效率变成10倍速

     服务器/数据库  4个月前  2.16k

    在AI与编程的交汇点上,awesome-cursorrules项目犹如一座灯塔,指引着开发者们驶向更高效、更智能的编程未来。无论你是经验丰富的老手,还是刚入行的新人,这个项目都能为你的编程之旅增添一抹亮色。这些规则文件就像是你私人定制的AI助手,能够根据你的项目需求和个人偏好,精确地调教AI的行为。突然间,你会发现AI不仅能理解Next.js的最佳实践,还能自动应用TypeScript的类型检查,甚至主动提供Tailwind CSS的类名建议。探索新的应用场景,推动AI辅助编程的边界。

    探索Django 5: 从零开始,打造你的第一个Web应用

    探索Django 5: 从零开始,打造你的第一个Web应用

     服务器/数据库  4个月前  1.97k

    Django 是一个开放源代码的 Web 应用程序框架,由 Python 写成。它遵循 MVT(Model-View-Template)的设计模式,旨在帮助开发者高效地构建复杂且功能丰富的 Web 应用程序。随着每个版本的升级,Django 不断演变,提供更多功能和改进,让开发变得更加便捷。《Django 5 Web应用开发实战》集Django架站基础、项目实践、开发经验于一体,是一本从零基础到精通Django Web企业级开发技术的实战指南《Django 5 Web应用开发实战》内容以。

    MySQL 的mysql_secure_installation安全脚本执行过程介绍

    MySQL 的mysql_secure_installation安全脚本执行过程介绍

     服务器/数据库  4个月前  1.96k

    mysql_secure_installation 是 MySQL 提供的一个安全脚本,用于提高数据库服务器的安全性

    【MySQL基础篇】概述及SQL指令:DDL及DML

    【MySQL基础篇】概述及SQL指令:DDL及DML

     服务器/数据库  4个月前  778

    数据库是长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。数据库不仅仅是数据的简单堆积,而是遵循一定的规则和模式进行组织和管理的。数据库中的数据可以包括文本、数字、图像、音频等各种类型的信息。

    Redis中的哨兵(Sentinel)

    Redis中的哨兵(Sentinel)

     服务器/数据库  4个月前  649

    ​ 上篇文章我们讲述了Redis中的主从复制(Redis分布式系统中的主从复制-CSDN博客),本篇文章针对主从复制中的问题引出Redis中的哨兵,希望本篇文章会对你有所帮助。