やり残していたことの答えっぽいもの

unionsep

ClaudeOpenAPIPostman

754 Words

2025-12-05 19:00 -0800


これは Kyash Advent Calendar 2025 の6日目の記事です。

年の瀬ですね。今年は おかあさんといっしょファミリーコンサート に行ってきまして、ゆういちろうおにいさんの歌唱を生で見て感動したのが印象的でした。毎朝見慣れているうたのおにいさんおねえさん、たいそうのおにいさんおねえさんですが、ステージ上の彼らは煌びやかで、感動的でした。

ステージといえば、今年は Kyash TechTalk #8 - スポットマネー開発の裏側 という、弊社主催のイベントのスピーカーとして壇上に立って発表してきました。おにいさん、おねえさんのように、うまく立ち回ることができず、また久々の登壇機会だったのでしどろもどろしてしまいましたが、なんとか体裁だけは保てたように思います。

この資料の以下のページでも話してきたのですが、今回のブログは そんなにうまいことできていない ことがテーマです。

業務の都合上、ExcelのAPI仕様書をPDFに変換し、Geminiを使ってOpenAPI形式のyamlファイルを生成したのですが、これがなかなか手間のかかる作業でした。当時はブラウザでGeminiにアクセスし、PDFファイルをアップロードしてAIに指示(Prompt)を出してYAMLを表示してもらってコピーしてローカルファイルにペーストしてGitにPushする流れでした。 作業の流れはPromptの内容がキモだったし、そこがみんなでワイワイできるポイントだったのですが、それ以外の作業であるアップロードやファイルコピー、Git操作がわりと面倒で、Promptの再現性を保つためにスイッチングコストが結構ばかにならないと感じていました。

また、単発的にPromptを何度も試したので、再現性を保てず、前回はどういう工夫をして、それのなにが良かったんだっけ?みたいなのが抜け落ちるのをもったいなく思っていました。

スライドにも書きましたが、CLIツールを利用したほうがタイプ量も減るし、私には合っている気がしていました。また、GitHub ActionsにできたらPull Requestを複数人でレビューできるので、より簡単で再現性を保てるのではないかと考え、年の瀬の駆け込みでActionsを作ってみました。


作ったActionsは、リポジトリのdocs配下にあるPDFファイルを読み込んで、AIに解析してもらい、それを元に生成したyamlファイルをコミットしてPull Requestを作る機能をつけました。個人的にAPI Keysが取得しやすかったので、AIはClaude Sonnet 4.5にしました。 また、push毎にActionsを実行してトークンを不用意に使いすぎてしまうことを恐れ、 workflow_dispatch で手動実行することにしました。

  • actions yaml
name: Generate YAML from Claude

on:
  workflow_dispatch: 

jobs:
  generate-claude:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:

      # 省略

      - name: Install Python dependencies
        run: |
          pip install -r .github/scripts/requirements_generate_claude_from_pdf.txt

      - name: Generate Claude YAML from PDFs
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python .github/scripts/generate_claude_from_pdf.py

      - name: Create Pull Request
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Git Configuration
          git config --global user.email "[email protected]"
          git config --global user.name "kyash-bot"

          # Create new branch name
          BRANCH_NAME="auto-generate-claude-$(date +%Y%m%d-%H%M%S)"

          # Checkout new branch
          git checkout -b "$BRANCH_NAME"

          # Add generated file to git
          git add docs/openapi.yaml
          git commit -m "Generate OpenAPI YAML from PDF specifications

          This YAML file was automatically generated by Claude AI from PDF specifications in docs/spec/.
          
          Important: Please review the generated content carefully as it may contain errors.
          
          Generated with Claude Code"

          # Push branch to origin
          git push origin "$BRANCH_NAME"

          # Create Pull Request
          gh pr create \
            --title "Generate Claude YAML from PDFs API specifications" \
            --body "## Summary
            
            This PR contains an automatically generated OpenAPI YAML file created from PDF specifications in `docs/spec/`.
            
            ## Generated File
            - `docs/openapi.yaml`
            - OpenAPI 3.0.3 specification
            
            ## Source PDFs
            
            The following PDF files were processed:
            $(ls -1 docs/spec/*.pdf | sed 's/^/- /')
            
            ## Important Notes
            
            **This file was generated by AI (Claude)** - Please review carefully:
              
            - [ ] Verify all API endpoints are correct
            - [ ] Check request/response schemas
            - [ ] Validate parameter types and constraints
            - [ ] Confirm field descriptions are accurate
            - [ ] Review error response definitions
            
            ## Next Steps
            
            1. Review the generated YAML file
            2. Make any necessary corrections
            3. Approve and merge if satisfied
            
            Generated with [Claude Code](https://claude.com/claude-code)"
  • generate python script
#!/usr/bin/env python3

# 省略

def extract_api_spec_from_pdf(client: anthropic.Anthropic, pdf_path: str, pdf_name: str) -> Dict[str, Any]:
    prompt = """このPDFファイルは日本語で書かれたAPI仕様書です。
このAPI仕様書を分析して、OpenAPI 3.0.3形式のYAML仕様の一部として出力してください。

以下の情報を抽出してください:
1. APIエンドポイント(パス)
2. HTTPメソッド(GET, POST, PUT, DELETEなど)
3. リクエストパラメータ(クエリ、パス、ボディ)
4. リクエストボディのスキーマ(該当する場合)
5. レスポンスの形式とスキーマ
6. 各フィールドの説明、型、制約(必須/任意、最大長など)
7. エラーレスポンスの定義

出力は以下の形式のJSONで返してください:

# 省略

重要な注意事項:
- 日本語の説明はそのまま維持してください
- データ型は適切なOpenAPI型(string, integer, number, boolean, array, objectなど)に変換してください
- 数値型の場合、整数はinteger、小数はnumberを使用してください
- 必須/任意の情報は required 配列に反映してください
- レスポンス例がある場合は examples に含めてください
- エラーレスポンスも responses に含めてください(400, 401, 500など)
- 出力は有効なJSONのみで、説明文は含めないでください
"""

    print(f"  Sending request to Claude API...")
    message = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=8192,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "document",
                        "source": {
                            "type": "base64",
                            "media_type": "application/pdf",
                            "data": pdf_data,
                        },
                    },
                    {
                        "type": "text",
                        "text": prompt
                    }
                ],
            }
        ],
    )

    # レスポンスからテキストを抽出
    response_text = message.content[0].text
    print(f"  Received response from Claude API")

    try:
        spec = json.loads(response_text)
        print(f"  ✓ Successfully parsed API spec")
        return spec
    except json.JSONDecodeError as e:
        print(f"  ✗ Error parsing JSON: {e}")
        print(f"  Response preview: {response_text[:300]}...")
        return None

def main():
    print("=" * 60)
    print("PDF to OpenAPI YAML Generator")
    print("=" * 60)

    # 環境変数からAPIキーを取得
    api_key = os.getenv('ANTHROPIC_API_KEY')
    if not api_key:
        print("❌ Error: ANTHROPIC_API_KEY environment variable is not set")
        sys.exit(1)

    # Claude APIクライアントを初期化
    client = anthropic.Anthropic(api_key=api_key)
    print("✓ Claude API client initialized\n")

    # PDFファイルのディレクトリ
    pdf_dir = Path('docs/spec')
    if not pdf_dir.exists():
        print(f"❌ Error: Directory {pdf_dir} does not exist")
        sys.exit(1)

    # PDFファイルを取得(.DS_Storeなどを除外)
    pdf_files = sorted([f for f in pdf_dir.glob('*.pdf') if not f.name.startswith('.')])
    print(f"Found {len(pdf_files)} PDF file(s) in {pdf_dir}\n")

    if not pdf_files:
        print("❌ Error: No PDF files found")
        sys.exit(1)

    # 各PDFからAPI仕様を抽出
    api_specs = []
    for i, pdf_file in enumerate(pdf_files, 1):
        print(f"[{i}/{len(pdf_files)}] Processing: {pdf_file.name}")
        try:
            spec = extract_api_spec_from_pdf(client, str(pdf_file), pdf_file.name)
            if spec:
                api_specs.append(spec)
                print(f"  ✓ Successfully extracted spec from {pdf_file.name}\n")
            else:
                print(f"  ⚠ Failed to extract spec from {pdf_file.name}\n")
        except Exception as e:
            print(f"  ❌ Error processing {pdf_file.name}: {e}\n")
            continue

    if not api_specs:
        print("❌ Error: No API specifications were extracted")
        sys.exit(1)

    print("=" * 60)
    print(f"✓ Successfully extracted {len(api_specs)} API specification(s)")
    print("=" * 60)

    # OpenAPI仕様にマージ
    print("\nMerging API specifications into OpenAPI format...")
    openapi_spec = merge_api_specs_to_openapi(api_specs)
    print(f"✓ Merged into single OpenAPI specification")
    print(f"  Total paths: {len(openapi_spec['paths'])}")

    # YAMLファイルとして出力
    output_file = Path('docs/openapi.yaml')
    output_file.parent.mkdir(parents=True, exist_ok=True)

    print(f"\nWriting to {output_file}...")
    with open(output_file, 'w', encoding='utf-8') as f:
        yaml.dump(openapi_spec, f, allow_unicode=True, sort_keys=False, default_flow_style=False)

    print("=" * 60)
    print(f"✅ SUCCESS: OpenAPI specification written to {output_file}")
    print("=" * 60)
    print("\n⚠️  重要: 生成されたYAMLファイルは必ずレビューしてください")
    print("   AIが生成した内容のため、誤りがある可能性があります\n")

if __name__ == '__main__':
    main()

上記のコードは大部分を端折っているので、そのままコピーしても動作しませんが、動作イメージは掴んでもらえるのではないでしょうか。promptという変数にClaude APIに指示するテキストを入れています。ファイルからプロンプト文字列を読み込ませたかったのですが、年末の駆け込み作業で時間をあまりかけられなかったため、そのまま埋め込んでいます。

なお、ぱっと見、絵文字があるしなんとなくおわかりだと思いますが、大部分をClaude codeに書いてもらいました。12月になって月が変わったので、なんとなくみんなが使っているClaude Proプランを契約してClaude codeに任せてみました。GitHub Actionsってあまり頻繁にいじらないので、 steps だっけ? jobs だっけ? name いるんだっけ?って毎回なっていたので雛形作ってくれるのはありがたかったです。また、Claude Code GitHub Actionsを使うのかなって思ってたんですが、anthropic-sdk-python で作ろうとしてたので、内容を見てそんな難しいことしてなさそうだったのでPython scriptを作ってもらいました。


イメージ的には以下のような使い方になります。 PDFのAPI仕様書をGit mergeして、Run workflowを押すとyamlファイルをcommitしたPull Requestが自動的にできて、メンバーでレビューするようなフローになります。

フロー図

仕様書が来たら、GitHubにmergeして以下のActionsを実行します。そうすると、branchを作って生成したyamlファイルをコミットしてPull Requestを作ってくれます。

キャプチャ

DescriptionもClaude codeが作ってくれました。使ったPDFファイルも書いてるし、作成したyamlファイルも書いているのでわかりやすい感じに仕上がりました。このPull Requestを元に、API仕様についてメンバーで議論したら良いのではないでしょうか。また、このPull RequestをmergeしたらOpenAPIのhtmlファイルを出力し、GitHub Pagesで社内公開できれば完璧です。

キャプチャ

これを作ったきっかけは、巷を騒がせているAIってどうやったら活用できるのかなと考えていたことに起因します。 私自身、AIの進化のスピードは早いなと驚いていたのですが、ChatGPTが良いとかClaudeがより賢い、Geminiが追いついたみたいな話題にあまり関心を持てていませんでした。もちろん、業務で使っていたので、どのAIサービスがより良い回答が返ってくるかな〜などと、漠然とは感じていたのですが、それはたまたまその時のPromptがそうだっただけなのでは?なんて思っていたりもしていました。

また、Claude codeやCodexなどのCLIを使ってコードを書いてもらったりもしましたが、開発業務の効率が高まったか?と問われれば、う〜ん・・・と疑問を感じてしまいます。

今回、これをやってみて思ったのは、私がAIにカバーして欲しい領域は不確実性や不透明性が高い部分や理解が足りていない部分、例えば、フォーマットや文体が揃っていないドキュメントを扱う時や初めて書くコードベースのフォローかなと思いました。

開発業務を行うとき、私的に一番時間がかかるなと感じていたのは調査の時間であり、コードを書いているときはさほど時間がかかっていると感じていないのではないかと思います。たとえ時間がかかったとしても、そんなに苦に感じていないなぁと思っています。

なので、今回のようにフォーマットが揃っていない仕様書を扱う場合にとても有効に働いてくれたと感じています。形式が決まっていて、どこに何が書いてあるのか判然としている場合、プログラミングで自動化できますが、そうではない場合、人間の脳みそを使ってノイズと戦いながら読み進めなければならず、文脈を整理したり行間を読んだりすることに少々疲れてしまいます。ということは、私は文章を読んだり筆者の思いを深堀りすることをあまり本質的に得意としていないのかな。。

多分、私は開発の作業が好きなので、すでに手癖もあるし自分が効率的に作業できるようにPCもカスタマイズしているので、いちいちAIにその背景を伝えるのを億劫に感じているのかもしれません。 であるなら、自分の知識が足りていないところの理解を深めるためにAIを活用して、能力を高めるためにAIと向き合っていくのが今のところ私にとっていいのかなと思っています。

余談ですが、AIを使い始めてから本をよく読むようになりました。不思議ですね。Fact Checkのためでもあるのですが、多分、普通にAIに自然言語で指示するのが億劫なんでしょうね。生身の人間相手にはそうでもないのに、AIには面倒に感じる自分に少々驚いています。

明日の投稿もぜひお楽しみに。バイバーイ。