<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>OpenAPI on notes</title><link>https://unionsep.dev/tags/openapi/</link><description>Recent content in OpenAPI on notes</description><generator>Hugo -- 0.161.1</generator><language>en</language><lastBuildDate>Sat, 06 Dec 2025 12:00:00 +0900</lastBuildDate><atom:link href="https://unionsep.dev/tags/openapi/index.xml" rel="self" type="application/rss+xml"/><item><title>やり残していたことの答えっぽいもの</title><link>https://unionsep.dev/posts/2025/12/06/120000/</link><pubDate>Sat, 06 Dec 2025 12:00:00 +0900</pubDate><author>unionsep</author><guid>https://unionsep.dev/posts/2025/12/06/120000/</guid><description>&amp;lt;no value&amp;gt;</description><content type="text/html" mode="escaped"><![CDATA[<p>これは <a href="https://adventar.org/calendars/11602">Kyash Advent Calendar 2025</a> の6日目の記事です。</p>
<p>年の瀬ですね。今年は <a href="https://www.nhk-p.co.jp/event/okasan/2025-26/tokyo2.html">おかあさんといっしょファミリーコンサート</a> に行ってきまして、ゆういちろうおにいさんの歌唱を生で見て感動したのが印象的でした。毎朝見慣れているうたのおにいさんおねえさん、たいそうのおにいさんおねえさんですが、ステージ上の彼らは煌びやかで、感動的でした。</p>
<p>ステージといえば、今年は <a href="https://kyash.connpass.com/event/366479/">Kyash TechTalk #8 - スポットマネー開発の裏側</a> という、弊社主催のイベントのスピーカーとして壇上に立って発表してきました。おにいさん、おねえさんのように、うまく立ち回ることができず、また久々の登壇機会だったのでしどろもどろしてしまいましたが、なんとか体裁だけは保てたように思います。</p>
<aside class="cite-card">
    <img
      class="cite-card__image"
      src="https://files.speakerdeck.com/presentations/0b150108051048a4ab86c907a0f30b1a/slide_0.jpg?37054925"
      alt=""
      loading="lazy"
    >

  <a href="https://speakerdeck.com/unionsep/kyash-x-postman-powering-a-culture-of-quality" target="_blank" rel="noopener noreferrer">
    <div class="cite-card__title">Kyash x Postman: Powering a Culture of Quality</div>
      <div class="cite-card__description">
        https://kyash.connpass.com/event/366479/
      </div>

    <div class="cite-card__site">Speaker Deck</div>
  </a>
</aside>

<p>この資料の以下のページでも話してきたのですが、今回のブログは <strong>そんなにうまいことできていない</strong> ことがテーマです。</p>
<div class="speakerdeck">
        <iframe
          src="https://speakerdeck.com/player/0b150108051048a4ab86c907a0f30b1a?slide=21"
          width="710"
          height="399"
          style="width: 100%; aspect-ratio: 710 / 399; border: 0;"
          frameborder="0"
          allowtransparency="true"
          allowfullscreen>
        </iframe>
      </div>
<p>業務の都合上、ExcelのAPI仕様書をPDFに変換し、Geminiを使ってOpenAPI形式のyamlファイルを生成したのですが、これがなかなか手間のかかる作業でした。当時はブラウザでGeminiにアクセスし、PDFファイルをアップロードしてAIに指示（Prompt）を出してYAMLを表示してもらってコピーしてローカルファイルにペーストしてGitにPushする流れでした。
作業の流れはPromptの内容がキモだったし、そこがみんなでワイワイできるポイントだったのですが、それ以外の作業であるアップロードやファイルコピー、Git操作がわりと面倒で、Promptの再現性を保つためにスイッチングコストが結構ばかにならないと感じていました。</p>
<p>また、単発的にPromptを何度も試したので、再現性を保てず、前回はどういう工夫をして、それのなにが良かったんだっけ？みたいなのが抜け落ちるのをもったいなく思っていました。</p>
<p>スライドにも書きましたが、CLIツールを利用したほうがタイプ量も減るし、私には合っている気がしていました。また、GitHub ActionsにできたらPull Requestを複数人でレビューできるので、より簡単で再現性を保てるのではないかと考え、年の瀬の駆け込みでActionsを作ってみました。</p>
<hr>
<p>作ったActionsは、リポジトリのdocs配下にあるPDFファイルを読み込んで、AIに解析してもらい、それを元に生成したyamlファイルをコミットしてPull Requestを作る機能をつけました。個人的にAPI Keysが取得しやすかったので、AIはClaude Sonnet 4.5にしました。
また、push毎にActionsを実行してトークンを不用意に使いすぎてしまうことを恐れ、 <code>workflow_dispatch</code> で手動実行することにしました。</p>
<ul>
<li>actions yaml</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Generate YAML from Claude</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>: 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">generate-claude</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pull-requests</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># 省略</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install Python dependencies</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          pip install -r .github/scripts/requirements_generate_claude_from_pdf.txt</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Generate Claude YAML from PDFs</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ANTHROPIC_API_KEY</span>: <span style="color:#ae81ff">${{ secrets.ANTHROPIC_API_KEY }}</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          python .github/scripts/generate_claude_from_pdf.py</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create Pull Request</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">GH_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.GITHUB_TOKEN }}</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          # Git Configuration
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          git config --global user.email &#34;kyash-bot@kyash.co&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          git config --global user.name &#34;kyash-bot&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          # Create new branch name
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          BRANCH_NAME=&#34;auto-generate-claude-$(date +%Y%m%d-%H%M%S)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          # Checkout new branch
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          git checkout -b &#34;$BRANCH_NAME&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          # Add generated file to git
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          git add docs/openapi.yaml
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          git commit -m &#34;Generate OpenAPI YAML from PDF specifications
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          This YAML file was automatically generated by Claude AI from PDF specifications in docs/spec/.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          Important: Please review the generated content carefully as it may contain errors.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          Generated with Claude Code&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          # Push branch to origin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          git push origin &#34;$BRANCH_NAME&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          # Create Pull Request
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          gh pr create \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --title &#34;Generate Claude YAML from PDFs API specifications&#34; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            --body &#34;## Summary
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            This PR contains an automatically generated OpenAPI YAML file created from PDF specifications in `docs/spec/`.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ## Generated File
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            - `docs/openapi.yaml`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            - OpenAPI 3.0.3 specification
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ## Source PDFs
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            The following PDF files were processed:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            $(ls -1 docs/spec/*.pdf | sed &#39;s/^/- /&#39;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ## Important Notes
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            **This file was generated by AI (Claude)** - Please review carefully:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            - [ ] Verify all API endpoints are correct
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            - [ ] Check request/response schemas
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            - [ ] Validate parameter types and constraints
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            - [ ] Confirm field descriptions are accurate
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            - [ ] Review error response definitions
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ## Next Steps
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            1. Review the generated YAML file
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            2. Make any necessary corrections
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            3. Approve and merge if satisfied
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            Generated with [Claude Code](https://claude.com/claude-code)&#34;</span>
</span></span></code></pre></div><ul>
<li>generate python script</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 省略</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">extract_api_spec_from_pdf</span>(client: anthropic<span style="color:#f92672">.</span>Anthropic, pdf_path: str, pdf_name: str) <span style="color:#f92672">-&gt;</span> Dict[str, Any]:
</span></span><span style="display:flex;"><span>    prompt <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;&#34;このPDFファイルは日本語で書かれたAPI仕様書です。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">このAPI仕様書を分析して、OpenAPI 3.0.3形式のYAML仕様の一部として出力してください。
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">以下の情報を抽出してください：
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">1. APIエンドポイント（パス）
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">2. HTTPメソッド（GET, POST, PUT, DELETEなど）
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">3. リクエストパラメータ（クエリ、パス、ボディ）
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">4. リクエストボディのスキーマ（該当する場合）
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">5. レスポンスの形式とスキーマ
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">6. 各フィールドの説明、型、制約（必須/任意、最大長など）
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">7. エラーレスポンスの定義
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">出力は以下の形式のJSONで返してください：
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># 省略
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">重要な注意事項：
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- 日本語の説明はそのまま維持してください
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- データ型は適切なOpenAPI型（string, integer, number, boolean, array, objectなど）に変換してください
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- 数値型の場合、整数はinteger、小数はnumberを使用してください
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- 必須/任意の情報は required 配列に反映してください
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- レスポンス例がある場合は examples に含めてください
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- エラーレスポンスも responses に含めてください（400, 401, 500など）
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- 出力は有効なJSONのみで、説明文は含めないでください
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  Sending request to Claude API...&#34;</span>)
</span></span><span style="display:flex;"><span>    message <span style="color:#f92672">=</span> client<span style="color:#f92672">.</span>messages<span style="color:#f92672">.</span>create(
</span></span><span style="display:flex;"><span>        model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;claude-sonnet-4-5-20250929&#34;</span>,
</span></span><span style="display:flex;"><span>        max_tokens<span style="color:#f92672">=</span><span style="color:#ae81ff">8192</span>,
</span></span><span style="display:flex;"><span>        messages<span style="color:#f92672">=</span>[
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;role&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;content&#34;</span>: [
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;document&#34;</span>,
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#34;source&#34;</span>: {
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;base64&#34;</span>,
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">&#34;media_type&#34;</span>: <span style="color:#e6db74">&#34;application/pdf&#34;</span>,
</span></span><span style="display:flex;"><span>                            <span style="color:#e6db74">&#34;data&#34;</span>: pdf_data,
</span></span><span style="display:flex;"><span>                        },
</span></span><span style="display:flex;"><span>                    },
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;text&#34;</span>,
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#34;text&#34;</span>: prompt
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                ],
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># レスポンスからテキストを抽出</span>
</span></span><span style="display:flex;"><span>    response_text <span style="color:#f92672">=</span> message<span style="color:#f92672">.</span>content[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>text
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  Received response from Claude API&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        spec <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>loads(response_text)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  ✓ Successfully parsed API spec&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> spec
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> json<span style="color:#f92672">.</span>JSONDecodeError <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  ✗ Error parsing JSON: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  Response preview: </span><span style="color:#e6db74">{</span>response_text[:<span style="color:#ae81ff">300</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">...&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;=&#34;</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;PDF to OpenAPI YAML Generator&#34;</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;=&#34;</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 環境変数からAPIキーを取得</span>
</span></span><span style="display:flex;"><span>    api_key <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>getenv(<span style="color:#e6db74">&#39;ANTHROPIC_API_KEY&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> api_key:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;❌ Error: ANTHROPIC_API_KEY environment variable is not set&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Claude APIクライアントを初期化</span>
</span></span><span style="display:flex;"><span>    client <span style="color:#f92672">=</span> anthropic<span style="color:#f92672">.</span>Anthropic(api_key<span style="color:#f92672">=</span>api_key)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;✓ Claude API client initialized</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># PDFファイルのディレクトリ</span>
</span></span><span style="display:flex;"><span>    pdf_dir <span style="color:#f92672">=</span> Path(<span style="color:#e6db74">&#39;docs/spec&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> pdf_dir<span style="color:#f92672">.</span>exists():
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;❌ Error: Directory </span><span style="color:#e6db74">{</span>pdf_dir<span style="color:#e6db74">}</span><span style="color:#e6db74"> does not exist&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># PDFファイルを取得（.DS_Storeなどを除外）</span>
</span></span><span style="display:flex;"><span>    pdf_files <span style="color:#f92672">=</span> sorted([f <span style="color:#66d9ef">for</span> f <span style="color:#f92672">in</span> pdf_dir<span style="color:#f92672">.</span>glob(<span style="color:#e6db74">&#39;*.pdf&#39;</span>) <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> f<span style="color:#f92672">.</span>name<span style="color:#f92672">.</span>startswith(<span style="color:#e6db74">&#39;.&#39;</span>)])
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Found </span><span style="color:#e6db74">{</span>len(pdf_files)<span style="color:#e6db74">}</span><span style="color:#e6db74"> PDF file(s) in </span><span style="color:#e6db74">{</span>pdf_dir<span style="color:#e6db74">}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> pdf_files:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;❌ Error: No PDF files found&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 各PDFからAPI仕様を抽出</span>
</span></span><span style="display:flex;"><span>    api_specs <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i, pdf_file <span style="color:#f92672">in</span> enumerate(pdf_files, <span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;[</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>len(pdf_files)<span style="color:#e6db74">}</span><span style="color:#e6db74">] Processing: </span><span style="color:#e6db74">{</span>pdf_file<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            spec <span style="color:#f92672">=</span> extract_api_spec_from_pdf(client, str(pdf_file), pdf_file<span style="color:#f92672">.</span>name)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> spec:
</span></span><span style="display:flex;"><span>                api_specs<span style="color:#f92672">.</span>append(spec)
</span></span><span style="display:flex;"><span>                print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  ✓ Successfully extracted spec from </span><span style="color:#e6db74">{</span>pdf_file<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  ⚠ Failed to extract spec from </span><span style="color:#e6db74">{</span>pdf_file<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  ❌ Error processing </span><span style="color:#e6db74">{</span>pdf_file<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> api_specs:
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">&#34;❌ Error: No API specifications were extracted&#34;</span>)
</span></span><span style="display:flex;"><span>        sys<span style="color:#f92672">.</span>exit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;=&#34;</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;✓ Successfully extracted </span><span style="color:#e6db74">{</span>len(api_specs)<span style="color:#e6db74">}</span><span style="color:#e6db74"> API specification(s)&#34;</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;=&#34;</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># OpenAPI仕様にマージ</span>
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">Merging API specifications into OpenAPI format...&#34;</span>)
</span></span><span style="display:flex;"><span>    openapi_spec <span style="color:#f92672">=</span> merge_api_specs_to_openapi(api_specs)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;✓ Merged into single OpenAPI specification&#34;</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;  Total paths: </span><span style="color:#e6db74">{</span>len(openapi_spec[<span style="color:#e6db74">&#39;paths&#39;</span>])<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># YAMLファイルとして出力</span>
</span></span><span style="display:flex;"><span>    output_file <span style="color:#f92672">=</span> Path(<span style="color:#e6db74">&#39;docs/openapi.yaml&#39;</span>)
</span></span><span style="display:flex;"><span>    output_file<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>mkdir(parents<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, exist_ok<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">Writing to </span><span style="color:#e6db74">{</span>output_file<span style="color:#e6db74">}</span><span style="color:#e6db74">...&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> open(output_file, <span style="color:#e6db74">&#39;w&#39;</span>, encoding<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;utf-8&#39;</span>) <span style="color:#66d9ef">as</span> f:
</span></span><span style="display:flex;"><span>        yaml<span style="color:#f92672">.</span>dump(openapi_spec, f, allow_unicode<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, sort_keys<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>, default_flow_style<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;=&#34;</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;✅ SUCCESS: OpenAPI specification written to </span><span style="color:#e6db74">{</span>output_file<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;=&#34;</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">60</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">⚠️  重要: 生成されたYAMLファイルは必ずレビューしてください&#34;</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;   AIが生成した内容のため、誤りがある可能性があります</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    main()
</span></span></code></pre></div><hr>
<p>上記のコードは大部分を端折っているので、そのままコピーしても動作しませんが、動作イメージは掴んでもらえるのではないでしょうか。promptという変数にClaude APIに指示するテキストを入れています。ファイルからプロンプト文字列を読み込ませたかったのですが、年末の駆け込み作業で時間をあまりかけられなかったため、そのまま埋め込んでいます。</p>
<p>なお、ぱっと見、絵文字があるしなんとなくおわかりだと思いますが、大部分をClaude codeに書いてもらいました。12月になって月が変わったので、なんとなくみんなが使っているClaude Proプランを契約してClaude codeに任せてみました。GitHub Actionsってあまり頻繁にいじらないので、 <code>steps</code> だっけ？ <code>jobs</code> だっけ？ <code>name</code> いるんだっけ？って毎回なっていたので雛形作ってくれるのはありがたかったです。また、<a href="https://code.claude.com/docs/ja/github-actions">Claude Code GitHub Actions</a>を使うのかなって思ってたんですが、anthropic-sdk-python で作ろうとしてたので、内容を見てそんな難しいことしてなさそうだったのでPython scriptを作ってもらいました。</p>
<aside class="cite-card">
    <img
      class="cite-card__image"
      src="https://opengraph.githubassets.com/7ec7a5eba51dada215f59990d8dcf4af885f475986b9fbb067e85da55af6a214/anthropics/anthropic-sdk-python"
      alt=""
      loading="lazy"
    >

  <a href="https://github.com/anthropics/anthropic-sdk-python" target="_blank" rel="noopener noreferrer">
    <div class="cite-card__title">GitHub - anthropics/anthropic-sdk-python</div>
      <div class="cite-card__description">
        Contribute to anthropics/anthropic-sdk-python development by creating an account on…
      </div>

    <div class="cite-card__site">GitHub</div>
  </a>
</aside>

<hr>
<p>イメージ的には以下のような使い方になります。
PDFのAPI仕様書をGit mergeして、Run workflowを押すとyamlファイルをcommitしたPull Requestが自動的にできて、メンバーでレビューするようなフローになります。</p>
<p><a href="/posts/2025/12/06/120000/20251204180918.png"><img src="/posts/2025/12/06/120000/20251204180918.png" alt="フロー図"></a></p>
<p>仕様書が来たら、GitHubにmergeして以下のActionsを実行します。そうすると、branchを作って生成したyamlファイルをコミットしてPull Requestを作ってくれます。</p>
<p><a href="/posts/2025/12/06/120000/20251205145357.png"><img src="/posts/2025/12/06/120000/20251205145357.png" alt="キャプチャ"></a></p>
<p>DescriptionもClaude codeが作ってくれました。使ったPDFファイルも書いてるし、作成したyamlファイルも書いているのでわかりやすい感じに仕上がりました。このPull Requestを元に、API仕様についてメンバーで議論したら良いのではないでしょうか。また、このPull RequestをmergeしたらOpenAPIのhtmlファイルを出力し、GitHub Pagesで社内公開できれば完璧です。</p>
<p><a href="/posts/2025/12/06/120000/20251205150305.png"><img src="/posts/2025/12/06/120000/20251205150305.png" alt="キャプチャ"></a></p>
<p>これを作ったきっかけは、巷を騒がせているAIってどうやったら活用できるのかなと考えていたことに起因します。
私自身、AIの進化のスピードは早いなと驚いていたのですが、ChatGPTが良いとかClaudeがより賢い、Geminiが追いついたみたいな話題にあまり関心を持てていませんでした。もちろん、業務で使っていたので、どのAIサービスがより良い回答が返ってくるかな〜などと、漠然とは感じていたのですが、それはたまたまその時のPromptがそうだっただけなのでは？なんて思っていたりもしていました。</p>
<p>また、Claude codeやCodexなどのCLIを使ってコードを書いてもらったりもしましたが、開発業務の効率が高まったか？と問われれば、う〜ん・・・と疑問を感じてしまいます。</p>
<p>今回、これをやってみて思ったのは、私がAIにカバーして欲しい領域は不確実性や不透明性が高い部分や理解が足りていない部分、例えば、フォーマットや文体が揃っていないドキュメントを扱う時や初めて書くコードベースのフォローかなと思いました。</p>
<p>開発業務を行うとき、私的に一番時間がかかるなと感じていたのは調査の時間であり、コードを書いているときはさほど時間がかかっていると感じていないのではないかと思います。たとえ時間がかかったとしても、そんなに苦に感じていないなぁと思っています。</p>
<p>なので、今回のようにフォーマットが揃っていない仕様書を扱う場合にとても有効に働いてくれたと感じています。形式が決まっていて、どこに何が書いてあるのか判然としている場合、プログラミングで自動化できますが、そうではない場合、人間の脳みそを使ってノイズと戦いながら読み進めなければならず、文脈を整理したり行間を読んだりすることに少々疲れてしまいます。ということは、私は文章を読んだり筆者の思いを深堀りすることをあまり本質的に得意としていないのかな。。</p>
<p>多分、私は開発の作業が好きなので、すでに手癖もあるし自分が効率的に作業できるようにPCもカスタマイズしているので、いちいちAIにその背景を伝えるのを億劫に感じているのかもしれません。
であるなら、自分の知識が足りていないところの理解を深めるためにAIを活用して、能力を高めるためにAIと向き合っていくのが今のところ私にとっていいのかなと思っています。</p>
<p>余談ですが、AIを使い始めてから本をよく読むようになりました。不思議ですね。Fact Checkのためでもあるのですが、多分、普通にAIに自然言語で指示するのが億劫なんでしょうね。生身の人間相手にはそうでもないのに、AIには面倒に感じる自分に少々驚いています。</p>
<p>明日の投稿もぜひお楽しみに。バイバーイ。</p>
]]></content></item></channel></rss>