本页面为开发者构建特定应用程序的 GPT Action 提供说明和指南。在继续之前,请务必先熟悉以下信息
此解决方案使 GPT Action 能够使用户能够访问的 SharePoint 或 Office 365 中的文件上下文来回答用户的问题,它使用了 Microsoft Graph API 的搜索功能和检索文件的能力。它使用 Azure 函数来处理 Graph API 响应,并将其转换为人类可读的格式或 ChatGPT 可以理解的结构。此代码旨在提供方向性指导,您应该根据您的要求对其进行修改。
此解决方案在 Azure 函数中预处理文件。Azure 函数返回文本,而不是 base64 编码的文件。由于预处理和转换为文本,此解决方案最适合用于大型非结构化文档,以及当您想要分析的文件数量超过第一个解决方案中支持的文件数量时(请参阅此处的文档)。
价值:用户现在可以利用 ChatGPT 的自然语言能力直接连接到 Sharpeoint 中的文件
示例用例:
此解决方案使用 Node.js Azure 函数,根据登录用户
基于用户的初始问题,搜索用户有权访问的相关文件。
对于找到的每个文件,将其转换为一致的可读格式并检索所有文本。
使用 GPT 4o mini (gpt-4o-mini) 从文件中提取与用户初始问题相关的文本。请注意 GPT 4o mini 的定价 - 由于我们处理的是小 token 块,因此此步骤的成本很小。
将数据返回给 ChatGPT。然后,GPT 使用该信息来回复用户的初始问题。
正如您可以从下面的架构图中看到的那样,前三个步骤与解决方案 1 相同。主要区别在于此解决方案将文件转换为文本而不是 base64 字符串,然后使用 GPT 4o mini 总结该文本。
在开始之前,请查看应用程序的这些链接
在开始之前,请确保在您的应用程序环境中完成以下步骤
如果您遵循搜索概念文件指南,则 Microsoft Graph 搜索 API 返回符合条件的文件引用,但不返回文件内容本身。因此,需要中间件,而不是直接访问 MSFT 端点。
步骤
现在您有了一个经过身份验证的 Azure 函数,我们可以更新该函数以搜索 SharePoint / O365
此代码旨在提供方向性指导 - 虽然它应该可以开箱即用,但它的设计目的是根据您的需求进行定制(请参阅本文档末尾的示例)。
通过转到左侧设置下的配置选项卡来设置以下环境变量。请注意,根据您的 Azure UI,这可能会直接列在环境变量中。
TENANT_ID
: 从上一节复制
CLIENT_ID
: 从上一节复制
OPENAI_API_KEY:
在 platform.openai.com 上启动一个 OpenAI API 密钥。
转到开发工具下的控制台选项卡
在控制台中安装以下软件包
npm install @microsoft/microsoft-graph-client
npm install axios
npm install pdf-parse
npm install openai
完成此操作后,再次尝试从 Postman 调用该函数 (POST 调用),将以下内容放入正文中(使用您认为会生成响应的查询和搜索词)。
{
"query": "<choose a question>",
"searchTerm": "<choose a search term>"
}
如果您得到响应,则说明您已准备好使用自定义 GPT 进行设置!
下面详细介绍了此解决方案独有的设置说明和演练,即在 Azure 函数中预处理文件并提取摘要。您可以在此处找到完整代码。
下面我们有一些辅助函数,我们将在函数中使用它们。
创建一个函数以使用访问令牌初始化 Graph 客户端。这将用于搜索 Office 365 和 SharePoint。
const { Client } = require('@microsoft/microsoft-graph-client');
function initGraphClient(accessToken) {
return Client.init({
authProvider: (done) => {
done(null, accessToken);
}
});
}
此函数使用现有的 bearer 令牌从 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('Error obtaining OBO token:', error.response?.data || error.message);
throw error;
}
}
此函数获取驱动器项目的内容,处理不同的文件类型,并在必要时将文件转换为 PDF 以进行文本提取。这使用了 PDF 的下载端点和其他支持文件类型的转换端点。
const getDriveItemContent = async (client, driveId, itemId, name) => {
try {
const fileType = path.extname(name).toLowerCase();
// the below files types are the ones that are able to be converted to PDF to extract the text. See 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 changes based on file type, adding ?format=pdf to convert non-pdf types to pdf for text extraction, so all files in allowedFileTypes above are converted to 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();
// The below takes the chunks in response and combines
let chunks = [];
for await (let chunk of response) {
chunks.push(chunk);
}
let buffer = Buffer.concat(chunks);
// the below extracts the text from the PDF.
const pdfContents = await pdfParse(buffer);
return pdfContents.text;
} else if (fileType === '.txt') {
// If the type is txt, it does not need to create a stream and instead just grabs the content
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 'Unsupported File Type';
}
} catch (error) {
console.error('Error fetching drive content:', error);
throw new Error(`Failed to fetch content for ${name}: ${error.message}`);
}
};
此函数利用 OpenAI SDK 来分析从文档中提取的文本,并根据用户查询查找相关信息。这有助于确保仅将与用户问题相关的文本返回给 GPT。
const getRelevantParts = async (text, query) => {
try {
// We use your OpenAI key to initialize the OpenAI client
const openAIKey = process.env["OPENAI_API_KEY"];
const openai = new OpenAI({
apiKey: openAIKey,
});
const response = await openai.chat.completions.create({
// Using gpt-4o-mini due to speed to prevent timeouts. You can tweak this prompt as needed
model: "gpt-4o-mini",
messages: [
{"role": "system", "content": "You are a helpful assistant that finds relevant content in text based on a query. You only return the relevant sentences, and you return a maximum of 10 sentences"},
{"role": "user", "content": `Based on this question: **"${query}"**, get the relevant parts from the following text:*****\n\n${text}*****. If you cannot answer the question based on the text, respond with 'No information provided'`}
],
// using temperature of 0 since we want to just extract the relevant content
temperature: 0,
// using max_tokens of 1000, but you can customize this based on the number of documents you are searching.
max_tokens: 1000
});
return response.choices[0].message.content;
} catch (error) {
console.error('Error with OpenAI:', error);
return 'Error processing text with OpenAI' + error;
}
};
现在我们有了所有这些辅助函数,Azure 函数将通过验证用户身份、执行搜索以及遍历搜索结果来提取文本并将文本的相关部分检索到 GPT,从而协调流程。
处理 HTTP 请求: 该函数首先从 HTTP 请求中提取查询和 searchTerm。它检查 Authorization 标头是否存在并提取 bearer 令牌。
身份验证: 使用 bearer 令牌,它使用上面定义的 getOboToken 从 Microsoft 身份平台获取 OBO 令牌。
初始化 Graph 客户端: 使用 OBO 令牌,它使用上面定义的 initGraphClient 初始化 Microsoft Graph 客户端。
文档搜索: 它构建一个搜索查询并将其发送到 Microsoft Graph API,以根据 searchTerm 查找文档。
文档处理:对于搜索返回的每个文档
它使用 getDriveItemContent 检索文档内容。
如果文件类型受支持,它将使用 getRelevantParts 分析内容,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 header is missing'
};
return;
}
/// The below takes the token passed to the function, to use to get an OBO token.
const bearerToken = req.headers.authorization.split(' ')[1];
let accessToken;
try {
accessToken = await getOboToken(bearerToken);
} catch (error) {
context.res = {
status: 500,
body: `Failed to obtain OBO token: ${error.message}`
};
return;
}
// Initialize the Graph Client using the initGraphClient function defined above
let client = initGraphClient(accessToken);
// this is the search body to be used in the Microsft Graph Search API: https://learn.microsoft.com/en-us/graph/search-concept-files
const requestBody = {
requests: [
{
entityTypes: ['driveItem'],
query: {
queryString: searchTerm
},
from: 0,
// the below is set to summarize the top 10 search results from the Graph API, but can configure based on your documents.
size: 10
}
]
};
try {
// Function to tokenize content (e.g., based on words).
const tokenizeContent = (content) => {
return content.split(/\s+/);
};
// Function to break tokens into 10k token windows for gpt-4o-mini
const breakIntoTokenWindows = (tokens) => {
const tokenWindows = []
const maxWindowTokens = 10000; // 10k tokens
let startIndex = 0;
while (startIndex < tokens.length) {
const window = tokens.slice(startIndex, startIndex + maxWindowTokens);
tokenWindows.push(window);
startIndex += maxWindowTokens;
}
return tokenWindows;
};
// This is where we are doing the search
const list = await client.api('/search/query').post(requestBody);
const processList = async () => {
// This will go through and for each search response, grab the contents of the file and summarize with 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;
// We use the below to grab the URL of the file to include in the response
const webUrl = hit.resource.webUrl.replace(/\s/g, "%20");
// The Microsoft Graph API ranks the reponses, so we use this to order it
const rank = hit.rank;
// The below is where the file lives
const driveId = hit.resource.parentReference.driveId;
const contents = await getDriveItemContent(client, driveId, id, name);
if (contents !== 'Unsupported File Type') {
// Tokenize content using function defined previously
const tokens = tokenizeContent(contents);
// Break tokens into 10k token windows
const tokenWindows = breakIntoTokenWindows(tokens);
// Process each token window and combine results
const relevantPartsPromises = tokenWindows.map(window => getRelevantParts(window.join(' '), query));
const relevantParts = await Promise.all(relevantPartsPromises);
const combinedResults = relevantParts.join('\n'); // Combine results
results.push({ name, webUrl, rank, contents: combinedResults });
}
else {
results.push({ name, webUrl, rank, contents: 'Unsupported File Type' });
}
}
}
}));
return results;
};
let results;
if (list.value[0].hitsContainers[0].total == 0) {
// Return no results found to the API if the Microsoft Graph API returns no results
results = 'No results found';
} else {
// If the Microsoft Graph API does return results, then run processList to iterate through.
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 performing search or processing results: ${error.message}`,
};
}
};
以下是一些潜在的自定义领域。
您可以自定义 GPT 提示,以便在未找到任何内容时再次搜索一定次数。
您可以自定义代码以仅搜索特定的 SharePoint 站点或 O365 驱动器,方法是自定义搜索查询。这将有助于集中搜索并提高检索效率。现在设置的函数会查看登录用户可以访问的所有文件。
您可以使用 gpt-4o 而不是 gpt-4o-mini。这将略微增加成本和延迟,但您可能会获得更高质量的摘要。
您可以自定义它在调用 Microsoft Graph 时搜索的文件数量。
请注意,Actions 的所有相同限制在此处适用,包括返回 10 万字符或更少以及45 秒超时。
这仅适用于文本,不适用于图像。通过在 Azure 函数中添加一些额外的代码,您可以使用 GPT-4o 自定义此功能以提取图像摘要。
这不适用于结构化数据。如果结构化数据是您用例的主要部分,我们建议使用解决方案 1。
创建自定义 GPT 后,将以下文本复制到“说明”面板中。有问题吗?查看入门示例,了解此步骤的详细工作方式。
You are a Q&A helper that helps answer users questions. You have access to a documents repository through your API action. When a user asks a question, you pass in that question exactly as stated to the "query" parameter, and for the "searchTerm" you use a single keyword or term you think you should use for the search.
****
Scenario 1: There are answers
If your action returns results, then you take the results from the action and summarize concisely with the webUrl returned from the action. You answer the users question to the best of your knowledge from the action
****
Scenario 2: No results found
If the response you get from the action is "No results found", stop there and let the user know there were no results and that you are going to try a different search term, and explain why. You must always let the user know before conducting another search.
Example:
****
I found no results for "DEI". I am now going to try [insert term] because [insert explanation]
****
Then, try a different searchTerm that is similar to the one you tried before, with a single word.
Try this three times. After the third time, then let the user know you did not find any relevant documents to answer the question, and to check SharePoint. Be sure to be explicit about what you are searching for at each step.
****
In either scenario, try to answer the user's question. If you cannot answer the user's question based on the knowledge you find, let the user know and ask them to go check the HR Docs in SharePoint. If the file is a CSV, XLSX, or XLS, you can tell the user to download the file using the link and re-upload to use Advanced Data Analysis.
创建自定义 GPT 后,将以下文本复制到“Actions”面板中。有问题吗?查看入门示例,了解此步骤的详细工作方式。
以下规范传入 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 函数食谱。
为什么您的代码中使用 Microsoft Graph API 而不是SharePoint API?
这支持哪些类型的文件?
这支持此处的转换文件端点文档中列出的所有文件。具体来说,它支持 pdf, doc, docx, odp, ods, odt, pot, potm, potx, pps, ppsx, ppsxm, ppt, pptm, pptx, rtf。
当搜索结果返回 XLS、XLSX 或 CSV 时,这会提示用户下载文件并重新上传,以使用高级数据分析提问。如上所述,如果结构化数据是您用例的一部分,我们建议使用解决方案 1。
为什么我需要请求 OBO 令牌?
当您尝试使用与验证 Azure 函数身份相同的令牌来验证 Graph API 身份时,您会收到“无效受众”令牌。这是因为令牌的受众只能是 user_impersonation。
为了解决这个问题,该函数使用代表用户流在应用程序中请求一个范围限定为 Files.Read.All 的新令牌。这将继承登录用户的权限,这意味着此函数将仅搜索登录用户有权访问的文件。
我们有目的地为每个请求请求一个新的代表用户令牌,因为 Azure 函数应用程序旨在是无状态的。您可以考虑将其与 Azure Key Vault 集成,以编程方式存储和检索密钥。
您希望我们优先考虑哪些集成?我们的集成中是否存在错误?在我们的 github 中提交 PR 或 issue,我们会查看一下。