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](#REST API)

[2、搭建Open WebUI](#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](#Task Endpoints)

[Pipelines Endpoints](#Pipelines Endpoints)

[Config Endpoints](#Config Endpoints)

[OAuth Login & Callback](#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文件,让其帮忙总结,(联网搜索功能开启),代码逻辑:

五、总结



一、项目基本信息


二、运行项目源码

**作者本地环境:**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,根据操作系统选择对应下载方式。

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

复制代码
curl -fsSL https://ollama.com/install.sh | sh

#下载完成后查询版本信息
ollama -v

#查看状态

如上,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.ollamamodels

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

复制代码
[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代表成功。

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

可见,已成功拉取:

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

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

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

  • 运行模型

运行模型并进行对话:

复制代码
ollama run qwen2:7b
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。

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

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

与模型聊天

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

也可以带历史记录

复制代码
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。

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

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

登陆账号:

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

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

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

选择模型即可进行对话:

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 版本问题 ,如下图(官方最新文档要求),目前版本低于要求版本。

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

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

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

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


三、项目结构

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

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

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

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

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

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

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

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

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

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

1、backend目录(后端代码)

  • 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 地址。

1.2、data目录
  • cache - 用于存储应用程序的缓存数据。

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

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

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

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

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

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

1.3、open_webui目录
  • 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类 :**用于处理与聊天补全相关的请求,包括模型选择、过滤函数、工具函数调用和文件处理。
  • PipelineMiddleware类:对请求进行预处理和后处理。处理管道中的过滤器调用**。**
  • 也添加中间件**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目录
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,实现聊天补全处理相关逻辑。

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目录
  • **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 无法访问,系统仍然可以正常工作。

rag模块的作用流程:

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****目录(前端代码)

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

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

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

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

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

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

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

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

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

核心部分:

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:用户进行联网搜索提问"武汉今天天气如何"时,代码调用逻辑:

核心代码:

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文件,让其帮忙总结(联网搜索功能关闭),代码逻辑:

核心代码:

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文件,让其帮忙总结 **,(联网搜索功能开启 ),**代码逻辑:


五、总结

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

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习