nikkie-ftnextの日記

イベントレポートや読書メモを発信

FastAPIで実装しての学び! GitHub Copilot Extensionsは、GitHubがホストするOpenAI API互換のLLM APIのレスポンスを、受け流す

はじめに

右から左へ〜 受け流す〜♪ nikkieです。

Blackbeard1を例に、GitHub Copilot Extensionsを実装して知ったことを記事にします。

目次

LT「PythonでもGitHub Copilot Extensions作れるもん!」

GitHub Copilot Extensionsとは、Copilot Chatで@する相手を作れるというもの。
OpenAIのGPTsのようなカスタムなLLMを、プログラミングして作れます
これは私のハッカー魂をめちゃめちゃくすぐっており、手に馴染むPythonでせひやりたい!と先日FastAPIでの実装例を公開しました2

FastAPIで実装する中で学んだことを紹介していきます。

読み解きNode.js版Blackbeard

Node.jsでの実装例を読み解き、FastAPIで実装しました。

https://github.com/copilot-extensions/blackbeard-extension/blob/766d3aa5c06c7ed2767c34d8b53b54048776a35f/index.js#L29-L47

  // Use Copilot's LLM to generate a response to the user's messages, with
  // our extra system messages attached.
  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,
      }),
    }
  );

  // Stream the response straight back to the user.
  Readable.from(copilotLLMResponse.body).pipe(res);

「エージェントに Copilot の LLM を使用する」というドキュメントにあるのですが、

POST 要求を使用して、https://api.githubcopilot.com/chat/completions で Copilot の LLM デプロイを呼び出すことができます。
要求と応答は、OpenAI API と同じ形式にする必要があります。

上記の実装はfetchして得たレスポンスをReadable.fromでstreamとして戻しています

これを徹底的にパクって、FastAPIでもstreamとして戻しました

  • StreamingResponse
  • トークンの取得(Blackbeardへのリクエストのヘッダより)
  • メッセージの取得(Blackbeardへのリクエストのボディより)

1️⃣StreamingResponse

https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse

Takes an async generator or a normal generator/iterator and streams the response body.

HTTPXからOpenAIのChat completions APIへのstream呼び出しは方法が分かっているので、それを使って実装しました。

@app.post("/")
async def stream(request: Request, x_github_token: str = Header(None)):
    # この引数たちは後述します

    # 省略

    def pass_generator():
        with httpx.stream(
            "POST",
            "https://api.githubcopilot.com/chat/completions",
            headers=headers,
            json=data,
        ) as response:
            for chunk in response.iter_lines():
                if chunk:
                    yield f"{chunk}\n\n"

    return StreamingResponse(pass_generator(), media_type="text/event-stream")

このFastAPIアプリは、GitHubhttps://api.githubcopilot.com/chat/completions でChat completionsさせます(stream呼び出し)。
そしてその返り値(Server-sent events)を、ジェネレータで処理します。
このジェネレータはServer-sent eventsを崩さないことを責務に実装しました。
GitHubのホストするLLM APIが返すServer-sent eventsをそのまま、FastAPIアプリ呼び出し側(=Copilot Chatの画面)に返しています。

2️⃣リクエストの扱い

Copilot Chatから、GitHub Copilot Extensions(今回はFastAPIアプリ)に送るリクエストの扱いについて2点あります。

ヘッダのX-Github-Token

再び「エージェントに Copilot の LLM を使用する」というドキュメントより

認証するには、エージェントに送信されたのと同じ X-Github-Token ヘッダーを使用します。

このヘッダ、FastAPIだとパス関数の引数として受け取れるんですね!

@app.post("/")
async def stream(
    request: Request, x_github_token: Annotated[str | None, Header()] = None
):
    ...

これでx_github_tokenという変数が、X-Github-Token ヘッダーの値を指します!

ボディのmessages

FastAPIにはリクエストボディを受け取る方法のドキュメントもあります。
https://fastapi.tiangolo.com/tutorial/body/
Pydanticを使って、リクエストボディをパースするクラスを定義します。

ですが今回はPydanticを使わずに、Requestクラスを直接使いました3
これはFastAPIがラップしているstarletteのクラスのようです。

Requestクラスのjson()メソッド4がawaitableなので、パス関数としては(awaitが書けるように)async defになりました5

@app.post("/")
async def stream(request: Request, x_github_token: str = Header(None)):
    payload = await request.json()

    # 省略

payload["messages"]にユーザが@して送ったテキストが入っています。
Node.js版同様に出力しておくと、どんな形式か掴むことができます。
(Copilot Chatからのリクエストの形式も少し分かってきたので、Pydanticを使うように変えてもいいかもしれません)

終わりに

Blackbeardを例に、FastAPIでGitHub Copilot Extensionsを実装しての学びを記しました。

  • GitHubがホストするLLMのWeb API(OpenAI API互換)のChat completionsをstream呼び出しする
  • Server-sent eventsで返るChat completionsをGitHub Copilot Extensionsではそのまま受け流し、Copilot Chatの画面側に返す
  • GitHubがホストするLLM APIを叩くための認証情報やユーザのチャットメッセージは、リクエストのヘッダやボディから得る

BlackbeardはHello world的な単純な例ですが、GitHubがホストするLLM APIのレスポンスをCopilot Chat側へ受け流すという点は複雑なアプリになっても変わらないと考えています。
例えば特定リポジトリのRAGをするようなCopilot Extensionの場合は、GitHubの各種APIを用いてmessagesを増強し、それをGitHubがホストするLLM APIに送り、そのレスポンスを受け流して画面に戻すという実装になるでしょう(手を動かしてこの理解が誤ってないか確認せねば💪)

もしもあなたにも GitHub Copilot Extensionsを実装することがあれば
この記事を思い出して GitHubがホストするLLM APIのレスポンスを受け流してほしい


  1. 海賊としてCopilot Chatに応じてくれます
  2. LTのアーカイブ (1:24:55〜)
  3. Node.jsでの実装例をLLMに入力して、FastAPIでの実装に変換させたものをドラフトするという進め方にしたところ、Requestを知りました
  4. https://fastapi.tiangolo.com/reference/request/#fastapi.Request.json
  5. 先日のPyCon JPでFastAPIにおけるdefasync defの違いを知りました