LlamaIndex 文档3


一、使用 LLamaIndex 构建全栈 Web 应用程序的指南

LlamaIndex 是一个 Python 库,这意味着将其与全栈 Web 应用程序集成将与您可能习惯的方式略有不同。

本指南旨在逐步介绍创建用 python 编写的基本 API 服务所需的步骤,以及它如何与 TypeScript+React 前端交互。

这里的所有代码示例都可以从flask_react文件夹中的llama_index_starter_pack获得。

本指南使用的主要技术如下:

  • python3.11
  • 骆驼索引
  • 烧瓶
  • 打字稿
  • 反应

1、Flask 后端

对于本指南,我们的后端将使用Flask API 服务器与我们的前端代码进行通信。
如果您愿意,您还可以轻松地将其转换为FastAPI服务器,或您选择的任何其他 python 服务器库。

使用 Flask 设置服务器很容易。
您导入包,创建应用程序对象,然后创建端点。
让我们首先为服务器创建一个基本框架:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello World!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5601)

烧瓶演示.py

如果您运行此文件 ( python flask_demo.py),它将在端口 5601 上启动服务器。
如果您访问http://localhost:5601/,您将看到“Hello World!”在浏览器中呈现的文本。
好的!

下一步是决定我们要在服务器中包含哪些功能,并开始使用 LlamaIndex。

为了简单起见,我们可以提供的最基本的操作是查询现有索引。
使用LlamaIndex 中的paul graham 论文,创建一个文档文件夹,然后下载+将论文文本文件放入其中。


基本 Flask - 处理用户索引查询

现在,让我们编写一些代码来初始化索引:

import os
from llama_index.core import (
    SimpleDirectoryReader,
    VectorStoreIndex,
    StorageContext,
)

# NOTE: for local testing only, do NOT deploy with your key hardcoded
os.environ["OPENAI_API_KEY"] = "your key here"

index = None

def initialize_index():
    global index
    storage_context = StorageContext.from_defaults()
    if os.path.exists(index_dir):
        index = load_index_from_storage(storage_context)
    else:
        documents = SimpleDirectoryReader("./documents").load_data()
        index = VectorStoreIndex.from_documents(
            documents, storage_context=storage_context
        )
        storage_context.persist(index_dir)

该函数将初始化我们的索引。
如果我们在函数中启动 Flask 服务器之前调用它main,那么我们的索引将准备好供用户查询!

我们的查询端点将接受GET以查询文本作为参数的请求。
完整的端点函数如下所示:

from flask import request

@app.route("/query", methods=["GET"])
def query_index():
    global index
    query_text = request.args.get("text", None)
    if query_text is None:
        return (
            "No text found, please include a ?text=blah parameter in the URL",
            400,
        )
    query_engine = index.as_query_engine()
    response = query_engine.query(query_text)
    return str(response), 200

现在,我们向我们的服务器引入了一些新概念:

  • 一个新的/query端点,由函数装饰器定义
  • 来自flask的新导入request,用于从请求中获取参数
  • 如果text缺少参数,我们将返回错误消息和相应的 HTML 响应代码
  • 否则,我们查询索引,并以字符串形式返回响应

您可以在浏览器中测试的完整查询示例可能如下所示:(http://localhost:5601/query?text=what did the author do growing up按 Enter 键后,浏览器会将空格转换为“%20”字符)。

事情看起来相当不错!我们现在有了一个功能性 API。
使用您自己的文档,您可以轻松地为任何应用程序提供一个接口来调用 Flask API 并获取查询的答案。


Advanced Flask - 处理用户文档上传

事情看起来很酷,但我们怎样才能更进一步呢?如果我们想让用户通过上传自己的文档来建立自己的索引怎么办?不用担心,Flask 可以处理这一切:muscle:。

为了让用户上传文档,我们必须采取一些额外的预防措施。
该索引将变为可变的,而不是查询现有索引。
如果有很多用户添加到同一个索引,我们需要考虑如何处理并发。
我们的 Flask 服务器是线程化的,这意味着多个用户可以通过同时处理的请求来 ping 服务器。

一种选择可能是为每个用户或组创建索引,并从 S3 存储和获取内容。
但对于本示例,我们假设有一个用户正在与之交互的本地存储索引。

为了处理并发上传并确保顺序插入索引,我们可以使用BaseManagerpython 包通过单独的服务器和锁来提供对索引的顺序访问。
这听起来很可怕,但其实也没那么糟糕!我们只需将所有索引操作(初始化、查询、插入)移至BaseManager“index_server”中,该服务器将从 Flask 服务器调用。

index_server.py这是移动代码后的基本示例:

import os
from multiprocessing import Lock
from multiprocessing.managers import BaseManager
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, Document

# NOTE: for local testing only, do NOT deploy with your key hardcoded
os.environ["OPENAI_API_KEY"] = "your key here"

index = None
lock = Lock()

def initialize_index():
    global index

    with lock:
        # same as before ...
        pass

def query_index(query_text):
    global index
    query_engine = index.as_query_engine()
    response = query_engine.query(query_text)
    return str(response)

if __name__ == "__main__":
    # init the global index
    print("initializing index...")
    initialize_index()

    # setup server
    # NOTE: you might want to handle the password in a less hardcoded way
    manager = BaseManager(("", 5602), b"password")
    manager.register("query_index", query_index)
    server = manager.get_server()

    print("starting server...")
    server.serve_forever()

索引服务器.py

因此,我们移动了我们的函数,引入了Lock确保对全局索引进行顺序访问的对象,在服务器中注册了我们的单个函数,并使用密码在端口 5602 上启动了服务器password

然后,我们可以如下调整我们的flask代码:

from multiprocessing.managers import BaseManager
from flask import Flask, request

# initialize manager connection
# NOTE: you might want to handle the password in a less hardcoded way
manager = BaseManager(("", 5602), b"password")
manager.register("query_index")
manager.connect()

@app.route("/query", methods=["GET"])
def query_index():
    global index
    query_text = request.args.get("text", None)
    if query_text is None:
        return (
            "No text found, please include a ?text=blah parameter in the URL",
            400,
        )
    response = manager.query_index(query_text)._getvalue()
    return str(response), 200

@app.route("/")
def home():
    return "Hello World!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5601)

烧瓶演示.py

两个主要的变化是连接到我们现有的BaseManager服务器并注册函数,以及通过/query端点中的管理器调用函数。

需要注意的一件特别的事情是BaseManager服务器并不像我们期望的那样返回对象。
为了将返回值解析为其原始对象,我们调用该_getvalue()函数。

如果我们允许用户上传自己的文档,我们可能应该从文档文件夹中删除 Paul Graham 的文章,所以我们首先这样做。
然后,让我们添加一个端点来上传文件!首先,让我们定义 Flask 端点函数:

...
manager.register("insert_into_index")
...

@app.route("/uploadFile", methods=["POST"])
def upload_file():
    global manager
    if "file" not in request.files:
        return "Please send a POST request with a file", 400

    filepath = None
    try:
        uploaded_file = request.files["file"]
        filename = secure_filename(uploaded_file.filename)
        filepath = os.path.join("documents", os.path.basename(filename))
        uploaded_file.save(filepath)

        if request.form.get("filename_as_doc_id", None) is not None:
            manager.insert_into_index(filepath, doc_id=filename)
        else:
            manager.insert_into_index(filepath)
    except Exception as e:
        # cleanup temp file
        if filepath is not None and os.path.exists(filepath):
            os.remove(filepath)
        return "Error: {}".format(str(e)), 500

    # cleanup temp file
    if filepath is not None and os.path.exists(filepath):
        os.remove(filepath)

    return "File inserted!", 200

还不错!您会注意到我们将文件写入磁盘。
如果我们只接受txt文件等基本文件格式,我们可以跳过这一点,但写入磁盘时我们可以利用 LlamaIndexSimpleDirectoryReader来处理一堆更复杂的文件格式。
(可选)我们还使用第二个POST参数来使用文件名作为 doc_id 或让 LlamaIndex 为我们生成一个。
一旦我们实现了前端,这就会更有意义。

对于这些更复杂的请求,我还建议使用像Postman这样的工具。
使用邮递员测试端点的示例位于该项目的存储库中。

最后,您会注意到我们向管理器添加了一个新功能。
让我们在里面实现它index_server.py

def insert_into_index(doc_text, doc_id=None):
    global index
    document = SimpleDirectoryReader(input_files=[doc_text]).load_data()[0]
    if doc_id is not None:
        document.doc_id = doc_id

    with lock:
        index.insert(document)
        index.storage_context.persist()

...
manager.register("insert_into_index", insert_into_index)
...

简单的!如果我们同时启动index_server.pyflask_demo.pypython 文件,我们就有了一个 Flask API 服务器,它可以处理将文档插入向量索引并响应用户查询的多个请求!

为了支持前端的某些功能,我调整了 Flask API 中的一些响应,并添加了一些功能来跟踪哪些文档存储在索引中(LlamaIndex 目前在用户中不支持此功能) -友好的方式,但我们可以自己增强它!)。
最后,我必须使用 python 包向服务器添加 CORS 支持Flask-cors

查看存储库flask_demo.py中的完整内容和index_server.py脚本,了解最终的细微更改、文件和示例以帮助部署。
requirements.txt``Dockerfile


2、React 前端

一般来说,React 和 Typescript 是当今编写 Web 应用程序最流行的库和语言之一。
本指南假设您熟悉这些工具的工作原理,否则本指南的长度将增加三倍😄。

存储库中,前端代码组织在react_frontend文件夹内。

前端最相关的部分是src/apis文件夹。
这是我们调用 Flask 服务器的地方,支持以下查询:

  • /query– 查询现有索引
  • /uploadFile– 将文件上传到flask服务器以插入索引
  • /getDocuments– 列出当前文档标题及其部分文本

使用这三个查询,我们可以构建一个强大的前端,允许用户上传和跟踪他们的文件、查询索引、查看查询响应以及有关哪些文本节点用于形成响应的信息。


获取文档.tsx

您猜对了,该文件包含的功能是获取索引中当前文档的列表。
代码如下:

export type Document = {
  id: string;
  text: string;
};

const fetchDocuments = async (): Promise<Document[]> => {
  const response = await fetch("http://localhost:5601/getDocuments", {
    mode: "cors",
  });

  if (!response.ok) {
    return [];
  }

  const documentList = (await response.json()) as Document[];
  return documentList;
};

如您所见,我们向 Flask 服务器进行查询(此处假设在本地主机上运行)。
请注意,我们需要包含该mode: 'cors'选项,因为我们正在发出外部请求。

然后,我们检查响应是否正确,如果正确,则获取响应 json 并返回它。
这里,响应 json 是Document在同一文件中定义的对象列表。


查询索引.tsx

该文件将用户查询发送到 Flask 服务器,并获取响应,以及有关索引中哪些节点提供响应的详细信息。

export type ResponseSources = {
  text: string;
  doc_id: string;
  start: number;
  end: number;
  similarity: number;
};

export type QueryResponse = {
  text: string;
  sources: ResponseSources[];
};

const queryIndex = async (query: string): Promise<QueryResponse> => {
  const queryURL = new URL("http://localhost:5601/query?text=1");
  queryURL.searchParams.append("text", query);

  const response = await fetch(queryURL, { mode: "cors" });
  if (!response.ok) {
    return { text: "Error in query", sources: [] };
  }

  const queryResponse = (await response.json()) as QueryResponse;

  return queryResponse;
};

export default queryIndex;

这与文件类似fetchDocuments.tsx,主要区别在于我们将查询文本作为参数包含在 URL 中。
然后,我们检查响应是否正确并以适当的打字稿类型返回它。


插入文档.tsx

最复杂的 API 调用可能是上传文档。
这里的函数接受一个文件对象并POST使用 构造一个请求FormData

应用程序中不使用实际的响应文本,但可用于提供有关文件是否上传失败的一些用户反馈。

const insertDocument = async (file: File) => {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("filename_as_doc_id", "true");

  const response = await fetch("http://localhost:5601/uploadFile", {
    mode: "cors",
    method: "POST",
    body: formData,
  });

  const responseText = response.text();
  return responseText;
};

export default insertDocument;

所有其他前端优点

这几乎涵盖了前端部分! React 前端代码的其余部分是一些非常基本的 React 组件,我尽最大努力让它看起来至少有点漂亮😄。

我鼓励阅读代码库的其余部分并提交任何 PR 以进行改进!


3、结论

本指南涵盖了大量信息。
我们从用 python 编写的基本“Hello World”Flask 服务器,到功能齐全的 LlamaIndex 支持的后端以及如何将其连接到前端应用程序。

正如您所看到的,我们可以轻松地增强和包装 LlamaIndex 提供的服务(如小型外部文档跟踪器),以帮助在前端提供良好的用户体验。

您可以利用它并添加许多功能(多索引/用户支持、将对象保存到 S3、添加 Pinecone 矢量服务器等)。
当您在阅读本文后构建应用程序时,请务必在 Discord 中分享最终结果!祝你好运! :肌肉:


二、使用 Delphic 构建全栈 LlamaIndex Web 应用程序的指南

本指南旨在引导您将 LlamaIndex 与名为Delphic 的可用于生产的 Web 应用程序启动模板一起使用。
这里的所有代码示例都可以从Delphic存储库中获取


1、我们正在建设什么

以下是 Delphic 开箱即用功能的快速演示:

https://user-images.githubusercontent.com/5049984/233236432-aa4980b6-a510-42f3-887a-81485c9644e6.mp4


2、架构概览

Delphic 利用 LlamaIndex python 库让用户创建自己的文档集合,然后可以在响应式前端中进行查询。

我们选择的堆栈提供了响应式、稳健的技术组合,可以 (1) 编排复杂的 Python 处理任务,同时提供 (2) 现代化的响应式前端和 (3) 一个安全的后端来构建附加功能。

核心库是:

  1. 姜戈
  2. 姜戈频道
  3. 忍者姜戈
  4. 雷迪斯
  5. 芹菜
  6. llama-index
  7. 朗查恩
  8. 反应
  9. Docker 和 Docker 组合

由于这个建立在超级稳定的 Django Web 框架上的现代堆栈,入门 Delphic 应用程序拥有简化的开发人员体验、内置身份验证和用户管理、异步向量存储处理以及用于响应式 UI 的基于 Web 套接字的查询连接。
此外,我们的前端是用 TypeScript 构建的,并基于 MUI React,以实现响应式和现代的用户界面。


3、系统要求

Celery 不能在 Windows 上运行。
它可以与适用于 Linux 的 Windows 子系统一起部署,但配置超出了本教程的范围。
因此,我们建议您仅在运行 Linux 或 OSX 时才遵循本教程。
您需要安装 Docker 和 Docker Compose 才能部署应用程序。
本地开发将需要节点版本管理器(nvm)。


4、Django 后端


4.1 项目目录概述

Delphic 应用程序具有遵循常见 Django 项目约定的结构化后端目录组织。
在存储库根目录的子文件夹中./delphic,主要文件夹是:

  1. contrib:此目录包含对 Django 内置contrib应用程序的自定义修改或添加。

  2. indexes:该目录包含与文档索引和 LLM 集成相关的核心功能。
    这包括:

  3. admin.py:应用程序的 Django 管理配置

  4. apps.py:应用配置

  5. models.py:包含应用程序的数据库模型

  6. migrations:包含应用程序的数据库架构迁移的目录

  7. signals.py:定义应用程序的任何信号

  8. tests.py:应用程序的单元测试

  9. tasks:该目录包含使用 Celery 进行异步处理的任务。
    index_tasks.py文件包含创建向量索引的任务。

  10. users:该目录专门用于用户管理,包括:

  11. utils:此目录包含跨应用程序使用的实用程序模块和功能,例如自定义存储后端、路径帮助程序和与集合相关的实用程序。


4.2 数据库模型

Delphic 应用程序有两个核心模型:DocumentCollection
这些模型代表应用程序在使用 LLM 索引和查询文档时处理的中心实体。
它们定义在 ./delphic/indexes/models.py.

  1. Collection:
  2. api_key:将集合链接到 API 密钥的外键。
    这有助于将作业与源 API 密钥关联起来。
  3. title:提供集合标题的字符字段。
  4. description:提供集合描述的文本字段。
  5. status:一个字符字段,利用枚举存储集合的处理状态CollectionStatus
  6. created:记录集合创建时间的日期时间字段。
  7. modified:日期时间字段,记录集合的最后修改时间。
  8. model:存储与集合关联的模型的文件字段。
  9. processing:一个布尔字段,指示当前是否正在处理集合。
  10. Document:
  11. collection:将文档链接到集合的外键。
    这表示文档和集合之间的关系。
  12. file:文件字段,存放上传的文档文件。
  13. description:提供文档描述的文本字段。
  14. created:记录文档创建时间的日期时间字段。
  15. modified:日期时间字段,记录文档的最后修改时间。

这些模型为文档集合以及使用 LlamaIndex 从文档集合创建的索引提供了坚实的基础。


4.3 Django 忍者 API

Django Ninja 是一个 Web 框架,用于使用 Django 和 Python 3.7+ 类型提示构建 API。
它提供了一种简单、直观且富有表现力的方式来定义 API 端点,利用 Python 的类型提示自动生成输入验证、序列化和文档。

在 Delphic 存储库中,该./config/api/endpoints.py 文件包含 API 端点的 API 路由和逻辑。
现在,让我们简要介绍一下文件中每个端点的用途endpoints.py

  1. /heartbeat:一个简单的 GET 端点,用于检查 API 是否已启动并正在运行。
    返回TrueAPI 是否可访问。
    这对于希望能够查询容器以确保其正常运行的 Kubernetes 设置很有帮助。

  2. /collections/create:一个 POST 端点,用于创建新的Collection.接受表单参数,例如titledescription和 的列表files
    为每个文件创建一个新Collection实例Document,并安排一个 Celery 任务来创建索引。

@collections_router.post("/create")
async def create_collection(
    request,
    title: str = Form(...),
    description: str = Form(...),
    files: list[UploadedFile] = File(...),
):
    key = None if getattr(request, "auth", None) is None else request.auth
    if key is not None:
        key = await key

    collection_instance = Collection(
        api_key=key,
        title=title,
        description=description,
        status=CollectionStatusEnum.QUEUED,
    )

    await sync_to_async(collection_instance.save)()

    for uploaded_file in files:
        doc_data = uploaded_file.file.read()
        doc_file = ContentFile(doc_data, uploaded_file.name)
        document = Document(collection=collection_instance, file=doc_file)
        await sync_to_async(document.save)()

    create_index.si(collection_instance.id).apply_async()

    return await sync_to_async(CollectionModelSchema)(...)

  1. /collections/query— 使用 LLM 查询文档集合的 POST 端点。
    接受包含collection_id和的 JSON 负载query_str,并返回通过查询集合生成的响应。
    我们实际上并不在聊天 GUI 中使用此端点(我们使用 websocket - 见下文),但您可以构建一个应用程序来集成到此 REST 端点以查询特定集合。
@collections_router.post(
    "/query",
    response=CollectionQueryOutput,
    summary="Ask a question of a document collection",
)
def query_collection_view(
    request: HttpRequest, query_input: CollectionQueryInput
):
    collection_id = query_input.collection_id
    query_str = query_input.query_str
    response = query_collection(collection_id, query_str)
    return {"response": response}

  1. /collections/available:一个 GET 端点,返回使用用户的 API 密钥创建的所有集合的列表。
    输出使用CollectionModelSchema.
@collections_router.get(
    "/available",
    response=list[CollectionModelSchema],
    summary="Get a list of all of the collections created with my api_key",
)
async def get_my_collections_view(request: HttpRequest):
    key = None if getattr(request, "auth", None) is None else request.auth
    if key is not None:
        key = await key

    collections = Collection.objects.filter(api_key=key)

    return [{...} async for collection in collections]

  1. /collections/{collection_id}/add_file:用于将文件添加到现有集合的 POST 端点。
    接受collection_id路径参数和表单参数,例如filedescription
    将文件添加为Document 与指定集合关联的实例。
@collections_router.post(
    "/{collection_id}/add_file", summary="Add a file to a collection"
)
async def add_file_to_collection(
    request,
    collection_id: int,
    file: UploadedFile = File(...),
    description: str = Form(...),
):
    collection = await sync_to_async(Collection.objects.get)(id=collection_id)

4.4 Websocket 简介

WebSocket 是一种通信协议,可通过单个长期连接在客户端和服务器之间实现双向和全双工通信。
WebSocket 协议设计为在与 HTTP 和 HTTPS 相同的端口(分别为端口 80 和 443)上工作,并使用类似的握手过程来建立连接。
一旦建立连接,数据就可以作为“帧”在两个方向上发送,而不需要每次都重新建立连接,这与传统的 HTTP 请求不同。

使用 WebSockets 有几个原因,特别是在处理需要很长时间加载到内存但加载后很快运行的代码时:

  1. 性能:WebSocket 消除了与每个请求打开和关闭多个连接相关的开销,从而减少了延迟。

  2. 效率:WebSocket 允许实时通信,无需轮询,从而更有效地利用资源和更好的响应能力。

  3. 可扩展性:WebSockets 可以处理大量并发连接,非常适合需要高并发的应用程序。

对于 Delphic 应用程序,使用 WebSocket 是有意义的,因为将 LLM 加载到内存中的成本可能很高。
通过建立 WebSocket 连接,LLM 可以保持加载在内存中,从而可以快速处理后续请求,而无需每次都重新加载模型。

ASGI 配置文件./config/asgi.py定义应用程序应如何处理传入连接,使用 Django ChannelsProtocolTypeRouter根据协议类型路由连接。
在本例中,我们有两种协议类型:“http”和“websocket”。

“http”协议类型使用标准 Django ASGI 应用程序来处理 HTTP 请求,而“websocket”协议类型使用自定义TokenAuthMiddleware来验证 WebSocket 连接。
URLRouterTokenAuthMiddleware定义了 的 URL 模式CollectionQueryConsumer,它负责处理与查询文档集合相关的 WebSocket 连接。

application = ProtocolTypeRouter(
    {
        "http": get_asgi_application(),
        "websocket": TokenAuthMiddleware(
            URLRouter(
                [
                    re_path(
                        r"ws/collections/(?P<collection_id>\w+)/query/$",
                        CollectionQueryConsumer.as_asgi(),
                    ),
                ]
            )
        ),
    }
)

此配置允许客户端与 Delphic 应用程序建立 WebSocket 连接,以使用 LLM 高效查询文档集合,而无需为每个请求重新加载模型。


4.5 Websocket 处理程序

CollectionQueryConsumer中的类负责config/api/websockets/queries.py处理与查询文档集合相关的 WebSocket 连接。
它继承自AsyncWebsocketConsumerDjango Channels提供的类。


该类CollectionQueryConsumer有三个主要方法:

  1. connect:当 WebSocket 在连接过程中握手时调用。
  2. disconnect:当 WebSocket 因任何原因关闭时调用。
  3. receive:当服务器从WebSocket接收到消息时调用。

Websocket连接监听器

connect方法负责建立连接、从连接路径中提取集合ID、加载集合模型并接受连接。

async def connect(self):
    try:
        self.collection_id = extract_connection_id(self.scope["path"])
        self.index = await load_collection_model(self.collection_id)
        await self.accept()

    except ValueError as e:
        await self.accept()
        await self.close(code=4000)
    except Exception as e:
        pass

Websocket 断开监听器

在这种情况下,该disconnect方法为空,因为关闭 WebSocket 时无需执行任何其他操作。


Websocket接收监听器

receive方法负责处理来自 WebSocket 的传入消息。
它获取传入消息,对其进行解码,然后使用提供的查询查询加载的集合模型。
然后,响应被格式化为 markdown 字符串,并通过 WebSocket 连接发送回客户端。

async def receive(self, text_data):
    text_data_json = json.loads(text_data)

    if self.index is not None:
        query_str = text_data_json["query"]
        modified_query_str = f"Please return a nicely formatted markdown string to this request:\n\n{query_str}"
        query_engine = self.index.as_query_engine()
        response = query_engine.query(modified_query_str)

        markdown_response = f"## Response\n\n{response}\n\n"
        if response.source_nodes:
            markdown_sources = (
                f"## Sources\n\n{response.get_formatted_sources()}"
            )
        else:
            markdown_sources = ""

        formatted_response = f"{markdown_response}{markdown_sources}"

        await self.send(json.dumps({"response": formatted_response}, indent=4))
    else:
        await self.send(
            json.dumps(
                {"error": "No index loaded for this connection."}, indent=4
            )
        )

要加载集合模型,load_collection_model需要使用该函数,该函数可以在 中找到delphic/utils/collections.py
此函数检索具有给定集合 ID 的集合对象,检查集合模型的 JSON 文件是否存在,如果不存在,则创建一个。
然后,它在加载之前设置LLM并使用缓存文件。
Settings``VectorStoreIndex

from llama_index.core import Settings

async def load_collection_model(collection_id: str | int) -> VectorStoreIndex:
    """
    Load the Collection model from cache or the database, and return the index.

    Args:
        collection_id (Union[str, int]): The ID of the Collection model instance.

    Returns:
        VectorStoreIndex: The loaded index.

    This function performs the following steps:
    1. Retrieve the Collection object with the given collection_id.
    2. Check if a JSON file with the name '/cache/model_{collection_id}.json' exists.
    3. If the JSON file doesn't exist, load the JSON from the Collection.model FileField and save it to
       '/cache/model_{collection_id}.json'.
    4. Call VectorStoreIndex.load_from_disk with the cache_file_path.
    """
    # Retrieve the Collection object
    collection = await Collection.objects.aget(id=collection_id)
    logger.info(f"load_collection_model() - loaded collection {collection_id}")

    # Make sure there's a model
    if collection.model.name:
        logger.info("load_collection_model() - Setup local json index file")

        # Check if the JSON file exists
        cache_dir = Path(settings.BASE_DIR) / "cache"
        cache_file_path = cache_dir / f"model_{collection_id}.json"
        if not cache_file_path.exists():
            cache_dir.mkdir(parents=True, exist_ok=True)
            with collection.model.open("rb") as model_file:
                with cache_file_path.open(
                    "w+", encoding="utf-8"
                ) as cache_file:
                    cache_file.write(model_file.read().decode("utf-8"))

        # define LLM
        logger.info(
            f"load_collection_model() - Setup Settings with tokens {settings.MAX_TOKENS} and "
            f"model {settings.MODEL_NAME}"
        )
        Settings.llm = OpenAI(
            temperature=0, model="gpt-3.5-turbo", max_tokens=512
        )

        # Call VectorStoreIndex.load_from_disk
        logger.info("load_collection_model() - Load llama index")
        index = VectorStoreIndex.load_from_disk(
            cache_file_path,
        )
        logger.info(
            "load_collection_model() - Llamaindex loaded and ready for query..."
        )

    else:
        logger.error(
            f"load_collection_model() - collection {collection_id} has no model!"
        )
        raise ValueError("No model exists for this collection!")

    return index

5、React 前端


5.1 概述

我们选择使用 TypeScript、React 和 Material-UI (MUI) 作为 Delphic 项目的前端有几个原因。
首先,作为最流行的前端框架(React)的最流行的组件库(MUI),这种选择使得这个项目可以被庞大的开发者社区所访问。
其次,React 在这一点上是一个稳定且普遍受欢迎的框架,它以虚拟 DOM 的形式提供有价值的抽象,同时仍然相对稳定,并且在我们看来,非常容易学习,再次使其易于访问。


5.2 前端项目结构

前端可以在/frontendrepo 目录中找到,React 相关组件位于/frontend/src.您会注意到该目录中有一个 DockerFile以及与配置我们的前端 Web 服务器 - nginxfrontend 相关的几个文件夹和文件。

/frontend/src/App.tsx文件充当应用程序的入口点。
它定义了主要组件,例如登录表单、抽屉布局和集合创建模式。
主要组件根据用户是否登录并拥有身份验证令牌有条件地呈现。

DrawerLayout2 组件在文件中定义DrawerLayour2.tsx
该组件管理应用程序的布局并提供导航和主要内容区域。

由于应用程序相对简单,因此我们可以不使用像 Redux 这样复杂的状态管理解决方案,而只使用 React 的 useState 钩子。


5.3 从后端抓取集合

登录用户可用的集合将被检索并显示在 DrawerLayout2 组件中。
该过程可以分为以下步骤:

  1. 初始化状态变量:
const [collections, setCollections] = useState<CollectionModelSchema[]>([]);
const [loading, setLoading] = useState(true);

在这里,我们初始化两个状态变量:collections存储集合列表并loading跟踪是否正在获取集合。

  1. 使用以下函数为登录用户获取集合fetchCollections()
const
fetchCollections = async () = > {
try {
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
const response = await getMyCollections(accessToken);
setCollections(response.data);
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};

该函数通过使用用户的访问令牌调用 API 函数来fetchCollections检索登录用户的集合。
getMyCollections然后,它collections使用检索到的数据更新状态,并将状态设置loadingfalse来指示获取已完成。


5.4 显示集合

最新的收藏品在抽屉中显示如下:

< List >
{collections.map((collection) = > (
    < div key={collection.id} >
    < ListItem disablePadding >
    < ListItemButton
    disabled={
    collection.status != = CollectionStatus.COMPLETE | |
    !collection.has_model
    }
    onClick={() = > handleCollectionClick(collection)}
selected = {
    selectedCollection & &
    selectedCollection.id == = collection.id
}
>
< ListItemText
primary = {collection.title} / >
          {collection.status == = CollectionStatus.RUNNING ? (
    < CircularProgress
    size={24}
    style={{position: "absolute", right: 16}}
    / >
): null}
< / ListItemButton >
    < / ListItem >
        < / div >
))}
< / List >

您会注意到,disabled集合的属性ListItemButton是根据集合的状态是否为“否”CollectionStatus.COMPLETE或集合是否没有模型 ( !collection.has_model) 来设置的。
如果其中任何一个条件为真,则该按钮将被禁用,从而防止用户选择不完整或无模型的集合。
当 CollectionStatus 为 RUNNING 时,我们还在按钮上显示一个加载轮。

在一个单独的useEffect钩子中,我们检查collections状态中是否有任何集合的状态为CollectionStatus.RUNNINGCollectionStatus.QUEUED
如果是这样,我们设置一个间隔,fetchCollections每 15 秒(15,000 毫秒)重复调用该函数来更新收集状态。
这样,应用程序会定期检查已完成的集合,并在处理完成后相应地更新 UI。

useEffect(() = > {
    let
interval: NodeJS.Timeout;
if (
    collections.some(
        (collection) = >
collection.status == = CollectionStatus.RUNNING | |
collection.status == = CollectionStatus.QUEUED
)
) {
    interval = setInterval(() = > {
    fetchCollections();
}, 15000);
}
return () = > clearInterval(interval);
}, [collections]);

5.5 聊天视图组件

ChatView中的组件负责frontend/src/chat/ChatView.tsx处理和显示聊天界面,以便用户与集合进行交互。
该组件建立 WebSocket 连接以与服务器实时通信,发送和接收消息。

该组件的主要特性ChatView包括:

  1. 建立并管理与服务器的 WebSocket 连接。

  2. 以类似聊天的格式显示来自用户和服务器的消息。

  3. 处理用户输入以将消息发送到服务器。

  4. 根据从服务器接收到的消息更新消息状态和 UI。

  5. 显示连接状态和错误,例如加载消息、连接到服务器或加载集合时遇到错误。

总之,所有这些使用户能够以非常流畅、低延迟的体验与他们选择的集合进行交互。


聊天 Websocket 客户端

组件中的WebSocket连接ChatView用于建立客户端和服务器之间的实时通信。
WebSocket 连接在组件中设置和管理ChatView如下:

首先,我们要初始化 WebSocket 引用:

const websocket = useRef(无效的);

websocket使用 创建引用,useRef该引用保存将用于通信的 WebSocket 对象。
useRef是 React 中的一个钩子,允许您创建在渲染过程中持续存在的可变引用对象。
当您需要保存对可变对象(例如 WebSocket 连接)的引用而不导致不必要的重新渲染时,它特别有用。

ChatView组件中,WebSocket 连接需要在组件的整个生命周期中建立和维护,并且当连接状态发生变化时不应该触发重新渲染。
通过使用useRef,您可以确保 WebSocket 连接保留为引用,并且组件仅在实际状态发生更改(例如更新消息或显示错误)时重新呈现。

setupWebsocket函数负责建立WebSocket连接并设置事件处理程序来处理不同的WebSocket事件。

总的来说,setupWebsocket 函数如下所示:

const setupWebsocket = () => {
  setConnecting(true);
  // Here, a new WebSocket object is created using the specified URL, which includes the
  // selected collection's ID and the user's authentication token.

  websocket.current = new WebSocket(
    `ws://localhost:8000/ws/collections/${selectedCollection.id}/query/?token=${authToken}`,
  );

  websocket.current.onopen = (event) => {
    //...
  };

  websocket.current.onmessage = (event) => {
    //...
  };

  websocket.current.onclose = (event) => {
    //...
  };

  websocket.current.onerror = (event) => {
    //...
  };

  return () => {
    websocket.current?.close();
  };
};

请注意,在很多地方,我们根据来自 Web 套接字客户端的信息触发 GUI 的更新。

当组件首次打开并且我们尝试建立连接时,onopen侦听器被触发。
在回调中,组件更新状态以反映连接已建立,所有先前的错误均已清除,并且没有消息正在等待响应:

websocket.current.onopen = (event) => {
  setError(false);
  setConnecting(false);
  setAwaitingMessage(false);

  console.log("WebSocket connected:", event);
};

onmessage当通过 WebSocket 连接从服务器接收到新消息时触发。
在回调中,解析接收到的数据并messages使用来自服务器的新消息更新状态:

websocket.current.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log("WebSocket message received:", data);
  setAwaitingMessage(false);

  if (data.response) {
    // Update the messages state with the new message from the server
    setMessages((prevMessages) => [
      ...prevMessages,
      {
        sender_id: "server",
        message: data.response,
        timestamp: new Date().toLocaleTimeString(),
      },
    ]);
  }
};

onclose当 WebSocket 连接关闭时触发。
在回调中,组件检查特定的关闭代码 ( 4000) 以显示警告 Toast 并相应地更新组件状态。
它还记录关闭事件:

websocket.current.onclose = (event) => {
  if (event.code === 4000) {
    toast.warning(
      "Selected collection's model is unavailable. Was it created properly?",
    );
    setError(true);
    setConnecting(false);
    setAwaitingMessage(false);
  }
  console.log("WebSocket closed:", event);
};

最后,onerror当 WebSocket 连接发生错误时触发。
在回调中,组件更新状态以反映错误并记录错误事件:

websocket.current.onerror = (event) => {
  setError(true);
  setConnecting(false);
  setAwaitingMessage(false);

  console.error("WebSocket error:", event);
};

渲染我们的聊天消息

ChatView组件中,布局是使用 CSS 样式和 Material-UI 组件确定的。
主要布局由一个带有flex显示器的容器和一个面向列的flexDirection.这确保了容器内的内容物垂直排列。

布局中有三个主要部分:

  1. 聊天消息区域:此部分占据了大部分可用空间,并显示用户和服务器之间交换的消息列表。
    它的overflow-y 设置为“auto”,允许在内容溢出可用空间时滚动。
    使用ChatMessage每条消息的组件和ChatMessageLoading在等待服务器响应时显示加载状态的组件来呈现消息。

  2. 分隔线:Material-UIDivider组件用于将聊天消息区域与输入区域分开,在两个部分之间创建清晰的视觉区别。

  3. 输入区域:此部分位于底部,允许用户键入和发送消息。
    它包含TextField来自 Material-UI 的组件,该组件设置为接受最多 2 行的多行输入。
    输入区域还包括Button发送消息的组件。
    用户可以单击“发送”按钮或按键盘上的“Enter”来发送消息。

组件中接受的用户输入ChatView是用户在TextField.该组件处理这些文本输入并通过 WebSocket 连接将它们发送到服务器。


6、部署


先决条件

要部署该应用程序,您需要安装 Docker 和 Docker Compose。
如果您使用的是 Ubuntu 或其他常见的 Linux 发行版,DigitalOcean 有一个很棒的 Docker 教程和另一个很棒的Docker Compose教程, 您可以遵循。
如果这些不适合您,请尝试官方 docker 文档。


构建和部署

该项目基于 django-cookiecutter,很容易将其部署在虚拟机上并配置为为特定域提供 HTTPs 流量。
然而,配置有些复杂——不是因为这个项目,而是配置证书、DNS 等的一个相当复杂的主题。

出于本指南的目的,我们只在本地运行。
也许我们会发布有关生产部署的指南。
同时,请查看Django Cookiecutter 项目文档 以了解入门信息。

本指南假设您的目标是启动并运行应用程序以供使用。
如果您想要开发,很可能您不想使用 —profiles fullstack 标志启动 compose 堆栈,而是希望使用节点开发服务器启动 React 前端。

要部署,首先克隆存储库:

git clone https://github.com/yourusername/delphic.git

切换到项目目录:

cd delphic

复制示例环境文件:

mkdir -p ./.envs/.local/
cp -a ./docs/sample_envs/local/.frontend ./frontend
cp -a ./docs/sample_envs/local/.django ./.envs/.local
cp -a ./docs/sample_envs/local/.postgres ./.envs/.local

编辑.django.postgres配置文件以包含您的 OpenAI API 密钥并为您的数据库用户设置唯一的密码。
您还可以在 .django 文件中设置响应令牌限制或切换要使用的 OpenAI 模型。
支持 GPT4,前提是您有权访问它。

使用以下标志构建 docker compose 堆栈--profiles fullstack

sudo docker-compose --profiles fullstack -f local.yml build

fullstack 标志指示 compose 从前端文件夹构建一个 docker 容器,该容器将与所有需要的后端容器一起启动。
然而,构建生产 React 容器需要很长时间,因此我们不建议您采用这种方式进行开发。
请按照项目 readme.md 中的说明进行开发环境设置说明。

最后,启动应用程序:

sudo docker-compose -f local.yml up

现在,localhost:3000在浏览器中访问以查看前端,并在本地使用 Delphic 应用程序。


7、使用应用程序


设置用户

为了实际使用该应用程序(目前,我们打算与未经身份验证的用户共享某些模型),您需要登录。
您可以使用超级用户或非超级用户。
无论哪种情况,都需要先使用控制台创建超级用户:

为什么要设置 Django 超级用户? Django超级用户拥有应用程序中的所有权限,可以管理系统的各个方面,包括创建、修改和删除用户、集合和其他数据。
设置超级用户允许您完全控制和管理应用程序。

如何创建 Django 超级用户:

1 运行以下命令创建超级用户:

sudo docker-compose -f local.yml 运行 django python manage.py createsuperuser

2 系统将提示您提供超级用户的用户名、电子邮件地址和密码。
输入所需信息。

如何使用 Django admin 创建其他用户:

  1. 按照部署说明在本地启动 Delphic 应用程序。
  2. http://localhost:8000/admin通过在浏览器中导航到 来访问 Django 管理界面。
  3. 使用您之前创建的超级用户凭据登录。
  4. 单击“身份验证和授权”部分下的“用户”。
  5. 单击右上角的“添加用户+”按钮。
  6. 输入新用户所需的信息,例如用户名和密码。
    单击“保存”创建用户。
  7. 要授予新用户额外的权限或使他们成为超级用户,请在用户列表中单击他们的用户名,向下滚动到“权限”部分,然后相应地配置他们的权限。
    保存您的更改。

三、如何构建聊天机器人

LlamaIndex 充当数据和大型语言模型 (LLM) 之间的桥梁,提供一个工具包,使您能够围绕数据建立查询接口,以执行各种任务,例如问答和摘要。

在本教程中,我们将引导您使用数据代理构建上下文增强的聊天机器人。
该代理由 LLM 提供支持,能够对您的数据智能执行任务。
最终结果是聊天机器人代理配备了 LlamaIndex 提供的一组强大的数据接口工具,用于回答有关您的数据的查询。

注意:本教程建立在通过 SEC 10-K 文件创建查询接口的初步工作基础上 -请在此处查看


1、语境

在本指南中,我们将构建一个“10-K 聊天机器人”,它使用来自 Dropbox 的原始 UBER 10-K HTML 文件。
用户可以与聊天机器人交互,询问与 10-K 文件相关的问题。


2、准备

import os
import openai

os.environ["OPENAI_API_KEY"] = "sk-..."
openai.api_key = os.environ["OPENAI_API_KEY"]

import nest_asyncio

nest_asyncio.apply()

3、摄取数据

我们首先下载 2019-2022 年的原始 10-k 文件。

# NOTE: the code examples assume you're operating within a Jupyter notebook.
# download files
!mkdir data
!wget "https://www.dropbox.com/s/948jr9cfs7fgj99/UBER.zip?dl=1" -O data/UBER.zip
!unzip data/UBER.zip -d data

要将 HTML 文件解析为格式化文本,我们使用非结构化库。
感谢LlamaHub,我们可以直接与 Unstructed 集成,允许将任何文本转换为 LlamaIndex 可以摄取的文档格式。

首先我们安装必要的软件包:

!pip install llama-hub unstructured

然后我们可以使用 将UnstructuredReaderHTML 文件解析为对象列表Document

from llama_index.readers.file import UnstructuredReader
from pathlib import Path

years = [2022, 2021, 2020, 2019]

loader = UnstructuredReader()
doc_set = {}
all_docs = []
for year in years:
    year_docs = loader.load_data(
        file=Path(f"./data/UBER/UBER_{year}.html"), split_documents=False
    )
    # insert year metadata into each year
    for d in year_docs:
        d.metadata = {"year": year}
    doc_set[year] = year_docs
    all_docs.extend(year_docs)

4、为每年设置向量索引

我们首先为每年设置一个向量索引。
每个向量索引都允许我们询问有关给定年份的 10-K 归档的问题。

我们构建每个索引并将其保存到磁盘。

# initialize simple vector indices
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.core import Settings

Settings.chunk_size = 512
index_set = {}
for year in years:
    storage_context = StorageContext.from_defaults()
    cur_index = VectorStoreIndex.from_documents(
        doc_set[year],
        storage_context=storage_context,
    )
    index_set[year] = cur_index
    storage_context.persist(persist_dir=f"./storage/{year}")

要从磁盘加载索引,请执行以下操作

# Load indices from disk
from llama_index.core import load_index_from_storage

index_set = {}
for year in years:
    storage_context = StorageContext.from_defaults(
        persist_dir=f"./storage/{year}"
    )
    cur_index = load_index_from_storage(
        storage_context,
    )
    index_set[year] = cur_index

5、设置子问题查询引擎以综合 10-K 份申请的答案

由于我们可以访问 4 年的文件,因此我们可能不仅想提出有关特定年份的 10-K 文件的问题,而且还提出需要对所有 10-K 文件进行分析的问题。

为了解决这个问题,我们可以使用子问题查询引擎
它将查询分解为子查询,每个子查询由一个单独的向量索引回答,并综合结果以回答整个查询。

LlamaIndex 提供了一些索引(和查询引擎)的包装器,以便查询引擎和代理可以使用它们。
首先我们QueryEngineTool为每个向量索引定义一个。
每个工具都有一个名称和描述;这些是LLM agent所看到的,以决定选择哪种工具。

from llama_index.core.tools import QueryEngineTool, ToolMetadata

individual_query_engine_tools = [
    QueryEngineTool(
        query_engine=index_set[year].as_query_engine(),
        metadata=ToolMetadata(
            name=f"vector_index_{year}",
            description=f"useful for when you want to answer queries about the {year} SEC 10-K for Uber",
        ),
    )
    for year in years
]

现在我们可以创建子问题查询引擎,这将使我们能够综合 10-K 文件的答案。
我们传入individual_query_engine_tools上面定义的 以及llm将用于运行子查询的 。

from llama_index.llms.openai import OpenAI
from llama_index.core.query_engine import SubQuestionQueryEngine

query_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=individual_query_engine_tools,
    llm=OpenAI(model="gpt-3.5-turbo"),
)

6、设置聊天机器人代理

我们使用 LlamaIndex 数据代理来设置外部聊天机器人代理,它可以访问一组工具。
具体来说,我们将使用 OpenAIAgent,它利用 OpenAI API 函数调用。
我们想要使用之前为每个索引(对应于给定年份)定义的单独工具,以及我们上面定义的子问题查询引擎的工具。

首先我们QueryEngineTool为子问题定义一个查询引擎:

query_engine_tool = QueryEngineTool(
    query_engine=query_engine,
    metadata=ToolMetadata(
        name="sub_question_query_engine",
        description="useful for when you want to answer queries that require analyzing multiple SEC 10-K documents for Uber",
    ),
)

然后,我们将上面定义的工具组合成代理的单个工具列表:

tools = individual_query_engine_tools + [query_engine_tool]

最后,我们调用OpenAIAgent.from_tools创建代理,传入我们上面定义的工具列表。

from llama_index.agent.openai import OpenAIAgent

agent = OpenAIAgent.from_tools(tools, verbose=True)

7、测试代理

我们现在可以使用各种查询来测试代理。

如果我们使用简单的“hello”查询对其进行测试,则代理不会使用任何工具。

response = agent.chat("hi, i am bob")
print(str(response))
Hello Bob! How can I assist you today?

如果我们使用有关给定年份 10-k 的查询来测试它,代理将使用相关的向量索引工具。

response = agent.chat(
    "What were some of the biggest risk factors in 2020 for Uber?"
)
print(str(response))
=== Calling Function ===
Calling function: vector_index_2020 with args: {
  "input": "biggest risk factors"
}
Got output: The biggest risk factors mentioned in the context are:
1. The adverse impact of the COVID-19 pandemic and actions taken to mitigate it on the business.
2. The potential reclassification of drivers as employees, workers, or quasi-employees instead of independent contractors.
3. Intense competition in the mobility, delivery, and logistics industries, with low-cost alternatives and well-capitalized competitors.
4. The need to lower fares or service fees and offer driver incentives and consumer discounts to remain competitive.
5. Significant losses incurred and the uncertainty of achieving profitability.
6. The risk of not attracting or maintaining a critical mass of platform users.
7. Operational, compliance, and cultural challenges related to the workplace culture and forward-leaning approach.
8. The potential negative impact of international investments and the challenges of conducting business in foreign countries.
9. Risks associated with operational and compliance challenges, localization, laws and regulations, competition, social acceptance, technological compatibility, improper business practices, liability uncertainty, managing international operations, currency fluctuations, cash transactions, tax consequences, and payment fraud.
========================
Some of the biggest risk factors for Uber in 2020 were:

1. The adverse impact of the COVID-19 pandemic and actions taken to mitigate it on the business.
2. The potential reclassification of drivers as employees, workers, or quasi-employees instead of independent contractors.
3. Intense competition in the mobility, delivery, and logistics industries, with low-cost alternatives and well-capitalized competitors.
4. The need to lower fares or service fees and offer driver incentives and consumer discounts to remain competitive.
5. Significant losses incurred and the uncertainty of achieving profitability.
6. The risk of not attracting or maintaining a critical mass of platform users.
7. Operational, compliance, and cultural challenges related to the workplace culture and forward-leaning approach.
8. The potential negative impact of international investments and the challenges of conducting business in foreign countries.
9. Risks associated with operational and compliance challenges, localization, laws and regulations, competition, social acceptance, technological compatibility, improper business practices, liability uncertainty, managing international operations, currency fluctuations, cash transactions, tax consequences, and payment fraud.

These risk factors highlight the challenges and uncertainties that Uber faced in 2020.

最后,如果我们使用查询来测试它以比较/对比多年来的风险因素,代理将使用子问题查询引擎工具。

cross_query_str = "Compare/contrast the risk factors described in the Uber 10-K across years. Give answer in bullet points."

response = agent.chat(cross_query_str)
print(str(response))
=== Calling Function ===
Calling function: sub_question_query_engine with args: {
  "input": "Compare/contrast the risk factors described in the Uber 10-K across years"
}
Generated 4 sub questions.
[vector_index_2022] Q: What are the risk factors described in the 2022 SEC 10-K for Uber?
[vector_index_2021] Q: What are the risk factors described in the 2021 SEC 10-K for Uber?
[vector_index_2020] Q: What are the risk factors described in the 2020 SEC 10-K for Uber?
[vector_index_2019] Q: What are the risk factors described in the 2019 SEC 10-K for Uber?
[vector_index_2021] A: The risk factors described in the 2021 SEC 10-K for Uber include the adverse impact of the COVID-19 pandemic on their business, the potential reclassification of drivers as employees instead of independent contractors, intense competition in the mobility, delivery, and logistics industries, the need to lower fares and offer incentives to remain competitive, significant losses incurred by the company, the importance of attracting and maintaining a critical mass of platform users, and the ongoing legal challenges regarding driver classification.
[vector_index_2020] A: The risk factors described in the 2020 SEC 10-K for Uber include the adverse impact of the COVID-19 pandemic on their business, the potential reclassification of drivers as employees instead of independent contractors, intense competition in the mobility, delivery, and logistics industries, the need to lower fares and offer incentives to remain competitive, significant losses and the uncertainty of achieving profitability, the importance of attracting and retaining a critical mass of drivers and users, and the challenges associated with their workplace culture and operational compliance.
[vector_index_2022] A: The risk factors described in the 2022 SEC 10-K for Uber include the potential adverse effect on their business if drivers were classified as employees instead of independent contractors, the highly competitive nature of the mobility, delivery, and logistics industries, the need to lower fares or service fees to remain competitive in certain markets, the company's history of significant losses and the expectation of increased operating expenses in the future, and the potential impact on their platform if they are unable to attract or maintain a critical mass of drivers, consumers, merchants, shippers, and carriers.
[vector_index_2019] A: The risk factors described in the 2019 SEC 10-K for Uber include the loss of their license to operate in London, the complexity of their business and operating model due to regulatory uncertainties, the potential for additional regulations for their other products in the Other Bets segment, the evolving laws and regulations regarding the development and deployment of autonomous vehicles, and the increasing number of data protection and privacy laws around the world. Additionally, there are legal proceedings, litigation, claims, and government investigations that Uber is involved in, which could impose a burden on management and employees and come with defense costs or unfavorable rulings.
Got output: The risk factors described in the Uber 10-K reports across the years include the potential reclassification of drivers as employees instead of independent contractors, intense competition in the mobility, delivery, and logistics industries, the need to lower fares and offer incentives to remain competitive, significant losses incurred by the company, the importance of attracting and maintaining a critical mass of platform users, and the ongoing legal challenges regarding driver classification. Additionally, there are specific risk factors mentioned in each year's report, such as the adverse impact of the COVID-19 pandemic in 2020 and 2021, the loss of their license to operate in London in 2019, and the evolving laws and regulations regarding autonomous vehicles in 2019. Overall, while there are some similarities in the risk factors mentioned, there are also specific factors that vary across the years.
========================
=== Calling Function ===
Calling function: vector_index_2022 with args: {
  "input": "risk factors"
}
Got output: Some of the risk factors mentioned in the context include the potential adverse effect on the business if drivers were classified as employees instead of independent contractors, the highly competitive nature of the mobility, delivery, and logistics industries, the need to lower fares or service fees to remain competitive, the company's history of significant losses and the expectation of increased operating expenses, the impact of future pandemics or disease outbreaks on the business and financial results, and the potential harm to the business due to economic conditions and their effect on discretionary consumer spending.
========================
=== Calling Function ===
Calling function: vector_index_2021 with args: {
  "input": "risk factors"
}
Got output: The COVID-19 pandemic and the impact of actions to mitigate the pandemic have adversely affected and may continue to adversely affect parts of our business. Our business would be adversely affected if Drivers were classified as employees, workers or quasi-employees instead of independent contractors. The mobility, delivery, and logistics industries are highly competitive, with well-established and low-cost alternatives that have been available for decades, low barriers to entry, low switching costs, and well-capitalized competitors in nearly every major geographic region. To remain competitive in certain markets, we have in the past lowered, and may continue to lower, fares or service fees, and we have in the past offered, and may continue to offer, significant Driver incentives and consumer discounts and promotions. We have incurred significant losses since inception, including in the United States and other major markets. We expect our operating expenses to increase significantly in the foreseeable future, and we may not achieve or maintain profitability. If we are unable to attract or maintain a critical mass of Drivers, consumers, merchants, shippers, and carriers, whether as a result of competition or other factors, our platform will become less appealing to platform users.
========================
=== Calling Function ===
Calling function: vector_index_2020 with args: {
  "input": "risk factors"
}
Got output: The risk factors mentioned in the context include the adverse impact of the COVID-19 pandemic on the business, the potential reclassification of drivers as employees, the highly competitive nature of the mobility, delivery, and logistics industries, the need to lower fares or service fees to remain competitive, the company's history of significant losses and potential future expenses, the importance of attracting and maintaining a critical mass of platform users, and the operational and cultural challenges faced by the company.
========================
=== Calling Function ===
Calling function: vector_index_2019 with args: {
  "input": "risk factors"
}
Got output: The risk factors mentioned in the context include competition with local companies, differing levels of social acceptance, technological compatibility issues, exposure to improper business practices, legal uncertainty, difficulties in managing international operations, fluctuations in currency exchange rates, regulations governing local currencies, tax consequences, financial accounting burdens, difficulties in implementing financial systems, import and export restrictions, political and economic instability, public health concerns, reduced protection for intellectual property rights, limited influence over minority-owned affiliates, and regulatory complexities. These risk factors could adversely affect the international operations, business, financial condition, and operating results of the company.
========================
Here is a comparison of the risk factors described in the Uber 10-K reports across years:

2022 Risk Factors:
- Potential adverse effect if drivers were classified as employees instead of independent contractors.
- Highly competitive nature of the mobility, delivery, and logistics industries.
- Need to lower fares or service fees to remain competitive.
- History of significant losses and expectation of increased operating expenses.
- Impact of future pandemics or disease outbreaks on the business and financial results.
- Potential harm to the business due to economic conditions and their effect on discretionary consumer spending.

2021 Risk Factors:
- Adverse impact of the COVID-19 pandemic and actions to mitigate it on the business.
- Potential reclassification of drivers as employees instead of independent contractors.
- Highly competitive nature of the mobility, delivery, and logistics industries.
- Need to lower fares or service fees and offer incentives to remain competitive.
- History of significant losses and uncertainty of achieving profitability.
- Importance of attracting and maintaining a critical mass of platform users.

2020 Risk Factors:
- Adverse impact of the COVID-19 pandemic on the business.
- Potential reclassification of drivers as employees.
- Highly competitive nature of the mobility, delivery, and logistics industries.
- Need to lower fares or service fees to remain competitive.
- History of significant losses and potential future expenses.
- Importance of attracting and maintaining a critical mass of platform users.
- Operational and cultural challenges faced by the company.

2019 Risk Factors:
- Competition with local companies.
- Differing levels of social acceptance.
- Technological compatibility issues.
- Exposure to improper business practices.
- Legal uncertainty.
- Difficulties in managing international operations.
- Fluctuations in currency exchange rates.
- Regulations governing local currencies.
- Tax consequences.
- Financial accounting burdens.
- Difficulties in implementing financial systems.
- Import and export restrictions.
- Political and economic instability.
- Public health concerns.
- Reduced protection for intellectual property rights.
- Limited influence over minority-owned affiliates.
- Regulatory complexities.

These comparisons highlight both common and unique risk factors that Uber faced in different years.

8、设置聊天机器人循环

现在我们已经设置了聊天机器人,只需几个步骤即可设置基本的交互循环来与我们的 SEC 增强聊天机器人聊天!

agent = OpenAIAgent.from_tools(tools)  # verbose=False by default

while True:
    text_input = input("User: ")
    if text_input == "exit":
        break
    response = agent.chat(text_input)
    print(f"Agent: {response}")

下面是循环的运行示例:

User:  What were some of the legal proceedings against Uber in 2022?
Agent: In 2022, Uber faced several legal proceedings. Some of the notable ones include:

1. Petition against Proposition 22: A petition was filed in California alleging that Proposition 22, which classifies app-based drivers as independent contractors, is unconstitutional.

2. Lawsuit by Massachusetts Attorney General: The Massachusetts Attorney General filed a lawsuit against Uber, claiming that drivers should be classified as employees and entitled to protections under wage and labor laws.

3. Allegations by New York Attorney General: The New York Attorney General made allegations against Uber regarding the misclassification of drivers and related employment violations.

4. Swiss social security rulings: Swiss social security rulings classified Uber drivers as employees, which could have implications for Uber's operations in Switzerland.

5. Class action lawsuits in Australia: Uber faced class action lawsuits in Australia, with allegations that the company conspired to harm participants in the taxi, hire-car, and limousine industries.

It's important to note that the outcomes of these legal proceedings are uncertain and may vary.

User:

9、Notebook

看看我们相应的Notebook


四、术语和定义提取指南

Llama Index 有许多有据可查的用例(语义搜索、摘要等)。
然而,这并不意味着我们不能将 Llama Index 应用于非常具体的用例!

在本教程中,我们将介绍使用 Llama Index 从文本中提取术语和定义的设计过程,同时允许用户稍后查询这些术语。
使用Streamlit,我们可以提供一种简单的方法来构建前端来运行和测试所有这些,并快速迭代我们的设计。

本教程假设您已经安装了 Python3.9+ 和以下软件包:

  • llama-index
  • streamlit

在基础层面,我们的目标是从文档中获取文本,提取术语和定义,然后为用户提供一种查询术语和定义知识库的方法。
本教程将介绍 Llama Index 和 Streamlit 的功能,并希望为出现的常见问题提供一些有趣的解决方案。

本教程的最终版本可以在此处找到,并且Huggingface Spaces上提供了实时托管演示。


1、上传文本

第一步是为用户提供上传文档的方法。
让我们使用 Streamlit 编写一些代码来为此提供接口!使用以下代码并使用 启动应用程序streamlit run app.py

import streamlit as st

st.title("🦙 Llama Index Term Extractor 🦙")

document_text = st.text_area("Or enter raw text")
if st.button("Extract Terms and Definitions") and document_text:
    with st.spinner("Extracting..."):
        extracted_terms = document_text  # this is a placeholder!
    st.write(extracted_terms)

超级简单对不对!但您会注意到该应用程序还没有做任何有用的事情。
要使用 llama_index,我们还需要设置 OpenAI LLM。
LLM 有很多可能的设置,因此我们可以让用户找出最好的设置。
我们还应该让用户设置提取术语的提示(这也将帮助我们调试最有效的方法)。


2、LLM设置

下一步将向我们的应用程序引入一些选项卡,将其分为提供不同功能的不同窗格。
让我们为 LLM 设置和上传文本创建一个选项卡:

import os
import streamlit as st

DEFAULT_TERM_STR = (
    "Make a list of terms and definitions that are defined in the context, "
    "with one pair on each line. "
    "If a term is missing it's definition, use your best judgment. "
    "Write each line as as follows:\nTerm: <term> Definition: <definition>"
)

st.title("🦙 Llama Index Term Extractor 🦙")

setup_tab, upload_tab = st.tabs(["Setup", "Upload/Extract Terms"])

with setup_tab:
    st.subheader("LLM Setup")
    api_key = st.text_input("Enter your OpenAI API key here", type="password")
    llm_name = st.selectbox(
        "Which LLM?", ["text-davinci-003", "gpt-3.5-turbo", "gpt-4"]
    )
    model_temperature = st.slider(
        "LLM Temperature", min_value=0.0, max_value=1.0, step=0.1
    )
    term_extract_str = st.text_area(
        "The query to extract terms and definitions with.",
        value=DEFAULT_TERM_STR,
    )

with upload_tab:
    st.subheader("Extract and Query Definitions")
    document_text = st.text_area("Or enter raw text")
    if st.button("Extract Terms and Definitions") and document_text:
        with st.spinner("Extracting..."):
            extracted_terms = document_text  # this is a placeholder!
        st.write(extracted_terms)

现在我们的应用程序有两个选项卡,这对组织确实有帮助。
您还会注意到我添加了一个提取术语的默认提示 - 当您尝试提取某些术语时,您可以稍后更改它,这只是我在尝试了一下之后得到的提示。

说到提取术语,是时候添加一些函数来做到这一点了!


3、提取和存储术语

现在我们能够定义 LLM 设置并上传文本,我们可以尝试使用 Llama Index 从文本中提取术语!

我们可以添加以下函数来初始化我们的 LLM,并使用它从输入文本中提取术语。

from llama_index.core import Document, SummaryIndex, load_index_from_storage
from llama_index.llms.openai import OpenAI
from llama_index.core import Settings

def get_llm(llm_name, model_temperature, api_key, max_tokens=256):
    os.environ["OPENAI_API_KEY"] = api_key
    return OpenAI(
        temperature=model_temperature, model=llm_name, max_tokens=max_tokens
    )

def extract_terms(
    documents, term_extract_str, llm_name, model_temperature, api_key
):
    llm = get_llm(llm_name, model_temperature, api_key, max_tokens=1024)

    temp_index = SummaryIndex.from_documents(
        documents,
    )
    query_engine = temp_index.as_query_engine(
        response_mode="tree_summarize", llm=llm
    )
    terms_definitions = str(query_engine.query(term_extract_str))
    terms_definitions = [
        x
        for x in terms_definitions.split("\n")
        if x and "Term:" in x and "Definition:" in x
    ]
    # parse the text into a dict
    terms_to_definition = {
        x.split("Definition:")[0]
        .split("Term:")[-1]
        .strip(): x.split("Definition:")[-1]
        .strip()
        for x in terms_definitions
    }
    return terms_to_definition

现在,使用新函数,我们终于可以提取我们的术语了!

...
with upload_tab:
    st.subheader("Extract and Query Definitions")
    document_text = st.text_area("Or enter raw text")
    if st.button("Extract Terms and Definitions") and document_text:
        with st.spinner("Extracting..."):
            extracted_terms = extract_terms(
                [Document(text=document_text)],
                term_extract_str,
                llm_name,
                model_temperature,
                api_key,
            )
        st.write(extracted_terms)

现在发生了很多事情,让我们花点时间回顾一下正在发生的事情。

get_llm()正在根据设置选项卡中的用户配置实例化 LLM。
根据模型名称,我们需要使用适当的类(OpenAIvs. ChatOpenAI)。

extract_terms()是所有美好事物发生的地方。
首先,我们调用get_llm()with max_tokens=1024,因为我们不想在提取术语和定义时过多限制模型(如果未设置,默认值为 256)。
然后,我们定义Settomgs对象,num_output与我们的max_tokens值对齐,并将块大小设置为不大于输出。
当文档通过 Llama Index 建立索引时,如果文档很大,它们会被分成块(也称为节点),并chunk_size设置这些块的大小。

接下来,我们创建一个临时摘要索引并传入我们的 llm。
摘要索引将读取索引中的每一段文本,这非常适合提取术语。
最后,我们使用预定义的查询文本来提取术语,使用response_mode="tree_summarize.此响应模式将从下至上生成一个摘要树,其中每个父级都会总结其子级。
最后,返回树的顶部,其中将包含我们提取的所有术语和定义。

最后,我们进行一些小的后期处理。
我们假设模型遵循指令并在每行上放置一个术语/定义对。
如果一行缺少Term:Definition:标签,我们会跳过它。
然后,我们将其转换为字典以便于存储!


4、保存提取的术语

现在我们可以提取术语,我们需要将它们放在某个地方,以便稍后查询它们。
VectorStoreIndex目前A应该是一个完美的选择!但此外,我们的应用程序还应该跟踪哪些术语插入到索引中,以便我们稍后检查它们。
使用st.session_state,我们可以将当前的术语列表存储在会话字典中,对于每个用户都是唯一的!

首先,让我们添加一个功能来初始化全局向量索引,并添加另一个函数来插入提取的术语。

from llama_index.core import Settings

...
if "all_terms" not in st.session_state:
    st.session_state["all_terms"] = DEFAULT_TERMS
...

def insert_terms(terms_to_definition):
    for term, definition in terms_to_definition.items():
        doc = Document(text=f"Term: {term}\nDefinition: {definition}")
        st.session_state["llama_index"].insert(doc)

@st.cache_resource
def initialize_index(llm_name, model_temperature, api_key):
    """Create the VectorStoreIndex object."""
    Settings.llm = get_llm(llm_name, model_temperature, api_key)

    index = VectorStoreIndex([])

    return index, llm

...

with upload_tab:
    st.subheader("Extract and Query Definitions")
    if st.button("Initialize Index and Reset Terms"):
        st.session_state["llama_index"] = initialize_index(
            llm_name, model_temperature, api_key
        )
        st.session_state["all_terms"] = {}

    if "llama_index" in st.session_state:
        st.markdown(
            "Either upload an image/screenshot of a document, or enter the text manually."
        )
        document_text = st.text_area("Or enter raw text")
        if st.button("Extract Terms and Definitions") and (
            uploaded_file or document_text
        ):
            st.session_state["terms"] = {}
            terms_docs = {}
            with st.spinner("Extracting..."):
                terms_docs.update(
                    extract_terms(
                        [Document(text=document_text)],
                        term_extract_str,
                        llm_name,
                        model_temperature,
                        api_key,
                    )
                )
            st.session_state["terms"].update(terms_docs)

        if "terms" in st.session_state and st.session_state["terms"]:
            st.markdown("Extracted terms")
            st.json(st.session_state["terms"])

            if st.button("Insert terms?"):
                with st.spinner("Inserting terms"):
                    insert_terms(st.session_state["terms"])
                st.session_state["all_terms"].update(st.session_state["terms"])
                st.session_state["terms"] = {}
                st.experimental_rerun()

现在您真正开始利用 Streamlit 的力量了!让我们从上传选项卡下的代码开始。
我们添加了一个按钮来初始化向量索引,并将其存储在全局 Streamlit 状态字典中,并重置当前提取的术语。
然后,从输入文本中提取术语后,我们再次将提取的术语存储在全局状态中,并让用户有机会在插入之前查看它们。
如果按下插入按钮,则我们调用插入术语函数,更新插入术语的全局跟踪,并从会话状态中删除最近提取的术语。


5、查询提取的术语/定义

提取并保存了术语和定义后,我们如何使用它们?用户如何记住以前保存的内容?我们可以简单地向应用程序添加更多选项卡来处理这些功能。

...
setup_tab, terms_tab, upload_tab, query_tab = st.tabs(
    ["Setup", "All Terms", "Upload/Extract Terms", "Query Terms"]
)
...
with terms_tab:
    with terms_tab:
        st.subheader("Current Extracted Terms and Definitions")
        st.json(st.session_state["all_terms"])
...
with query_tab:
    st.subheader("Query for Terms/Definitions!")
    st.markdown(
        (
            "The LLM will attempt to answer your query, and augment it's answers using the terms/definitions you've inserted. "
            "If a term is not in the index, it will answer using it's internal knowledge."
        )
    )
    if st.button("Initialize Index and Reset Terms", key="init_index_2"):
        st.session_state["llama_index"] = initialize_index(
            llm_name, model_temperature, api_key
        )
        st.session_state["all_terms"] = {}

    if "llama_index" in st.session_state:
        query_text = st.text_input("Ask about a term or definition:")
        if query_text:
            query_text = (
                query_text
                + "\nIf you can't find the answer, answer the query with the best of your knowledge."
            )
            with st.spinner("Generating answer..."):
                response = st.session_state["llama_index"].query(
                    query_text, similarity_top_k=5, response_mode="compact"
                )
            st.markdown(str(response))

虽然这主要是基本的,但需要注意一些重要事项:

  • 我们的初始化按钮与其他按钮具有相同的文本。
    Streamlit 会抱怨这一点,因此我们提供了一个唯一的密钥。
  • 一些附加文本已添加到查询中!这是为了尝试弥补索引没有答案的情况。
  • 在我们的索引查询中,我们指定了两个选项:
  • similarity_top_k=5表示索引将获取与查询最接近的前 5 个术语/定义。
  • response_mode="compact"意味着每次 LLM 调用中将使用 5 个匹配术语/定义中尽可能多的文本。
    如果没有这个,索引将至少对 LLM 进行 5 次调用,这可能会减慢用户的速度。

6、试运行测试

好吧,实际上我希望您在我们进行测试时一直在进行测试。
但现在,让我们尝试一次完整的测试。

  1. 刷新应用程序
  2. 输入您的 LLM 设置
  3. 转到查询选项卡
  4. 问以下问题:What is a bunnyhug?
  5. 该应用程序应该给出一些无意义的响应。
    如果您不知道,bunnyhug 是连帽衫的另一种说法,由来自加拿大大草原的人们使用!
  6. 让我们将此定义添加到应用程序中。
    打开上传选项卡并输入以下文本:A bunnyhug is a common term used to describe a hoodie. This term is used by people from the Canadian Prairies.
  7. 单击提取按钮。
    片刻之后,应用程序应该显示正确提取的术语/定义。
    单击插入术语按钮保存它!
  8. 如果我们打开术语选项卡,我们刚刚提取的术语和定义应该显示
  9. 返回查询选项卡并尝试询问 bunnyhug 是什么。
    现在,答案应该是正确的!

改进1 - 创建起始索引

当我们的基础应用程序运行时,建立一个有用的索引可能会感觉需要做很多工作。
如果我们为用户提供某种起点来展示应用程序的查询功能会怎么样?我们就能做到!首先,让我们对应用程序进行一个小更改,以便在每次上传后将索引保存到磁盘:

def insert_terms(terms_to_definition):
    for term, definition in terms_to_definition.items():
        doc = Document(text=f"Term: {term}\nDefinition: {definition}")
        st.session_state["llama_index"].insert(doc)
    # TEMPORARY - save to disk
    st.session_state["llama_index"].storage_context.persist()

现在,我们需要一些文档来提取!该项目的存储库使用了纽约市的维基百科页面,您可以在此处找到文本。

如果您将文本粘贴到上传选项卡中并运行它(可能需要一些时间),我们就可以插入提取的术语。
在插入索引之前,请务必将提取的术语的文本复制到记事本或类似工具中!我们很快就会需要它们。

插入后,删除我们用于将索引保存到磁盘的代码行。
现在保存了起始索引,我们可以将initialize_index函数修改为如下所示:

@st.cache_resource
def initialize_index(llm_name, model_temperature, api_key):
    """Load the Index object."""
    Settings.llm = get_llm(llm_name, model_temperature, api_key)

    index = load_index_from_storage(storage_context)

    return index

您是否记得将提取的术语的巨大列表保存在记事本中?现在,当我们的应用程序初始化时,我们希望将索引中的默认术语传递到全局术语状态:

...
if "all_terms" not in st.session_state:
    st.session_state["all_terms"] = DEFAULT_TERMS
...

在我们之前重置值的任何地方重复上述操作all_terms


改进#2 -(精炼)更好的提示

如果您现在稍微玩一下该应用程序,您可能会注意到它停止遵循我们的提示!请记住,我们在query_str变量中添加了以下内容:如果无法找到术语/定义,请尽其所能地回答。
但现在,如果您尝试询问随机术语(例如 bunnyhug!),它可能会也可能不会遵循这些说明。

这是由于 Llama Index 中“精炼”答案的概念。
由于我们正在查询前 5 个匹配结果,有时所有结果并不适合单个提示! OpenAI 模型的最大输入大小通常为 4097 个令牌。
因此,Llama Index 通过将匹配结果分解为适合提示的块来解决此问题。
Llama Index 从第一个 API 调用获得初始答案后,会将下一个块与之前的答案一起发送到 API,并要求模型完善该答案。

所以,精炼过程似乎扰乱了我们的结果!与其向 附加额外的指令query_str,不如删除它,Llama Index 将让我们提供自己的自定义提示!现在让我们使用默认提示聊天特定提示作为指导来创建它们。
使用新文件constants.py,让我们创建一些新的查询模板:

from llama_index.core import (
    PromptTemplate,
    SelectorPromptTemplate,
    ChatPromptTemplate,
)
from llama_index.core.prompts.utils import is_chat_model
from llama_index.core.llms import ChatMessage, MessageRole

# Text QA templates
DEFAULT_TEXT_QA_PROMPT_TMPL = (
    "Context information is below. \n"
    "---------------------\n"
    "{context_str}"
    "\n---------------------\n"
    "Given the context information answer the following question "
    "(if you don't know the answer, use the best of your knowledge): {query_str}\n"
)
TEXT_QA_TEMPLATE = PromptTemplate(DEFAULT_TEXT_QA_PROMPT_TMPL)

# Refine templates
DEFAULT_REFINE_PROMPT_TMPL = (
    "The original question is as follows: {query_str}\n"
    "We have provided an existing answer: {existing_answer}\n"
    "We have the opportunity to refine the existing answer "
    "(only if needed) with some more context below.\n"
    "------------\n"
    "{context_msg}\n"
    "------------\n"
    "Given the new context and using the best of your knowledge, improve the existing answer. "
    "If you can't improve the existing answer, just repeat it again."
)
DEFAULT_REFINE_PROMPT = PromptTemplate(DEFAULT_REFINE_PROMPT_TMPL)

CHAT_REFINE_PROMPT_TMPL_MSGS = [
    ChatMessage(content="{query_str}", role=MessageRole.USER),
    ChatMessage(content="{existing_answer}", role=MessageRole.ASSISTANT),
    ChatMessage(
        content="We have the opportunity to refine the above answer "
        "(only if needed) with some more context below.\n"
        "------------\n"
        "{context_msg}\n"
        "------------\n"
        "Given the new context and using the best of your knowledge, improve the existing answer. "
        "If you can't improve the existing answer, just repeat it again.",
        role=MessageRole.USER,
    ),
]

CHAT_REFINE_PROMPT = ChatPromptTemplate(CHAT_REFINE_PROMPT_TMPL_MSGS)

# refine prompt selector
REFINE_TEMPLATE = SelectorPromptTemplate(
    default_template=DEFAULT_REFINE_PROMPT,
    conditionals=[(is_chat_model, CHAT_REFINE_PROMPT)],
)

这看起来代码很多,但还不错!如果您查看默认提示,您可能会注意到有默认提示和特定于聊天模型的提示。
延续这一趋势,我们对自定义提示也做了同样的事情。
然后,使用提示选择器,我们可以将两个提示合并到一个对象中。
如果使用的 LLM 是聊天模型(ChatGPT、GPT-4),则使用聊天提示。
否则,请使用普通的提示模板。

另一件需要注意的事情是我们只定义了一个 QA 模板。
在聊天模型中,这将被转换为单个“人类”消息。

因此,现在我们可以将这些提示导入我们的应用程序并在查询期间使用它们。

from constants import REFINE_TEMPLATE, TEXT_QA_TEMPLATE

...
if "llama_index" in st.session_state:
    query_text = st.text_input("Ask about a term or definition:")
    if query_text:
        query_text = query_text  # Notice we removed the old instructions
        with st.spinner("Generating answer..."):
            response = st.session_state["llama_index"].query(
                query_text,
                similarity_top_k=5,
                response_mode="compact",
                text_qa_template=TEXT_QA_TEMPLATE,
                refine_template=REFINE_TEMPLATE,
            )
        st.markdown(str(response))
...

如果您对查询进行更多尝试,希望您注意到现在的响应更好地遵循了我们的说明!


改进 #3 - 图像支持

Llama 索引还支持图像!使用 Llama Index,我们可以上传文档(论文、信件等)的图像,Llama Index 负责提取文本。
我们还可以利用这一点来允许用户上传其文档的图像并从中提取术语和定义。

如果您收到有关 PIL 的导入错误,请先使用以下命令安装它pip install Pillow

from PIL import Image
from llama_index.readers.file import (
    DEFAULT_FILE_EXTRACTOR,
    ImageParser,
)

@st.cache_resource
def get_file_extractor():
    image_parser = ImageParser(keep_image=True, parse_text=True)
    file_extractor = DEFAULT_FILE_EXTRACTOR
    file_extractor.update(
        {
            ".jpg": image_parser,
            ".png": image_parser,
            ".jpeg": image_parser,
        }
    )

    return file_extractor

file_extractor = get_file_extractor()
...
with upload_tab:
    st.subheader("Extract and Query Definitions")
    if st.button("Initialize Index and Reset Terms", key="init_index_1"):
        st.session_state["llama_index"] = initialize_index(
            llm_name, model_temperature, api_key
        )
        st.session_state["all_terms"] = DEFAULT_TERMS

    if "llama_index" in st.session_state:
        st.markdown(
            "Either upload an image/screenshot of a document, or enter the text manually."
        )
        uploaded_file = st.file_uploader(
            "Upload an image/screenshot of a document:",
            type=["png", "jpg", "jpeg"],
        )
        document_text = st.text_area("Or enter raw text")
        if st.button("Extract Terms and Definitions") and (
            uploaded_file or document_text
        ):
            st.session_state["terms"] = {}
            terms_docs = {}
            with st.spinner("Extracting (images may be slow)..."):
                if document_text:
                    terms_docs.update(
                        extract_terms(
                            [Document(text=document_text)],
                            term_extract_str,
                            llm_name,
                            model_temperature,
                            api_key,
                        )
                    )
                if uploaded_file:
                    Image.open(uploaded_file).convert("RGB").save("temp.png")
                    img_reader = SimpleDirectoryReader(
                        input_files=["temp.png"], file_extractor=file_extractor
                    )
                    img_docs = img_reader.load_data()
                    os.remove("temp.png")
                    terms_docs.update(
                        extract_terms(
                            img_docs,
                            term_extract_str,
                            llm_name,
                            model_temperature,
                            api_key,
                        )
                    )
            st.session_state["terms"].update(terms_docs)

        if "terms" in st.session_state and st.session_state["terms"]:
            st.markdown("Extracted terms")
            st.json(st.session_state["terms"])

            if st.button("Insert terms?"):
                with st.spinner("Inserting terms"):
                    insert_terms(st.session_state["terms"])
                st.session_state["all_terms"].update(st.session_state["terms"])
                st.session_state["terms"] = {}
                st.experimental_rerun()

在这里,我们添加了使用 Streamlit 上传文件的选项。
然后图像被打开并保存到磁盘(这看起来很老套,但它使事情变得简单)。
然后我们将图像路径传递给阅读器,提取文档/文本,并删除临时图像文件。

现在我们有了文档,我们可以extract_terms()像以前一样调用。


结论/TLDR

在本教程中,我们介绍了大量信息,同时解决了一些常见问题:

  • 针对不同的用例使用不同的索引(列表与向量索引)
  • session_state使用 Streamlit 的概念存储全局状态值
  • 使用 Llama Index 自定义内部提示
  • 使用 Llama Index 从图像中读取文本

本教程的最终版本可以在此处找到,并且Huggingface Spaces上提供了实时托管演示。


2024-04-15(一)

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Llamaindex是一个开源的搜索引擎,可以用于快速搜索和索引大型数据集。为了在本地部署Llamaindex,您需要按照以下步骤进行操作。 首先,您需要从Llamaindex的官方GitHub页面上下载源代码。确保您的计算机已安装了Git系统,然后使用命令行工具输入以下命令来克隆代码库: ``` git clone https://github.com/llama-lab/llamaindex.git ``` 下载完成后,进入项目文件夹并创建一个Python虚拟环境。使用以下命令可以创建一个虚拟环境: ``` python3 -m venv llama-env ``` 然后需要激活虚拟环境。在Mac和Linux系统下,使用以下命令: ``` source llama-env/bin/activate ``` 在Windows系统下,使用以下命令: ``` llama-env\Scripts\activate ``` 接下来,安装Llamaindex的依赖项。在虚拟环境中运行以下命令: ``` pip install -r requirements.txt ``` 等待依赖项安装完成后,可以开始配置Llamaindex。编辑`config.yaml`文件,根据您的需求进行相应的修改。您可以设置数据集的路径、索引文件的位置和其他相关参数。 完成配置后,运行以下命令来创建索引: ``` python3 llama.py -f path/to/dataset ``` 上述命令中的`path/to/dataset`应替换为实际的数据集路径。运行该命令后,Llamaindex会开始索引数据集。 当索引完成后,您可以使用以下命令来搜索索引中的数据: ``` python3 llama.py -s "your search query" ``` 您可以将`"your search query"`替换为实际的搜索关键字。Llamaindex将返回与关键字匹配的结果。 以上就是在本地部署Llamaindex的步骤。祝您在使用Llamaindex时取得成功!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伊织Scope

请我喝杯伯爵奶茶~!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值