はじめに
右から左へ〜 受け流す〜♪ nikkieです。
Blackbeard1を例に、GitHub Copilot Extensionsを実装して知ったことを記事にします。
目次
- はじめに
- 目次
- LT「PythonでもGitHub Copilot Extensions作れるもん!」
- 読み解きNode.js版Blackbeard
- 1️⃣StreamingResponse
- 2️⃣リクエストの扱い
- 終わりに
LT「PythonでもGitHub Copilot Extensions作れるもん!」
#stapy LTご清聴いただきありがとうございました!
— nikkie / にっきー (@ftnext) 2024年11月14日
「PythonでもGitHub Copilot Extensions作れるもん!」https://t.co/B3lCKFDMbM
FastAPIで作れました!以下はPythonで動いています🙌https://t.co/stSzzYQYzh pic.twitter.com/uNVYWixmrG
GitHub Copilot Extensionsとは、Copilot Chatで@
する相手を作れるというもの。
OpenAIのGPTsのようなカスタムなLLMを、プログラミングして作れます!
これは私のハッカー魂をめちゃめちゃくすぐっており、手に馴染むPythonでせひやりたい!と先日FastAPIでの実装例を公開しました2。
FastAPIで実装する中で学んだことを紹介していきます。
読み解きNode.js版Blackbeard
Node.jsでの実装例を読み解き、FastAPIで実装しました。
// 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として戻しました。
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アプリは、GitHubの https://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のレスポンスを受け流してほしい