GPT 操作库:Sharepoint (以文档形式返回)

简介

本页面为开发者提供了针对特定应用程序构建 GPT 操作的说明和指南。在继续之前,请确保您已熟悉以下信息:

此解决方案使 GPT 操作能够使用 Microsoft 的 Graph API 搜索功能检索文件的能力,以用户可访问的 SharePoint 或 Office365 文件为上下文来回答用户的问题。它使用 Azure Functions 来处理 Graph API 响应,并将其转换为人类可读的格式或以 ChatGPT 理解的方式进行结构化。此代码仅供参考,您应根据自己的需求进行修改。

此解决方案在 Azure Function 中预处理文件。Azure Function 返回文本,而不是 base64 编码的文件。由于预处理和转换为文本,此解决方案最适合处理大型、非结构化文档,以及当您需要分析的文件数量超过第一个解决方案支持的数量时(请参阅此处的文档)。

价值 + 示例业务用例

价值:用户现在可以利用 ChatGPT 的自然语言功能直接连接到 SharePoint 中的文件

示例用例

  • 用户需要查找与特定主题相关的文件
  • 用户需要一个关键问题的答案,而该答案深埋在文档中

架构 / 示例

此解决方案使用 Node.js Azure Function,根据登录用户:

  1. 根据用户的初始问题搜索用户可以访问的相关文件。

  2. 对于找到的每个文件,将其转换为一致的可读格式并检索所有文本。

  3. 使用 GPT 4o mini (gpt-4o-mini) 根据用户的初始问题从文件中提取相关文本。请注意 GPT 4o mini 的定价此处 - 由于我们处理的是小令牌块,因此此步骤的成本微乎其微。

  4. 将数据返回给 ChatGPT。然后,GPT 使用该信息来回答用户的初始问题。

从下面的架构图可以看出,前三个步骤与解决方案 1 相同。主要区别在于此解决方案将文件转换为文本而不是 base64 字符串,然后使用 GPT 4o mini 对文本进行摘要。

应用程序信息

应用程序关键链接

在开始之前,请查看此应用程序的以下链接:

  • 应用程序网站:https://www.microsoft.com/en-us/microsoft-365/sharepoint/collaboration
  • 应用程序 API 文档:https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-rest-reference/

应用程序先决条件

在开始之前,请确保在您的应用程序环境中完成以下步骤:

  • 可以访问 SharePoint 环境
  • Postman(以及 API 和 OAuth 知识)
  • 来自 platform.openai.com 的 OpenAI API 密钥

中间件信息

如果您遵循搜索概念文件指南Microsoft Graph 搜索 API会返回文件的引用,但不会返回文件内容本身。因此,需要中间件,而不是直接命中 MSFT 端点。

步骤:

  1. 循环遍历返回的文件,使用下载文件终结点转换文件终结点下载文件

  2. 使用pdf-parse将二进制流转换为人类可读的文本

  3. 然后,我们可以通过在函数中使用 gpt-4o-mini 进行摘要来进一步优化,以帮助处理我们今天对 Actions 施加的 100,000 个字符限制。

附加步骤

设置 Azure Function

  1. 使用Azure Function 食谱中的步骤设置 Azure Function

添加函数代码

现在您有了一个经过身份验证的 Azure Function,我们可以更新该函数以搜索 SharePoint / O365

  1. 转到您的测试函数,并将代码粘贴到此文件中。保存函数。

此代码仅供参考 - 虽然它可以直接使用,但它旨在根据您的需求进行自定义(请参阅本文档末尾的示例)。

  1. 通过转到左侧“设置”下的“配置”选项卡来设置以下环境变量。请注意,这可能直接显示在“环境变量”中,具体取决于您的 Azure UI。

    1. TENANT_ID:从上一节复制
    2. CLIENT_ID:从上一节复制
    3. OPENAI_API_KEY::在 platform.openai.com 上生成 OpenAI API 密钥。
  2. 转到“开发工具”下的“控制台”选项卡

    1. 在控制台中安装以下软件包

    2. npm install @microsoft/microsoft-graph-client

    3. npm install axios
    4. npm install pdf-parse
    5. npm install openai
  3. 完成后,请尝试再次从 Postman 调用函数(POST 调用),将以下内容放入正文中(使用您认为会生成响应的问题和搜索词)。

{
    "query": "<选择一个问题>",
    "searchTerm": "<选择一个搜索词>"
}
  1. 如果收到响应,则可以设置自定义 GPT!

详细演练

以下内容将介绍此解决方案的设置说明和独特演练,即预处理文件并在 Azure Function 中提取摘要。您可以在此处找到整个代码。

代码演练

实现身份验证

下面是一些我们将在函数中使用的辅助函数。

初始化 Microsoft Graph Client

创建一个函数,使用访问令牌初始化 Graph 客户端。这将用于搜索 Office 365 和 SharePoint。

const { Client } = require('@microsoft/microsoft-graph-client');

function initGraphClient(accessToken) {
    return Client.init({
        authProvider: (done) => {
            done(null, accessToken);
        }
    });
}

获取代表 (OBO) 令牌

此函数使用现有的持有者令牌从 Microsoft 的身份平台请求 OBO 令牌。这允许传递凭据,以确保搜索仅返回登录用户可以访问的文件。

const axios = require('axios');
const qs = require('querystring');

async function getOboToken(userAccessToken) {
    const { TENANT_ID, CLIENT_ID, MICROSOFT_PROVIDER_AUTHENTICATION_SECRET } = process.env;
    const params = {
        client_id: CLIENT_ID,
        client_secret: MICROSOFT_PROVIDER_AUTHENTICATION_SECRET,
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        assertion: userAccessToken,
        requested_token_use: 'on_behalf_of',
        scope: 'https://graph.microsoft.com/.default'
    };

    const url = `https\://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`;
    try {
        const response = await axios.post(url, qs.stringify(params), {
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
        });
        return response.data.access_token;
    } catch (error) {
        console.error('获取 OBO 令牌时出错:', error.response?.data || error.message);
        throw error;
    }
}

从 O365 / SharePoint 项检索内容

此函数获取驱动器项的内容,处理不同的文件类型,并在必要时将文件转换为 PDF 以提取文本。它使用下载终结点处理 PDF,并使用转换终结点处理其他支持的文件类型。

const getDriveItemContent = async (client, driveId, itemId, name) => {
    try {
        const fileType = path.extname(name).toLowerCase();
        // 以下文件类型是可转换为 PDF 以提取文本的文件类型。请参阅 https://learn.microsoft.com/en-us/graph/api/driveitem-get-content-format?view=graph-rest-1.0&tabs=http
        const allowedFileTypes = ['.pdf', '.doc', '.docx', '.odp', '.ods', '.odt', '.pot', '.potm', '.potx', '.pps', '.ppsx', '.ppsxm', '.ppt', '.pptm', '.pptx', '.rtf'];
        // filePath 根据文件类型而变化,添加 ?format=pdf 将非 PDF 类型转换为 PDF 以进行文本提取,因此上面允许的文件类型都将转换为 PDF
        const filePath = `/drives/${driveId}/items/${itemId}/content` + ((fileType === '.pdf' || fileType === '.txt' || fileType === '.csv') ? '' : '?format=pdf');
        if (allowedFileTypes.includes(fileType)) {
            response = await client.api(filePath).getStream();
            // 以下代码将响应中的块组合起来
            let chunks = [];
            for await (let chunk of response) {
                chunks.push(chunk);
            }
            let buffer = Buffer.concat(chunks);
            // 以下代码从 PDF 中提取文本。
            const pdfContents = await pdfParse(buffer);
            return pdfContents.text;
        } else if (fileType === '.txt') {
            // 如果类型是 txt,则不需要创建流,而是直接获取内容
            response = await client.api(filePath).get();
            return response;
        }  else if (fileType === '.csv') {
            response = await client.api(filePath).getStream();
            let chunks = [];
            for await (let chunk of response) {
                chunks.push(chunk);
            }
            let buffer = Buffer.concat(chunks);
            let dataString = buffer.toString('utf-8');
            return dataString

    } else {
        return '不支持的文件类型';
    }

    } catch (error) {
        console.error('获取驱动器内容时出错:', error);
        throw new Error(`无法获取 ${name} 的内容:${error.message}`);
    }
};

集成 GPT 4o mini 进行文本分析

此函数利用 OpenAI SDK 分析从文档中提取的文本,并根据用户查询查找相关信息。这有助于确保只有与用户问题相关的文本才会被返回给 GPT。

const getRelevantParts = async (text, query) => {
    try {
        // 我们使用您的 OpenAI 密钥来初始化 OpenAI 客户端
        const openAIKey = process.env["OPENAI_API_KEY"];
        const openai = new OpenAI({
            apiKey: openAIKey,
        });
        const response = await openai.chat.completions.create({
            // 使用 gpt-4o-mini 是因为它速度快,可以防止超时。您可以根据需要调整此提示
            model: "gpt-4o-mini",
            messages: [
                {"role": "system", "content": "您是一个乐于助人的助手,可以根据查询在文本中查找相关内容。您只返回相关句子,最多返回 10 句"},
                {"role": "user", "content": `根据这个问题:**"${query}"\*\*,从以下文本中提取相关部分:\*\*\*\*\*\n\n${text}*****。如果您无法根据文本回答问题,请回复“未提供信息”`}
            ],
            // 使用 0 的温度,因为我们只想提取相关内容
            temperature: 0,
            // 使用 1000 的 max_tokens,但您可以根据您搜索的文档数量进行自定义。
            max_tokens: 1000
        });
        return response.choices[0].message.content;
    } catch (error) {
        console.error('OpenAI 出错:', error);
        return '使用 OpenAI 处理文本时出错' + error;
    }
};

创建处理请求的 Azure Function

现在我们有了所有这些辅助函数,Azure Function 将协调流程,通过身份验证用户、执行搜索以及遍历搜索结果来提取文本并将相关文本部分检索给 GPT。

处理 HTTP 请求: 函数首先从 HTTP 请求中提取查询和 searchTerm。它检查 Authorization 标头是否存在并提取持有者令牌。

身份验证: 使用持有者令牌,它使用上面定义的 getOboToken 从 Microsoft 的身份平台获取 OBO 令牌。

初始化 Graph Client: 使用 OBO 令牌,它使用上面定义的 initGraphClient 初始化 Microsoft Graph 客户端。

文档搜索: 它构建搜索查询并将其发送到 Microsoft Graph API 以根据 searchTerm 查找文档。

文档处理:对于搜索返回的每个文档:

  • 它使用 getDriveItemContent 检索文档内容。

  • 如果文件类型受支持,它会使用 getRelevantParts 分析内容,该函数将文本发送到 OpenAI 的模型以根据查询提取相关信息。

  • 它收集分析结果并包含文档名称和 URL 等元数据。

响应:函数按相关性对结果进行排序,并将它们作为 HTTP 响应返回。

module.exports = async function (context, req) {
    const query = req.query.query || (req.body && req.body.query);
    const searchTerm = req.query.searchTerm || (req.body && req.body.searchTerm);
    if (!req.headers.authorization) {
        context.res = {
            status: 400,
            body: '缺少 Authorization 标头'
        };
        return;
    }
    /// 以下代码获取传递给函数的令牌,用于获取 OBO 令牌。
    const bearerToken = req.headers.authorization.split(' ')[1];
    let accessToken;
    try {
        accessToken = await getOboToken(bearerToken);
    } catch (error) {
        context.res = {
            status: 500,
            body: `获取 OBO 令牌失败:${error.message}`
        };
        return;
    }
    // 使用上面定义的 initGraphClient 函数初始化 Graph Client
    let client = initGraphClient(accessToken);
    // 这是用于 Microsoft Graph 搜索 API 的搜索请求体:https://learn.microsoft.com/en-us/graph/search-concept-files
    const requestBody = {
        requests: [
            {
                entityTypes: ['driveItem'],
                query: {
                    queryString: searchTerm
                },
                from: 0,
                // 以下设置为从 Graph API 摘要前 10 个搜索结果,但可以根据您的文档进行配置。
                size: 10
            }
        ]
    };

    try { 
        // 用于标记化内容(例如,基于单词)的函数。
        const tokenizeContent = (content) => {
            return content.split(/\s+/);
        };

        // 用于将令牌分解为 10k 令牌窗口的函数(适用于 gpt-4o-mini)
        const breakIntoTokenWindows = (tokens) => {
            const tokenWindows = []
            const maxWindowTokens = 10000; // 10k 令牌
            let startIndex = 0;

            while (startIndex < tokens.length) {
                const window = tokens.slice(startIndex, startIndex + maxWindowTokens);
                tokenWindows.push(window);
                startIndex += maxWindowTokens;
            }

            return tokenWindows;
        };
        // 这是我们进行搜索的地方
        const list = await client.api('/search/query').post(requestBody);

        const processList = async () => {
            // 这将遍历并为每个搜索响应获取文件内容,并使用 gpt-4o-mini 进行摘要
            const results = [];

            await Promise.all(list.value[0].hitsContainers.map(async (container) => {
                for (const hit of container.hits) {
                    if (hit.resource["@odata.type"] === "#microsoft.graph.driveItem") {
                        const { name, id } = hit.resource;
                        // 我们使用以下代码获取文件的 URL 以包含在响应中
                        const webUrl = hit.resource.webUrl.replace(/\s/g, "%20");
                        // Microsoft Graph API 对响应进行排名,因此我们使用此排名进行排序
                        const rank = hit.rank;
                        // 以下代码是文件所在的位置
                        const driveId = hit.resource.parentReference.driveId;
                        const contents = await getDriveItemContent(client, driveId, id, name);
                        if (contents !== '不支持的文件类型') {
                            // 使用之前定义的函数标记化内容
                            const tokens = tokenizeContent(contents);

                            // 将令牌分解为 10k 令牌窗口
                            const tokenWindows = breakIntoTokenWindows(tokens);

                            // 处理每个令牌窗口并合并结果
                            const relevantPartsPromises = tokenWindows.map(window => getRelevantParts(window.join(' '), query));
                            const relevantParts = await Promise.all(relevantPartsPromises);
                            const combinedResults = relevantParts.join('\n'); // 合并结果

                            results.push({ name, webUrl, rank, contents: combinedResults });
                        } 
                        else {
                            results.push({ name, webUrl, rank, contents: '不支持的文件类型' });
                        }
                    }
                }
            }));

            return results;
        };
        let results;
        if (list.value[0].hitsContainers[0].total == 0) {
            // 如果 Microsoft Graph API 未返回任何结果,则向 API 返回无结果
            results = '未找到结果';
        } else {
            // 如果 Microsoft Graph API 返回结果,则运行 processList 进行迭代。
            results = await processList();
            results.sort((a, b) => a.rank - b.rank);
        }
        context.res = {
            status: 200,
            body: results
        };
    } catch (error) {
        context.res = {
            status: 500,
            body: `执行搜索或处理结果时出错:${error.message}`,
        };
    }
};

自定义

以下是一些可以自定义的潜在领域。

  • 您可以自定义 GPT 提示,以便在找不到结果时再次搜索一定次数。

  • 您可以自定义代码,仅搜索特定的 SharePoint 站点或 O365 Drives,方法是自定义搜索查询。这将有助于集中搜索并改进检索。目前设置的函数会查找登录用户可以访问的所有文件。

  • 您可以使用 gpt-4o 而不是 gpt-4o-mini。这将稍微增加成本和延迟,但您可能会获得更高质量的摘要。

  • 您可以自定义在调用 Microsoft Graph 时搜索的文件数量。

注意事项

请注意,Actions 的所有相同限制仍然适用,包括返回 100K 个字符或更少以及 45 秒超时

  • 此功能仅适用于文本,不适用于图像。通过在 Azure Function 中添加一些额外代码,您可以通过使用 GPT-4o 提取图像摘要来对其进行自定义。

  • 此功能不适用于结构化数据。如果结构化数据是您用例的主要部分,我们建议使用解决方案 1。

ChatGPT 步骤

自定义 GPT 说明

创建自定义 GPT 后,请将以下文本复制到“说明”面板中。有疑问吗?请查看入门示例,了解此步骤的详细信息。

您是一位问答助手,可以帮助回答用户的问题。您可以通过您的 API 操作访问文档存储库。当用户提出问题时,您将按原样将该问题传递给“query”参数,对于“searchTerm”,您将使用您认为应该用于搜索的单个关键字或术语。

****

场景 1:有答案

如果您的操作返回了结果,那么您将从操作中获取结果,并使用操作返回的 webUrl 简洁地进行摘要。您将尽您所知,根据操作中的信息回答用户的问题。

****

场景 2:未找到结果

如果从操作中获得响应是“未找到结果”,请在此停止,并告知用户没有找到结果,您将尝试使用不同的搜索词,并解释原因。在进行另一次搜索之前,您必须始终告知用户。

示例:

****

我找不到关于“DEI”的结果。我现在将尝试 [插入术语],因为 [插入解释]

****

然后,尝试使用与之前相似的单个词的 searchTerm。

尝试三次。第三次之后,告知用户您没有找到任何相关文档来回答问题,并让他们检查 SharePoint。请务必在每个步骤中明确您正在搜索的内容。

****

在任何一种情况下,都请尝试回答用户的问题。如果您无法根据找到的知识回答用户的问题,请告知用户,并让他们检查 SharePoint 中的 HR Docs。如果文件是 CSV、XLSX 或 XLS,您可以告知用户使用链接下载文件并重新上传以使用高级数据分析。

OpenAPI 架构

创建自定义 GPT 后,请将以下文本复制到“操作”面板中。有疑问吗?请查看入门示例,了解此步骤的详细信息。

以下规范会将 query 参数传递给预处理程序,并将 searchTerm 用于在 Microsoft Graph 中查找正确的文件。

请确保根据上面屏幕截图中的链接切换函数应用名称、函数名称和代码

openapi: 3.1.0
info:
  title: SharePoint Search API
  description: API for searching SharePoint documents.
  version: 1.0.0
servers:

  - url: https://{your_function_app_name}.azurewebsites.net/api
    description: SharePoint Search API server
paths:
  /{your_function_name}?code={enter your specific endpoint id here}:
    post:
      operationId: searchSharePoint
      summary: Searches SharePoint for documents matching a query and term.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  description: The full query to search for in SharePoint documents.
                searchTerm:
                  type: string
                  description: A specific term to search for within the documents.
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    documentName:
                      type: string
                      description: The name of the document.
                    snippet:
                      type: string
                      description: A snippet from the document containing the search term.
                    url:
                      type: string
                      description: The URL to access the document.

身份验证说明

以下是有关使用此第三方应用程序设置身份验证的说明。有疑问吗?请查看入门示例,了解此步骤的详细信息。

有关身份验证的更详细说明,请参阅上面的Azure Function 食谱

常见问题解答和故障排除

  • 为什么在代码中使用 Microsoft Graph API 而不是SharePoint API

  • SharePoint API 是旧版 API - 根据 Microsoft 的文档此处,“对于 SharePoint Online,通过 Microsoft Graph REST API 对 SharePoint 进行 REST API 创新”。Graph API 提供了更大的灵活性,而 SharePoint API 仍然存在为什么需要此项而不是直接与 Microsoft Graph API 交互?部分中列出的相同文件问题。

  • 此功能支持哪些类型的文件?

    1. 此功能支持转换文件终结点文档此处中列出的所有文件。具体来说,它支持 pdf、doc、docx、odp、ods、odt、pot、potm、potx、pps、ppsx、ppsxm、ppt、pptm、pptx、rtf

    2. 当搜索结果返回 XLS、XLSX 或 CSV 时,此功能会提示用户下载文件并重新上传以使用高级数据分析进行提问。如上所述,如果结构化数据是您用例的一部分,我们建议使用解决方案 1。

  • 为什么我需要请求 OBO 令牌?

  • 当您尝试使用与用于向 Azure Function 进行身份验证的令牌相同的令牌向 Graph API 进行身份验证时,您会收到“无效受众”令牌。这是因为令牌的受众只能是 user_impersonation。

  • 为解决此问题,该函数使用代表流请求一个作用域为 Files.Read.All 的新令牌。这将继承登录用户的权限,意味着此函数将仅搜索登录用户有权访问的文件。

  • 我们特意为每次请求请求新的代表令牌,因为 Azure Function Apps 被设计为无状态的。您可能会将其与 Azure Key Vault 集成以存储密钥并以编程方式检索。

您希望我们优先处理哪些集成?我们的集成中是否存在错误?在我们的 GitHub 中提交 PR 或 issue,我们将进行查看。