返回

获取 GitHub Copilot Chat Completions API 完整响应及文档注入

javascript

获取GitHub Copilot Chat Completions API响应

在使用GitHub Copilot Chat Completions API构建扩展时,直接获取 Copilot LLM 响应文本是一个常见需求。上述代码示例虽然实现了流式响应,但获取完整文本内容还需要进一步处理。 本文将探讨如何解决这个问题。

问题分析

现有代码通过 fetch API 从 GitHub Copilot Chat Completions API 获取流式响应。 流式响应的好处是能即时显示结果,提高用户体验。但如果需要对完整响应进行处理,例如注入文档片段,则需要将流转换为完整的文本字符串。 问题在于,尝试使用 Response.text() 方法获取文本时,得到的是一系列包含 data: 前缀的 Server-Sent Events (SSE) 格式的事件消息,而不是纯粹的文本。这些事件消息中包含了部分响应内容、元数据以及内容过滤结果等。

解决方案

解决这个问题的关键在于正确解析 SSE 格式的响应,并将多个事件消息中的文本内容拼接起来。以下提供两种方案:

方案一: 使用 JavaScript 解析 SSE 流

这种方案使用 JavaScript 遍历并解析 SSE 流,提取其中的 delta.content 并拼接成完整的文本。

代码示例:

async function getCopilotResponse(messages, tokenForUser) {
  const copilotLLMResponse = await fetch(
    "https://api.githubcopilot.com/chat/completions",
    {
      method: "POST",
      headers: {
        authorization: `Bearer ${tokenForUser}`,
        "content-type": "application/json",
      },
      body: JSON.stringify({
        messages,
        stream: true,
      }),
    }
  );

  if (!copilotLLMResponse.ok) {
    throw new Error(`HTTP error! status: ${copilotLLMResponse.status}`);
  }
  if (!copilotLLMResponse.body) {
    throw new Error("Empty response body");
  }
  const reader = copilotLLMResponse.body.getReader();
  const decoder = new TextDecoder();
  let responseText = "";
  let partialLine = "";

  while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      partialLine += decoder.decode(value);
      let eolIndex;
      while ((eolIndex = partialLine.indexOf('\n')) >= 0) {
          const line = partialLine.slice(0, eolIndex);
          partialLine = partialLine.slice(eolIndex + 1);
          if (line.startsWith("data:")) {
              const data = line.slice(5).trim(); // remove 'data: ' prefix
              if (data === '[DONE]') break;
              try {
                const parsedEvent = JSON.parse(data);
                responseText += parsedEvent?.choices?.[0]?.delta?.content || "";
                } catch(e) {
                  console.error("Error parsing JSON:", e, "Invalid JSON String",data);
                  continue; // skip the broken line
              }

          }
      }
  }

  return responseText;
}

操作步骤:

  1. 将以上代码复制到你的扩展代码中。
  2. 调用 getCopilotResponse(messages, tokenForUser) 函数,传入用户消息 messages 和用户 Token tokenForUser
  3. 该函数会返回一个 Promise,resolve 后得到完整的 Copilot 响应文本。

原理与安全建议:

此方案利用了 TextDecoder 和字符串操作来解析 SSE 格式的响应。 reader.read() 方法从流中读取数据块,TextDecoder 将字节流解码为字符串。代码逐行读取响应,并通过 startsWith("data:") 来判断是否为有效的事件消息,然后提取 delta.content 内容并拼接。错误处理机制保证在发生JSON解析错误时能够跳过错误行并记录日志,确保程序健壮性。另外, 应妥善保管用户的 tokenForUser, 避免泄露, 可考虑将其存储在安全的环境中, 如操作系统的凭据管理器或加密存储. 同时对请求体进行验证和过滤,避免注入攻击。

方案二: 使用第三方库解析 SSE 流

为了简化代码和提高可维护性,可以使用现成的第三方库来解析 SSE 流。例如,可以使用 eventsource 库。

代码示例:

  1. 首先安装依赖:
npm install eventsource
  1. 使用 eventsource 解析 SSE 流:
import EventSource from 'eventsource';

async function getCopilotResponse(messages, tokenForUser) {
  return new Promise((resolve, reject) => {
      let responseText = "";

      const eventSource = new EventSource("https://api.githubcopilot.com/chat/completions", {
          headers: {
              authorization: `Bearer ${tokenForUser}`,
              "content-type": "application/json",
          },
          method: "POST",
          body: JSON.stringify({
              messages,
              stream: true,
          }),
      });

      eventSource.onmessage = (event) => {
          if (event.data === "[DONE]") {
              eventSource.close();
              resolve(responseText);
          } else {
            try {
            const parsedEvent = JSON.parse(event.data);
                responseText += parsedEvent?.choices?.[0]?.delta?.content || "";
            } catch (e) {
              console.error("Error parsing JSON:", e, "Invalid JSON String",event.data);
                // Do nothing and ignore it since it's not critical
                // or implement other way to handle such cases
            }
          }
      };

      eventSource.onerror = (error) => {
          eventSource.close();
          reject(error);
      };
  });
}

操作步骤:

  1. 确保已安装 eventsource 库。
  2. 将以上代码复制到你的扩展代码中。
  3. 调用 getCopilotResponse(messages, tokenForUser) 函数,传入用户消息 messages 和用户 Token tokenForUser
  4. 该函数会返回一个 Promise,resolve 后得到完整的 Copilot 响应文本。

原理与安全建议:

该方案使用了eventsource 库, 该库会自动处理 SSE 流的连接、重连和错误处理。 代码通过监听 onmessage 事件来获取每个事件消息,解析 JSON 并拼接 delta.content。同时监听了onerror事件来捕获错误. 此方案代码更简洁,可读性更强。使用第三方库时, 务必选择信誉良好、活跃维护的库,以降低安全风险. 同时要检查库的版本和依赖, 及时更新到最新版本, 防止安全漏洞。 同样的,务必保证用户的 tokenForUser 得到妥善保管,并且对请求体进行验证和过滤。

实现文档注入

获取到完整的 Copilot 响应文本后,就可以实现文档注入了。 基本思路是在用户消息中加入提示词,引导 Copilot 参考提供的文档内容。 具体实现方式可以根据扩展的功能和需求进行调整。例如,可以在用户消息前拼接文档内容,或者将文档内容作为上下文信息传递给 Copilot LLM。

代码示例 (基于方案一):

async function handleMessage(userInput, documentation, tokenForUser) {
    const messages = [
      {
        role: "system",
        content: "You are a helpful AI assistant. Please use the following documentation to answer the user's question:\n" + documentation,
      },
      {
        role: "user",
        content: userInput,
      },
    ];
    const copilotResponse = await getCopilotResponse(messages, tokenForUser);
    // 处理 copilotResponse,例如显示在 UI 上
    console.log(copilotResponse);
    return copilotResponse;
  }

//使用方法
const documentation = "这是一个示例文档。这里应该放一些相关的文档内容,例如函数签名、用法说明等。\n例如: function add(a, b) { return a + b; }";
const userInput = "如何使用 add 函数?";
const tokenForUser = "YOUR_GITHUB_COPILOT_TOKEN";
handleMessage(userInput,documentation,tokenForUser).then(response => {
  // 使用响应
    console.log(response);
}).catch(error => {
  // 处理错误
  console.error("error:",error);
});

这段代码展示了如何将文档内容注入到 Copilot LLM 的 system 消息中,引导 Copilot 在回答用户问题时参考文档。 handleMessage 函数接收用户输入、文档内容和用户 token 作为参数,构建 Copilot LLM 所需的消息,然后调用 getCopilotResponse 获取响应。 可以根据实际需求修改 system 消息的内容和格式,以达到更好的效果。

相关资源