👾

【脱法AIエージェント】社内規制をかいくぐってカスタムGPTを使いこなす

に公開

この記事を読んだらできること


はじめまして、ふっきーです。
突然ですが、みなさんは仕事で自由にLLMサービスを使えますか?

背景

自分が勤める会社では、コンプライアンスやリスクばかり考えていて、LLM系サービスを自由に使えません。

気軽に使えるのはCopilotぐらいで、他のサービスは暗黒大陸かっていうぐらい手続きが多く、規制もきついので、実質使えないという状況です。

ChatGPTの有償版は社内情報入力可能で、手続きから比較的早く利用可能になる(それでも1か月弱待つ)ので、社内で自分がメインで使っているサービスになります。

とはいえ、チャットぐらいしかできることがなく、エージェント的なサービス使いたいなぁと思っていたところ、ふと思いついたアイデアが思いのほかうまくいったので、共有します。

カスタムGPTをAIエージェント化させる方法

カスタムGPTには、Actionという形でRestAPIを呼び出させる機能があります。

本来の使い方なら、公開されているWebAPIを登録して、動的に天気を取得したりなんなりするところかと思います。

そうではなく、localtunnelを使って、Flaskで作ったローカルAPIサーバーをMyGPT使わせるというのが、今回のアイデアです。

上記の方法により、自前でFunctionCallingを定義できる汎用AIエージェント化させることができました。

まずはリバースプロキシの設定

当たり前ですがローカルで立てたAPIサーバーのエンドポイントhttp://localhost:5000は、MyGPTからは見えないので、リバースプロキシを使って見えるようにしないといけないです。

今回は、無料で使い勝手の良かったlocaltunnnelを使ってみました。

こんな感じのコマンドで設定可能です。
npx localtunnel --port 5000 --subdomain 任意のドメイン

subdomainは好きなドメイン指定可能ですが、早い者勝ちです。

詳しくはこちらのわかりやすい記事を参照してください
https://www.geeklibrary.jp/counter-attack/localtunnel/

ローカルAPIサーバーの準備

Flaskを使ってローカルAPIサーバーを立てます。

外部から呼び出し可能になっているため、必ず認証設定をしておいてください
自分はAPIキーの設定をしています。

import base64
from flask import request, abort

API_KEY = "任意のAPIキー"

def check_basic_api_key():
    auth_header = request.headers.get("Authorization")
    print(auth_header)
    if not auth_header or not auth_header.startswith('Basic '):
        print("Empty auth header")
        abort(401)
    
    api_key = auth_header.split(' ')[1]
    if api_key != API_KEY:
        print(f"Wrong API Key {api_key}")
        abort(401)

次にAPI側の実装です。
試しに、UNIX系コマンドを実行するAPIを作って、ポートを指定して起動します。
指定するポートは、localtunnelで指定したものと一致させてください。

from flask import Flask, request, jsonify, Response
import subprocess, shlex, os, platform

from auth.ap_key_auth import check_basic_api_key

app = Flask(__name__)
app.before_request(check_basic_api_key)  # APIキーによる認証

@app.route("/run", methods=["POST"])
def run_command():
    data = request.get_json(force=True)
    cmd_str = data.get("command")
    if not cmd_str:
        return jsonify(error="command is required"), 400

    try:
        result = subprocess.check_output(
            cmd_str,
            stderr=subprocess.STDOUT,
            timeout=10,
            text=True,
            shell=True
        )
        return jsonify(output=result, returncode=0)
    except FileNotFoundError as e:
        return jsonify(error=f"File not found: {e.filename}"), 400
    except subprocess.CalledProcessError as e:
        return jsonify(error="Command failed", output=e.output,
                       returncode=e.returncode), 400
    except subprocess.TimeoutExpired:
        return jsonify(error="Command timed out"), 408


if __name__ == "__main__":
    app.run(port=5000, debug=False)

カスタムGPTのActionに登録する

API呼び出しのためにスキームを登録しないといけないです。
上記のソースコードとスキームのサンプルをLLMに入力させれば、作ったAPIのスキームをいい感じに作ってくれます。

先ほど作成したAPIのスキーム

openapi: 3.1.0
info:
  title: Local Utilities API
  version: "1.0.0"
  description: |
    ローカルファイル操作およびコマンド実行を提供する API。
    - `/run`: 任意のUNIX系コマンドをローカル実行し結果を返す
  license:
    name: MIT
servers:
  - url:  https://任意のドメイン.loca.lt
    description: ローカル開発サーバ

tags:
  - name: command
    description: ターミナルコマンド実行エンドポイント

paths:
  /run:
    post:
      tags: [command]
      summary: ターミナルコマンドを実行
      operationId: runCommand
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RunRequest"
            examples:
              lsExample:
                summary: ディレクトリ一覧(Unix系)
                value:
                  command: ls -l
              dirExample:
                summary: ディレクトリ一覧(Windows)
                value:
                  command: dir
      responses:
        "200":
          description: コマンド実行成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunSuccess"
        "400":
          description: リクエスト不正またはコマンド失敗
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunError"
        "408":
          description: コマンドタイムアウト
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RunTimeout"

components:
  schemas:
    RunRequest:
      type: object
      required: [command]
      properties:
        command:
          type: string
          description: 実行したいターミナルコマンド
      example:
        command: echo hello

    RunSuccess:
      type: object
      properties:
        output:
          type: string
          description: 標準出力+標準エラー出力(結合済み)
        returncode:
          type: integer
          format: int32
          description: プロセスの終了コード(0で成功)
      required: [output, returncode]

    RunError:
      type: object
      properties:
        error:
          type: string
          description: エラーメッセージ
        output:
          type: string
          description: コマンド実行時に得られた出力(あれば)
        returncode:
          type: integer
          format: int32
          description: プロセスの終了コード(0 以外)
      required: [error, returncode]

    RunTimeout:
      type: object
      properties:
        error:
          type: string
          description: タイムアウト時のメッセージ
      required: [error]

動作確認

一応テストしてみましょう
適当に作ったローカルフォルダ内のファイル一覧を取得してみます。

いい感じですね
コマンド実行ができれば、ファイルの読み書き、gitの操作などさまざまなことが可能になります。

あとは、プロジェクトの内容や、やりたいことに沿って新しいAPIを用意したり、システムプロンプトを調整したりするだけです。

まとめ

上記の設定により、カスタムGPTを汎用AIエージェントのように扱うことができます。
自分は複数のポートを開けて、メールの確認、TODOタスクの追加/更新、作業の記録など、管理作業系を全て1つのカスタムGPTに任せることにしました。

メールの確認してもらって、やることあればTODOに追加 → TODO実行に必要な情報をWeb検索し、記録しておく みたいなフローが勝手に流れます。

もし同じ境遇の方がいたら試してみてください。

Discussion