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

打印 上一主题 下一主题

主题 889|帖子 889|积分 2667

        感谢您点开这篇文章,鼠鼠我是一个代码小白,下文是学习开源项目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,根据操作系统选择对应下载方式。

以Ubuntu24.04为例,通过下述命令下载:
  1. curl -fsSL https://ollama.com/install.sh | sh
  2. #下载完成后查询版本信息
  3. ollama -v
  4. #查看状态
复制代码


如上,ollama已经成功安装。
1.2、设置ollama

通过编辑ollama.service举行设置:
  1. sudo vim /etc/systemd/system/ollama.service
复制代码


  • 更改HOST
由于Ollama的默认参数设置,启动时设置了仅当地访问,因此须要对HOST举行设置,开启监听任何泉源IP。
  1. [Service]
  2. # 配置远程访问
  3. Environment="OLLAMA_HOST=0.0.0.0"
复制代码


  • 更改模型存储路径
默认环境下,不同操作系统大模型存储的路径如下:
  1. macOS: ~/.ollama/models
  2. Linux: /usr/share/ollama/.ollama/models
  3. Windows: C:\Users.ollama\models
复制代码
假如要修改模型文件的存储路径,设置如下:
  1. [Service]
  2. # 配置OLLAMA的模型存放路径
  3. Environment="OLLAMA_MODELS=/data/ollama/models"
复制代码
假如因为指定的目录ollama用户及用户组没有相应权限,导致服务不能启动。可以通过授权给相应的目录权限办理题目:
  1. chown ollama:ollama ollama/models
复制代码


  • 应用设置
重载systemd并重启Ollama
  1. systemctl daemon-reload
  2. systemctl restart ollama
复制代码
设置完成后,访问测试。浏览器访问http://IP:11434/,出现Ollama is running代表成功。

1.3、下载模型

ollama的命令和docker操作命令非常相似。可通过shell窗口输入ollama查看相关命令:
  1. ******:~/work# ollama
  2. Usage:
  3.   ollama [flags]
  4.   ollama [command]
  5. Available Commands:
  6.   serve       Start ollama
  7.   create      Create a model from a Modelfile
  8.   show        Show information for a model
  9.   run         Run a model
  10.   pull        Pull a model from a registry
  11.   push        Push a model to a registry
  12.   list        List models
  13.   ps          List running models
  14.   cp          Copy a model
  15.   rm          Remove a model
  16.   help        Help about any command
  17. Flags:
  18.   -h, --help      help for ollama
  19.   -v, --version   Show version information
  20. Use "ollama [command] --help" for more information about a command.
复制代码
由上可知ollama相关命令:
  1. ollama serve                # 启动ollama
  2. ollama create               # 从模型文件创建模型
  3. ollama show                # 显示模型信息
  4. ollama run                # 运行模型
  5. ollama pull                # 从注册仓库中拉取模型
  6. ollama push                # 将模型推送到注册仓库
  7. ollama list                # 列出已下载模型
  8. ollama cp                # 复制模型
  9. ollama rm                # 删除模型
  10. ollama help                # 获取有关任何命令的帮助信息
复制代码


  • 拉取qwen2-7b模型
  1. ollama pull qwen2:7b
  2. #下载成功查看模型
  3. ollama list
复制代码
可见,已成功拉取:

也可以自界说模型,所谓自界说模型就是不适用Ollama官方模型库中的模型,理论可以利用其他各类经过转换处理的模型,有从GGUF导入和从PyTorch或Safetensors导入两种方式。
所谓从从PyTorch或Safetensors导入Ollama,实在就是利用llama.cpp项目,对PyTorch或Safetensors类型的模型举行转换、量化处理成GGUF格式的模型,然后再用Ollama加载利用 。
参考:Ollama:一个在当地摆设、运行大型语言模型的工具-CSDN博客



  • 运行模型
运行模型并举行对话:
  1. 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

生成回复
  1. curl http://localhost:11434/api/generate -d '{
  2.   "model": "qwen2:7b",
  3.   "prompt":"你是谁?为什么天空是蓝色的?"
  4. }'
复制代码
上述localhost也可以换成ip。

若要禁用流式,如下操作:
  1. curl http://ip:11434/api/generate -d '{
  2.   "model": "qwen2:7b",
  3.   "prompt":"你是谁?为什么天空是蓝色的?",
  4.   "stream":false
  5. }'
复制代码

与模型聊天
  1. curl http://localhost:11434/api/chat -d '{
  2.   "model": "qwen2:7b",
  3.   "messages": [
  4.     { "role": "user", "content": "天空为什么是蓝色的?" }
  5.   ]
  6. }'
复制代码

也可以带汗青记录
  1. curl http://localhost:11434/api/chat -d '{
  2.   "model": "qwen2:7b",
  3.   "messages": [
  4.     {
  5.       "role": "user",
  6.       "content": "why is the sky blue?"
  7.     },
  8.     {
  9.       "role": "assistant",
  10.       "content": "due to rayleigh scattering."
  11.     },
  12.     {
  13.       "role": "user",
  14.       "content": "how is that different than mie scattering?"
  15.     }
  16.   ]
  17. }'
复制代码
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,利用以下命令:
  1. 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为例)
  1. # Copying required .env file
  2. cp -RPp .env.example .env
  3. # Building Frontend Using Node
  4. npm install
  5. npm run build
  6. cd ./backend
  7. # Optional: To install using Conda as your development environment, follow these instructions:
  8. # Create and activate a Conda environment
  9. conda create --name open-webui-env python=3.11
  10. conda activate open-webui-env
  11. # Install dependencies
  12. pip install -r requirements.txt -U
  13. # Start the application
  14. bash start.sh
复制代码
在鼠鼠我多次构建的过程中,有次有碰到一个错误,报错如下:
  1. (venv) ******:~/PycharmProjects/openwebui(v0.3.32)/open-webui$ npm run build
  2. > open-webui@0.3.32 build
  3. > npm run pyodide:fetch && vite build
  4. > open-webui@0.3.32 pyodide:fetch
  5. > node scripts/prepare-pyodide.js
  6. Setting up pyodide + micropip
  7. 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
  8.     at new NodeError (node:internal/errors:405:5)
  9.     at finalizeResolution (node:internal/modules/esm/resolve:327:11)
  10.     at moduleResolve (node:internal/modules/esm/resolve:980:10)
  11.     at defaultResolve (node:internal/modules/esm/resolve:1193:11)
  12.     at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:403:12)
  13.     at ModuleLoader.resolve (node:internal/modules/esm/loader:372:25)
  14.     at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:249:38)
  15.     at ModuleLoader.import (node:internal/modules/esm/loader:335:34)
  16.     at importModuleDynamically (node:internal/modules/esm/translators:143:35)
  17.     at importModuleDynamicallyCallback (node:internal/modules/esm/utils:112:14) {
  18.   url: 'file:///home/***/PycharmProjects/openwebuiv0.3.32/open-webui/node_modules/pyodide/pyodide.asm.js',
  19.   code: 'ERR_MODULE_NOT_FOUND'
  20. }
  21. Copying Pyodide files into static directory
  22. node:internal/process/promises:288
  23.             triggerUncaughtException(err, true /* fromPromise */);
  24.             ^
  25. [Error: ENOENT: no such file or directory, open '/home/***/PycharmProjects/openwebuiv0.3.32/open-webui/node_modules/pyodide/python_stdlib.zip'] {
  26.   errno: -2,
  27.   code: 'ENOENT',
  28.   syscall: 'open',
  29.   path: '/home/***/PycharmProjects/openwebuiv0.3.32/open-webui/node_modules/pyodide/python_stdlib.zip'
  30. }
  31. 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:

    • 包含与向量相关的文件,如dbs和connector.py,用于处理向量数据库的连接和交互,以及向量化文本数据以用于相似性搜索。
    • main.py文件大概包含与向量检索相关的告急逻辑。

  • web:

    • 包含多个与Web相关的Python文件,如brave.py、duckduckgo.py等,这些文件用于实现与不同搜索引擎(如Brave Search、DuckDuckGo)的交互,以便从这些搜索引擎获取数据。
    • main.py和utils.py文件大概包含Web应用的告急逻辑和辅助功能。
    • testdata目录大概包含用于测试的示例数据。

  • utils.py:一个通用的工具文件,包含在整个应用中利用的辅助函数和类。
  • main.py:后端服务入口点,告急用于处理文档检索和向量数据库操作。

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

main.py

设置和模型更新


  • 界说 update_embedding_model 和 update_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_db 和 store_text_in_vector_db 函数,用于将数据存储到向量数据库中。
错误处理


  • 利用 HTTPException 处理错误环境,并返回错误信息。
辅助函数


  • 界说 get_web_loader、validate_url、resolve_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和调用大模型:
  1. """
  2. [open_webui.apps.ollama.main]
  3. """
  4. @app.post("/api/chat/{url_idx}")
  5. async def generate_chat_completion(
  6.     form_data: GenerateChatCompletionForm,
  7.     url_idx: Optional[int] = None,
  8.     user=Depends(get_verified_user),
  9. ):
  10.     log.info(f"/api/chat或/api/chat/{url_idx}")
  11.     payload = {**form_data.model_dump(exclude_none=True)}
  12.     log.debug(f"{payload = }")
  13.     if "metadata" in payload:
  14.         del payload["metadata"]
  15.     model_id = form_data.model
  16.     if app.state.config.ENABLE_MODEL_FILTER:
  17.         if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
  18.             raise HTTPException(
  19.                 status_code=403,
  20.                 detail="Model not found",
  21.             )
  22.     model_info = Models.get_model_by_id(model_id)
  23.     if model_info:
  24.         if model_info.base_model_id:
  25.             payload["model"] = model_info.base_model_id
  26.         params = model_info.params.model_dump()
  27.         if params:
  28.             if payload.get("options") is None:
  29.                 payload["options"] = {}
  30.             payload["options"] = apply_model_params_to_body_ollama(
  31.                 params, payload["options"]
  32.             )
  33.             #构建prompt
  34.             payload = apply_model_system_prompt_to_body(params, payload, user)
  35.     if ":" not in payload["model"]:
  36.         payload["model"] = f"{payload['model']}:latest"
  37.     url = get_ollama_url(url_idx, payload["model"])
  38.     log.info(f"url: {url}")
  39.     log.debug(payload)
  40.     #调用大模型
  41.     return await post_streaming_url(
  42.         f"{url}/api/chat",
  43.         json.dumps(payload),
  44.         stream=form_data.stream,
  45.         content_type="application/x-ndjson",
  46.     )
复制代码
构建prompt的apply_model_system_prompt_to_body函数细节:
  1. """
  2. backend/open_webui/utils/payload.py
  3. """
  4. # inplace function: form_data is modified
  5. def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
  6.     system = params.get("system", None)
  7.     if not system:
  8.         return form_data
  9.     if user:
  10.         template_params = {
  11.             "user_name": user.name,
  12.             "user_location": user.info.get("location") if user.info else None,
  13.         }
  14.     else:
  15.         template_params = {}
  16.     system = prompt_template(system, **template_params)
  17.     form_data["messages"] = add_or_update_system_message(
  18.         system, form_data.get("messages", [])
  19.     )
  20.     return form_data
复制代码
调用大模型的post_streaming_url函数细节:
  1. """
  2. backend/open_webui/apps/ollama/main.py
  3. """
  4. async def post_streaming_url(
  5.     url: str, payload: Union[str, bytes], stream: bool = True, content_type=None
  6. ):
  7.     log.info("post_streaming_url")
  8.     r = None
  9.     try:
  10.         session = aiohttp.ClientSession(
  11.             trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
  12.         )
  13.         r = await session.post(
  14.             url,
  15.             data=payload,
  16.             headers={"Content-Type": "application/json"},
  17.         )
  18.         r.raise_for_status()
  19.         if stream:
  20.             headers = dict(r.headers)
  21.             if content_type:
  22.                 headers["Content-Type"] = content_type
  23.             return StreamingResponse(
  24.                 r.content,
  25.                 status_code=r.status,
  26.                 headers=headers,
  27.                 background=BackgroundTask(
  28.                     cleanup_response, response=r, session=session
  29.                 ),
  30.             )
  31.         else:
  32.             res = await r.json()
  33.             await cleanup_response(r, session)
  34.             return res
  35.     except Exception as e:
  36.         error_detail = "Open WebUI: Server Connection Error"
  37.         if r is not None:
  38.             try:
  39.                 res = await r.json()
  40.                 if "error" in res:
  41.                     error_detail = f"Ollama: {res['error']}"
  42.             except Exception:
  43.                 error_detail = f"Ollama: {e}"
  44.         raise HTTPException(
  45.             status_code=r.status if r else 500,
  46.             detail=error_detail,
  47.         )
复制代码

情景2:用户举行联网搜索提问“武汉今天气候怎样”时,代码调用逻辑:




核心代码:
1、生成搜索查询:
  1. """
  2. backend/open_webui/main.py
  3. """
  4. @app.post("/api/task/query/completions")
  5. async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
  6.     log.info("/api/task/query/completions")
  7.     print("generate_search_query")
  8.     if not app.state.config.ENABLE_SEARCH_QUERY:
  9.         raise HTTPException(
  10.             status_code=status.HTTP_400_BAD_REQUEST,
  11.             detail=f"Search query generation is disabled",
  12.         )
  13.     model_id = form_data["model"]
  14.     if model_id not in app.state.MODELS:
  15.         raise HTTPException(
  16.             status_code=status.HTTP_404_NOT_FOUND,
  17.             detail="Model not found",
  18.         )
  19.     # Check if the user has a custom task model
  20.     # If the user has a custom task model, use that model
  21.     task_model_id = get_task_model_id(model_id)
  22.     print(task_model_id)
  23.     model = app.state.MODELS[task_model_id]
  24.     if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
  25.         template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
  26.     else:
  27.         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}}.
  28. User Message:
  29. {{prompt:end:4000}}
  30. Interaction History:
  31. {{MESSAGES:END:6}}
  32. Search Query:"""
  33.     content = search_query_generation_template(
  34.         template, form_data["messages"], {"name": user.name}
  35.     )
  36.     print("content", content)
  37.     payload = {
  38.         "model": task_model_id,
  39.         "messages": [{"role": "user", "content": content}],
  40.         "stream": False,
  41.         **(
  42.             {"max_tokens": 30}
  43.             if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
  44.             else {
  45.                 "max_completion_tokens": 30,
  46.             }
  47.         ),
  48.         "metadata": {"task": str(TASKS.QUERY_GENERATION), "task_body": form_data},
  49.     }
  50.     log.debug(payload)
  51.     # Handle pipeline filters
  52.     try:
  53.         payload = filter_pipeline(payload, user)
  54.     except Exception as e:
  55.         if len(e.args) > 1:
  56.             return JSONResponse(
  57.                 status_code=e.args[0],
  58.                 content={"detail": e.args[1]},
  59.             )
  60.         else:
  61.             return JSONResponse(
  62.                 status_code=status.HTTP_400_BAD_REQUEST,
  63.                 content={"detail": str(e)},
  64.             )
  65.     if "chat_id" in payload:
  66.         del payload["chat_id"]
  67.     return await generate_chat_completions(form_data=payload, user=user)
复制代码
2、执行搜索查询
  1. """
  2. backend/open_webui/apps/retrieval/main.py
  3. """
  4. @app.post("/process/web/search")
  5. def process_web_search(form_data: SearchForm, user=Depends(get_verified_user)):
  6.     log.info("调用函数process_web_search")
  7.     try:
  8.         logging.info(
  9.             f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
  10.         )
  11.         web_results = search_web(
  12.             app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
  13.         )
  14.     except Exception as e:
  15.         log.exception(e)
  16.         print(e)
  17.         raise HTTPException(
  18.             status_code=status.HTTP_400_BAD_REQUEST,
  19.             detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
  20.         )
  21.     try:
  22.         collection_name = form_data.collection_name
  23.         if collection_name == "":
  24.             collection_name = calculate_sha256_string(form_data.query)[:63]
  25.         urls = [result.link for result in web_results]
  26.         loader = get_web_loader(urls)
  27.         docs = loader.load()
  28.         save_docs_to_vector_db(docs, collection_name, overwrite=True)
  29.         return {
  30.             "status": True,
  31.             "collection_name": collection_name,
  32.             "filenames": urls,
  33.         }
  34.     except Exception as e:
  35.         log.exception(e)
  36.         raise HTTPException(
  37.             status_code=status.HTTP_400_BAD_REQUEST,
  38.             detail=ERROR_MESSAGES.DEFAULT(e),
  39.         )
复制代码
情景3.1:用户上传PDF文件,让其帮忙总结(联网搜索功能关闭),代码逻辑:



核心代码:
1、吸取处理保存用户上传的PDF文件
  1. """
  2. backend/open_webui/apps/webui/routers/files.py
  3. """
  4. @router.post("/")
  5. def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
  6.     log.info("调用函数:upload_file")
  7.     log.info(f"file.content_type: {file.content_type}")
  8.     try:
  9.         unsanitized_filename = file.filename
  10.         filename = os.path.basename(unsanitized_filename)
  11.         # replace filename with uuid
  12.         id = str(uuid.uuid4())
  13.         name = filename
  14.         filename = f"{id}_{filename}"
  15.         file_path = f"{UPLOAD_DIR}/{filename}"
  16.         contents = file.file.read()
  17.         with open(file_path, "wb") as f:
  18.             f.write(contents)
  19.             f.close()
  20.         file = Files.insert_new_file(
  21.             user.id,
  22.             FileForm(
  23.                 **{
  24.                     "id": id,
  25.                     "filename": filename,
  26.                     "meta": {
  27.                         "name": name,
  28.                         "content_type": file.content_type,
  29.                         "size": len(contents),
  30.                         "path": file_path,
  31.                     },
  32.                 }
  33.             ),
  34.         )
  35.         try:
  36.             process_file(ProcessFileForm(file_id=id))
  37.             file = Files.get_file_by_id(id=id)
  38.         except Exception as e:
  39.             log.exception(e)
  40.             log.error(f"Error processing file: {file.id}")
  41.         if file:
  42.             return file
  43.         else:
  44.             raise HTTPException(
  45.                 status_code=status.HTTP_400_BAD_REQUEST,
  46.                 detail=ERROR_MESSAGES.DEFAULT("Error uploading file"),
  47.             )
  48.     except Exception as e:
  49.         log.exception(e)
  50.         raise HTTPException(
  51.             status_code=status.HTTP_400_BAD_REQUEST,
  52.             detail=ERROR_MESSAGES.DEFAULT(e),
  53.         )
复制代码
2、上述调用的处理文件的函数
  1. """
  2. backend/open_webui/apps/retrieval/main.py
  3. """
  4. @app.post("/process/file")
  5. def process_file(
  6.     form_data: ProcessFileForm,
  7.     user=Depends(get_verified_user),
  8. ):
  9.     log.info("调用函数:process_file")
  10.     try:
  11.         file = Files.get_file_by_id(form_data.file_id)
  12.         collection_name = form_data.collection_name
  13.         if collection_name is None:
  14.             collection_name = f"file-{file.id}"
  15.         if form_data.content:
  16.             # Update the content in the file
  17.             # Usage: /files/{file_id}/data/content/update
  18.             VECTOR_DB_CLIENT.delete(
  19.                 collection_name=f"file-{file.id}",
  20.                 filter={"file_id": file.id},
  21.             )
  22.             docs = [
  23.                 Document(
  24.                     page_content=form_data.content,
  25.                     metadata={
  26.                         "name": file.meta.get("name", file.filename),
  27.                         "created_by": file.user_id,
  28.                         "file_id": file.id,
  29.                         **file.meta,
  30.                     },
  31.                 )
  32.             ]
  33.             text_content = form_data.content
  34.         elif form_data.collection_name:
  35.             # Check if the file has already been processed and save the content
  36.             # Usage: /knowledge/{id}/file/add, /knowledge/{id}/file/update
  37.             result = VECTOR_DB_CLIENT.query(
  38.                 collection_name=f"file-{file.id}", filter={"file_id": file.id}
  39.             )
  40.             if len(result.ids[0]) > 0:
  41.                 docs = [
  42.                     Document(
  43.                         page_content=result.documents[0][idx],
  44.                         metadata=result.metadatas[0][idx],
  45.                     )
  46.                     for idx, id in enumerate(result.ids[0])
  47.                 ]
  48.             else:
  49.                 docs = [
  50.                     Document(
  51.                         page_content=file.data.get("content", ""),
  52.                         metadata={
  53.                             "name": file.meta.get("name", file.filename),
  54.                             "created_by": file.user_id,
  55.                             "file_id": file.id,
  56.                             **file.meta,
  57.                         },
  58.                     )
  59.                 ]
  60.             text_content = file.data.get("content", "")
  61.         else:
  62.             # Process the file and save the content
  63.             # Usage: /files/
  64.             file_path = file.meta.get("path", None)
  65.             if file_path:
  66.                 loader = Loader(
  67.                     engine=app.state.config.CONTENT_EXTRACTION_ENGINE,
  68.                     TIKA_SERVER_URL=app.state.config.TIKA_SERVER_URL,
  69.                     PDF_EXTRACT_IMAGES=app.state.config.PDF_EXTRACT_IMAGES,
  70.                 )
  71.                 docs = loader.load(
  72.                     file.filename, file.meta.get("content_type"), file_path
  73.                 )
  74.             else:
  75.                 docs = [
  76.                     Document(
  77.                         page_content=file.data.get("content", ""),
  78.                         metadata={
  79.                             "name": file.filename,
  80.                             "created_by": file.user_id,
  81.                             "file_id": file.id,
  82.                             **file.meta,
  83.                         },
  84.                     )
  85.                 ]
  86.             text_content = " ".join([doc.page_content for doc in docs])
  87.         log.debug(f"text_content: {text_content}")
  88.         Files.update_file_data_by_id(
  89.             file.id,
  90.             {"content": text_content},
  91.         )
  92.         hash = calculate_sha256_string(text_content)
  93.         Files.update_file_hash_by_id(file.id, hash)
  94.         try:
  95.             result = save_docs_to_vector_db(
  96.                 docs=docs,
  97.                 collection_name=collection_name,
  98.                 metadata={
  99.                     "file_id": file.id,
  100.                     "name": file.meta.get("name", file.filename),
  101.                     "hash": hash,
  102.                 },
  103.                 add=(True if form_data.collection_name else False),
  104.             )
  105.             if result:
  106.                 Files.update_file_metadata_by_id(
  107.                     file.id,
  108.                     {
  109.                         "collection_name": collection_name,
  110.                     },
  111.                 )
  112.                 return {
  113.                     "status": True,
  114.                     "collection_name": collection_name,
  115.                     "filename": file.meta.get("name", file.filename),
  116.                     "content": text_content,
  117.                 }
  118.         except Exception as e:
  119.             raise e
  120.     except Exception as e:
  121.         log.exception(e)
  122.         if "No pandoc was found" in str(e):
  123.             raise HTTPException(
  124.                 status_code=status.HTTP_400_BAD_REQUEST,
  125.                 detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
  126.             )
  127.         else:
  128.             raise HTTPException(
  129.                 status_code=status.HTTP_400_BAD_REQUEST,
  130.                 detail=str(e),
  131.             )
复制代码
情景3.2:用户上传PDF文件,让其帮忙总结,(联网搜索功能开启),代码逻辑:




五、总结

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

缠丝猫

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表