我们现在可以对话,也有了内部的数据库,现在我们加一个上传文件功能。

上传文件功能:

# 导入 chainlit 库,用于构建和部署 AI 应用
from importlib.metadata import files

import chainlit as cl

# 导入 Settings 类,用于配置 llama_index 的核心设置
from llama_index.core import Settings, StorageContext, load_index_from_storage, SimpleDirectoryReader

# 导入 SimpleChatEngine 类,用于实现简单的聊天引擎
from llama_index.core.chat_engine import SimpleChatEngine
from llama_index.core.chat_engine.types import ChatMode
from llama_index.core.memory import ChatMemoryBuffer



# 导入 deepseek_llm,可能是一个自定义的或第三方的语言模型库
from llms import deepseek_llm,moonshot_llm
from base_rag import create_chat_model, create_index


# from rag_pro3.main import chat_engine
# from rag_pro2.base_rag import create_index



from dotenv import load_dotenv
load_dotenv()


@cl.on_chat_start
async def start():
    """
    当聊天开始时异步执行的函数。
    初始化设置和聊天引擎,并向用户发送欢迎消息。

    1. 初始化 Settings.llm 为 deepseek_llm()。
    2. 创建一个 SimpleChatEngine 实例,并存储到用户会话中。
    3. 异步发送一条消息给用户,介绍自己并询问用户需要的帮助。
    """
    # 初始化大型语言模型(LLM)设置
    Settings.llm = moonshot_llm()

    # 创建一个简单的聊天引擎实例
    # chat_engine = SimpleChatEngine.from_defaults()
    chat_engine = await create_chat_model() #
    # 将聊天引擎对象存储到用户会话中,以便后续使用
    cl.user_session.set("chat_engine", chat_engine)

    # 发送一条欢迎消息,介绍 AI 助手的功能和作用
    await cl.Message(
        author="Assistant",
        content="你好!我是 AI 助手。有什么可以帮助你的吗?"
    ).send()


# Chainlit 消息事件处理函数
@cl.on_message
async def main(message: cl.Message):
    """
    处理接收到的消息,并使用 chat_engine 生成回复。

    参数:
        message: cl.Message 类型,表示接收到的消息。
    """

    # 12.从用户会话中获取chat_engine
    chat_engine = cl.user_session.get("chat_engine")
    # 13.创建一个空的助理消息
    msg = cl.Message(content="", author="Assistant")

    files=[]
    # 遍历接收用户输入的元素 筛选出文件和图片类型的元素
    for element in message.elements:
        if isinstance(element, cl.File) or isinstance(element, cl.Image):
            files.append(element.path)

    # 如果用户上传了文件,则进一步处理这些文件
    if len( files)>0:
        # 使用SimpleDirectoryReader 读取文件内容并加载数据
        data = SimpleDirectoryReader(input_files=files).load_data()
        # 创建或更新本地索引,以支持后续的查询与检索操作
        # index = await create_index(data)
        index = create_index(data)

        chat_engine = index.as_chat_engine(chat_mode=ChatMode.CONTEXT)
        cl.user_session.set("chat_engine", chat_engine)

    # 1使用chat_engine生成回复
    res = await cl.make_async(chat_engine.stream_chat)(message.content)
    # 流式界面输出
    for token in res.response_gen:
        await msg.stream_token(token)
    # 发送消息
    await msg.send()






    # 从用户会话中获取 chat_engine 对象
    chat_engine = cl.user_session.get("chat_engine")

    # 初始化一个空的消息对象,内容为空,作者为 "Assistant"
    msg = cl.Message(content="", author="Assistant")

    # 使用 chat_engine 的 stream_chat 方法处理接收到的消息内容,并将结果转换为异步响应
    res = await cl.make_async(chat_engine.stream_chat)(message.content)

    # 流式界面输出
    # 遍历生成的响应内容,逐个令牌地输出
    for token in res.response_gen:
        await msg.stream_token(token)

    # 发送完整的消息
    await msg.send()


from typing import Optional
import chainlit as cl

@cl.password_auth_callback
def auth_callback(username: str, password: str):
    # Fetch the user matching username from your database
    # and compare the hashed password with the value stored in the database
    if (username, password) == ("admin", "admin"):
        return cl.User(
            identifier="admin", metadata={"role": "admin", "provider": "credentials"}
        )
    else:
        return None

@cl.on_chat_resume
async def on_chat_resume(thread):
    pass
files=[]
# 遍历接收用户输入的元素 筛选出文件和图片类型的元素
for element in message.elements:
    if isinstance(element, cl.File) or isinstance(element, cl.Image):
        files.append(element.path)

# 如果用户上传了文件,则进一步处理这些文件
if len( files)>0:
    # 使用SimpleDirectoryReader 读取文件内容并加载数据
    data = SimpleDirectoryReader(input_files=files).load_data()
    # 创建或更新本地索引,以支持后续的查询与检索操作
    # index = await create_index(data)
    index = create_index(data)

    chat_engine = index.as_chat_engine(chat_mode=ChatMode.CONTEXT)
    cl.user_session.set("chat_engine", chat_engine)

# 1使用chat_engine生成回复
res = await cl.make_async(chat_engine.stream_chat)(message.content)
# 流式界面输出
for token in res.response_gen:
    await msg.stream_token(token)
# 发送消息
await msg.send()

收集用户上传的文件

先建一个空列表files,用来存文件路径。然后逐个查看用户发的消息里包含的 “元素”(可能是文字、文件、图片等),挑出其中是 “文件” 或 “图片” 类型的,把它们的存放路径放进files列表里。

如果有文件,就处理这些文件

要是files列表里有东西(说明用户上传了文件),就用SimpleDirectoryReader这个工具去读取这些文件的内容,得到数据data。然后用这些数据创建一个 “索引”(可以理解为一个快速查找文件内容的目录),再基于这个索引创建一个 “聊天引擎”(专门用来结合文件内容回答问题的工具),最后把这个聊天引擎存到用户的会话里(方便后续继续用)。

用聊天引擎生成并发送回复

不管有没有文件,最终都用聊天引擎根据用户输入的文字内容生成回复。生成回复时不是一下子全出来,而是像打字一样一个字一个字 “流式” 输出(stream_token),最后把完整回复发送出去。

代码语法细节解释

初始化文件列表
files = []
  • 语法:创建一个空列表files,用来存储后续筛选出的文件路径。
  • 作用:准备一个容器,后续存放符合条件的文件路径。
筛选文件 / 图片类型的元素
for element in message.elements:
    if isinstance(element, cl.File) or isinstance(element, cl.Image):
        files.append(element.path)
  • for element in message.elements:遍历message.elements(用户消息中包含的所有元素,可能有文字、文件、图片等),每次取一个元素存到element里。
  • isinstance(element, cl.File) or isinstance(element, cl.Image):isinstance(a, b)是判断 “a是不是b类型的对象” 的函数。这里判断当前元素element是不是cl.File(文件类型)或者cl.Image(图片类型)。cl应该是一个库的缩写(比如 Chainlit,一个常用的聊天交互库)。
  • files.append(element.path):如果元素是文件或图片,就把它的path(文件存放路径)添加到files列表里。append是列表的方法,用来在末尾添加元素。
处理上传的文件(如果有)
if len(files) > 0:
    data = SimpleDirectoryReader(input_files=files).load_data()
    index = create_index(data)
    chat_engine = index.as_chat_engine(chat_mode=ChatMode.CONTEXT)
    cl.user_session.set("chat_engine", chat_engine)
  • if len(files) > 0:len(files)获取列表files的长度(即文件数量),如果大于 0,说明用户上传了文件,就执行下面的处理。
  • SimpleDirectoryReader(input_files=files).load_data():SimpleDirectoryReader是一个读取文件的工具(可能来自llama_index等处理文档的库),input_files=files指定要读取的文件路径列表,.load_data()方法执行读取,返回文件内容数据data
  • index = create_index(data):调用create_index函数,用读取到的data创建一个索引(索引是为了快速检索文件内容,类似书籍的目录)。
  • index.as_chat_engine(chat_mode=ChatMode.CONTEXT):基于索引创建一个 “聊天引擎”,chat_mode=ChatMode.CONTEXT指定聊天模式为 “结合上下文(即文件内容)”,让引擎能基于文件内容回答问题。
  • cl.user_session.set("chat_engine", chat_engine):把创建好的chat_engine存到用户会话(user_session)里,用键"chat_engine"标识,方便后续其他地方调用(比如用户继续提问时,不用重新创建引擎)。
生成并发送回复
res = await cl.make_async(chat_engine.stream_chat)(message.content)
for token in res.response_gen:
    await msg.stream_token(token)
await msg.send()
  • await cl.make_async(chat_engine.stream_chat)(message.content):
    • chat_engine.stream_chat是聊天引擎的 “流式聊天” 方法,接收用户输入的内容(message.content)并生成回复。
    • cl.make_async(...)是把这个方法转换成异步执行的形式(因为聊天生成可能耗时,异步不会阻塞程序)。
    • await表示等待这个异步操作完成,结果存到res里(res包含生成的回复信息)。
  • for token in res.response_gen:res.response_gen是一个 “生成器”,会逐个产生回复的 “token”(可以理解为文字片段,比如一个字、一个词)。这里遍历每个token
  • await msg.stream_token(token):把每个token通过stream_token方法 “流式输出”(就像实时打字一样,用户能看到文字一点点出来),await确保输出完成后再处理下一个。
  • await msg.send():所有token输出完后,调用send()方法把完整回复正式发送出去。

总结:这段代码的核心是 “处理用户上传的文件→基于文件内容创建聊天引擎→用引擎生成流式回复”,语法上主要涉及列表操作、循环判断、函数调用、异步操作(await)等。

在app.py中加上这个文件上传,总体流程:遍历用户发来的信息,然后进行判断是否有文件类型,或者图片类型, 有我们进行读取内容,将读取的内容方入我们的聊天引擎(思路不难,难点在于不同的语法)

接下来我们进行chainlit的配置,它有很多内部配置,在config.toml中

# 项目配置部分
# 定义应用程序运行所需的基础设置和安全选项

[project]
# 用户需要提供的环境变量列表(如 API 密钥)
# 每个用户在使用应用前必须提供这些变量
user_env = []

# 连接断开后会话保存的持续时间(单位:秒)
# 超过该时间未恢复连接将丢失会话数据
session_timeout = 3600

# 用户会话的有效期(单位:秒)
# 超过后用户需重新登录
user_session_timeout = 1296000  # 15天

# 是否启用第三方缓存功能(例如 LangChain 缓存)
cache = false

# 是否将用户的环境变量(如 API 密钥)持久化到数据库中
# true 表示存储到数据库,false 表示不存储以提高安全性
persist_user_env = false

# 是否在 UI 中隐藏用户环境变量(如显示为 ***)
# true 表示遮蔽显示,false 表示明文显示
mask_user_env = false

# 允许跨域请求的来源地址列表
# "*" 表示允许所有来源
allow_origins = ["*"]

# 功能特性配置部分
# 控制各种高级功能的启用状态和行为参数

[features]
# 是否允许在消息中渲染 HTML 内容
# 启用可能带来 XSS 安全风险,请谨慎使用
unsafe_allow_html = false

# 是否支持 LaTeX 数学公式渲染
# 可能与消息中的 "$" 字符冲突
latex = false

# 是否自动滚动新用户消息到窗口顶部
user_message_autoscroll = true

# 是否根据当前聊天配置自动为线程打标签
auto_tag_thread = true

# 是否允许用户编辑自己发送的消息
edit_message = true

# Slack 相关功能配置
[features.slack]
# 收到消息时是否添加表情符号反馈(需配置 reactions:write OAuth 权限)
reaction_on_message_received = false

# 文件自发上传功能配置
[features.spontaneous_file_upload]
    # 是否启用文件上传功能
    enabled = true

    # 接受的文件类型,支持 MIME 类型、通配符或扩展名映射
    # 示例:
    # 1. 特定文件类型: accept = ["image/jpeg", "image/png", "application/pdf"]
    # 2. 所有某类文件: accept = ["image/*", "audio/*", "video/*"]
    # 3. 特定扩展名: accept = { "application/octet-stream" = [".xyz", ".pdb"] }
    # 注意:不推荐使用 "*/*",可能导致浏览器警告
    accept = ["*/*"]

    # 单次上传最大文件数量
    max_files = 20

    # 单个文件最大大小(单位:MB)
    max_size_mb = 500

# 音频相关功能配置
[features.audio]
    # 是否启用音频功能
    enabled = false

    # 音频采样率设置
    sample_rate = 24000

# Model Context Protocol (MCP) 功能配置
[features.mcp]
    # 是否启用 MCP 功能
    enabled = false

# MCP 的 Server-Sent Events (SSE) 支持
[features.mcp.sse]
    # 是否启用 SSE 传输方式
    enabled = true

# MCP 的 HTTP 流式传输支持
[features.mcp.streamable-http]
    # 是否启用 HTTP 流式传输
    enabled = true

# MCP 的标准输入输出(STDIO)支持
[features.mcp.stdio]
    # 是否启用 STDIO 传输方式
    enabled = true

    # 允许用于 MCP stdio 服务的可执行程序白名单
    # 只需填写程序名称,如 "npx",而非完整路径
    # 注意:请勿注释此行,解析器依赖该字段
    allowed_executables = [ "npx", "uvx" ]

# 用户界面(UI)配置部分
# 控制前端展示样式、主题、资源加载等选项

[UI]
# 助手名称,用于界面展示
name = "Assistant"

# 默认主题风格(可选 "dark" 或 "light")
# default_theme = "dark"

# 页面布局模式(可选 "wide" 或其他布局)
# layout = "wide"

# 默认侧边栏状态(可选 "open" 或 "closed")
# default_sidebar_state = "open"

# 助手描述信息,用于 HTML meta 标签
# description = ""

# Chain of Thought (推理链) 显示模式
# 可选值:"hidden"(隐藏)、"tool_call"(仅工具调用)、"full"(完整显示)
cot = "full"

# 自定义 CSS 文件路径,用于界面样式定制
# 文件可来自 public 目录或外部链接
# custom_css = "/public/test.css"

# 自定义 CSS 文件附加属性(如 media="print")
# custom_css_attributes = "media=\"print\""

# 自定义 JavaScript 文件路径,用于界面交互增强
# 文件应位于 public 目录下
# custom_js = "/public/test.js"

# 警告框样式风格(可选 "classic" 或 "modern")
alert_style = "classic"

# 自定义 JS 文件附加属性(如 async type="module")
# custom_js_attributes = "async type = \"module\""

# 登录页面背景图片路径(相对 public 目录或外部 URL)
# login_page_image = "/public/custom-background.jpg"

# 登录页背景图滤镜效果(Tailwind 内置滤镜)
# login_page_image_filter = "brightness-50 grayscale"
# login_page_image_dark_filter = "contrast-200 blur-sm"

# 自定义 meta 图片 URL,用于分享时展示
# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"

# 助手 Logo 图片 URL
#logo_file_url = "/public/logo.gif"

# 助手默认头像图片 URL
#default_avatar_file_url = "/public/avatar.png"
#default_avatar_file_url = "https://example.com/my-avatar.png"  # 外部头像 URL
# 自定义前端构建目录路径
# 若为相对路径,不应以斜杠开头
# custom_build = "./public/build"

# 自定义头部链接配置(可添加多个)
# [[UI.header_links]]
#     name = "Issues"
#     display_name = "Report Issue"
#     icon_url = "https://avatars.githubusercontent.com/u/128686189?s=200&v=4"
#     url = "https://github.com/Chainlit/chainlit/issues"

在这其中我们可以进行ai助手页面的调整(切记图片的名字不要搞错了)

# 1. LOGO 更改
logo_file_url = "/public/logo.gif"  # 替换为你的 LOGO 文件路径

# 2. 头像更改
default_avatar_file_url = "/public/avatar.jpg"  # 替换为你的头像文件路径

# 3. 聊天历史记录侧边栏
default_sidebar_state = "open"  # 设置侧边栏默认打开

# 4. 登录权限窗口
login_page_image = "/public/login-background.png"  # 登录页面背景图片
# login_page_image_filter = "brightness-50 grayscale"  # 可选:登录页背景滤镜
# login_page_image_dark_filter = "contrast-200 blur-sm"  # 可选:暗色模式滤镜

# 5. 窗口的背景
custom_css = "/public/custom-style.css"  # 自定义 CSS 文件,用于设置窗口背景

# 6. 更改页面底部提示
custom_js = "/public/custom-footer.js"  # 自定义 JS 文件,用于修改底部提示

接下来我们配置一个登录

@cl.password_auth_callback
def auth_callback(username: str, password: str):
    # Fetch the user matching username from your database
    # and compare the hashed password with the value stored in the database
    if (username, password) == ("admin", "admin"):
        return cl.User(
            identifier="admin", metadata={"role": "admin", "provider": "credentials"}
        )
    else:
        return None

@cl.on_chat_resume
async def on_chat_resume(thread):
    pass

好这就是第三阶段的任务,已经完成拉(看起来这个ai助手有点假把式了),接下来我们进行第四阶段

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐