openapi: 3.0.3
info:
  title: ベテランAI 外部API
  version: "1.4.0"
  description: |
    社内ナレッジ支援AI「ベテランAI」の外部公開API。

    会社のナレッジ（議事録・チャット・ファイル等）に対して質問すると、
    RAG検索＋LLMで回答と出典を返します。

    この仕様書は次のURLで直接取得できます（Make / Zapier / n8n / Postman の
    「URLからインポート」にそのまま貼れます）:

        https://tool.veteranai.jp/api/v1/openapi-public.yaml

    ## 認証
    すべてのリクエストに `X-API-Key` ヘッダが必要です（値は `va_` で始まるキー）。
    キーはベテランAIの「設定」画面でオーナーが発行します（発行時に1回だけ表示）。
    Cookie認証ではないため CSRFトークンは不要です。

    ## 権限範囲（スコープ）
    キーには発行時に「何ができるか」の範囲が付きます。**必要な範囲は経路ごとに違います。**

      - `chat` … `POST /api/v1/chat`・`POST /api/v1/chat/stream`・MCP（発行時の既定）
      - `knowledge.read` … `GET /api/v1/files`（**既定の鍵には付いていません**）
      - `crm.read` … CRMの読み取り（顧客・タスク・要フォロー一覧。**既定の鍵には付いていません**）

    範囲が足りないキーで呼ぶと 403 が返り、本文に不足している範囲名が入ります。
    範囲は**後から変更できない**ので、足りない場合はキーを発行し直してください。

    ## 公開範囲
    この仕様書には、**APIキーで外から呼べる経路をすべて**記載しています
    （質問→回答、資料一覧）。管理系（ユーザー管理・請求・連携設定など）は
    外部APIでは提供しません。
    資料一覧が返すのはファイル名・種別・状態までで、**本文は含みません**
    （中身が要る場合は chat で質問してください）。

    同じ chat を MCP（AIツール用の接続方式）としても公開しています。
    Claude Desktop 等から直接ナレッジに質問したい場合はそちらを使ってください
    （エンドポイント `POST /mcp`、認証は同じ `X-API-Key`、ツール名 `ask_knowledge`）。

    ## レート制限
    chat は1分間に30回まで、資料一覧は1分間に60回まで。
    加えて会社ごとの利用量上限があります。

    ## 呼び出す場所（重要）
    **サーバ側から呼んでください。ブラウザの JavaScript から直接は呼べません。**

    外部オリジンからの呼び出しは許可していないため（CORS）、ブラウザから叩くと
    **原因の分かりにくいネットワークエラー**になります。
    そもそも APIキーをブラウザに置くと閲覧者に見えてしまうため、
    鍵は必ずサーバ側に置いてください。

    画面に出したい場合は、自社のサーバを1枚挟んでそこから呼び出す構成にしてください。

    ## 応答時間（重要）
    chat は検索と生成を行うため、**回答まで20秒前後かかります**
    （本番151件の実測: 中央値 19秒 / 9割が35秒以内 / 最大 54秒）。

    **約2割が30秒を超えます。** 呼び出し側の**タイムアウトは90秒以上**に
    してください。ノーコードツールの既定値（30〜40秒）のままだと、
    正常に処理されている呼び出しが5回に1回ほど打ち切られます。

    画面に出す用途では `POST /api/v1/chat/stream` を使うと、
    最初の文字が数秒で届くため待たされている感じがありません。
servers:
  - url: https://tool.veteranai.jp
    description: 本番（推奨・独自ドメイン）
  - url: https://veteran-ai-api-docker.onrender.com
    description: バックエンド直叩き（ストリーミングで proxy を避けたい場合など）

security:
  - ApiKeyAuth: []

paths:
  /api/v1/chat:
    post:
      operationId: askKnowledge
      summary: ナレッジに質問して回答を得る（非ストリーミング）
      description: 質問を送ると、回答テキストと出典のリストをJSONで返します。
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatRequest"
            examples:
              simple:
                summary: シンプルな質問
                value:
                  message: "先月の山田さんとの商談の要点は？"
              filtered:
                summary: ソースと期間を絞る
                value:
                  message: "今期の受注見込みは？"
                  filters:
                    sources: ["circleback", "tldv"]
                    date_from: "2026-04-01"
      responses:
        "200":
          description: 回答成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatResponse"
        "401":
          description: 認証失敗（APIキー未指定・不正）
        "403":
          description: 権限なし
        "422":
          description: リクエスト不正（message が空 / 500文字超 など）
        "429":
          description: レート制限超過（30回/分 または 会社の利用量上限）

  /api/v1/chat/stream:
    post:
      operationId: askKnowledgeStream
      summary: ナレッジに質問して回答を逐次受け取る（ストリーミング）
      description: |
        `/api/v1/chat` と同じリクエストで、回答を Server-Sent Events（SSE,
        `text/event-stream`）でリアルタイムに返します。逐次表示したいUI向け。
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatRequest"
      responses:
        "200":
          description: |
            SSEストリーム（text/event-stream）。
            最初の1文字目が数秒で届くため、画面表示にはこちらが向いています。
          content:
            text/event-stream:
              schema:
                type: string
        "401":
          description: 認証失敗
        "429":
          description: レート制限超過

  /api/v1/files:
    get:
      operationId: listKnowledgeFiles
      summary: 取り込み済みの資料一覧を取得する
      description: |
        ナレッジに取り込まれているファイルの一覧を返します。
        返すのはファイル名・種別・処理状態・サイズまでで、**本文は含みません**
        （中身が要る場合は `POST /api/v1/chat` で質問してください）。

        鍵に `knowledge.read` の範囲が要ります（**発行時の既定には入っていません**）。
        鍵は個人ではなく会社に紐づくため、**部署フォルダで限定した資料は一覧に出ません**
        （全社公開のものだけが返ります）。
      security:
        - ApiKeyAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          description: 1回に返す件数
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: 何件目から返すか（ページ送り用）
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: 資料一覧
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileListResponse"
        "401":
          description: 認証失敗（APIキーが未指定・不正）
        "403":
          description: 鍵に `knowledge.read` の範囲が無い
        "429":
          description: レート制限超過（60回/分）

  /api/crm/customers:
    get:
      operationId: listCrmCustomers
      summary: 顧客一覧を取得する
      description: |
        CRMの顧客一覧を返します（最終接触日・放置状態を含む）。

        鍵に `crm.read` の範囲が要ります（**発行時の既定には入っていません**）。
        読み取り専用です。顧客の作成・更新・削除は外部APIからはできません。
      security:
        - ApiKeyAuth: []
      parameters:
        - name: search
          in: query
          required: false
          description: 顧客名などの部分一致で絞り込み
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: 顧客一覧
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerListResponse"
        "401":
          description: 認証失敗（APIキーが未指定・不正）
        "403":
          description: 鍵に `crm.read` の範囲が無い
        "429":
          description: レート制限超過

  /api/crm/actions:
    get:
      operationId: listCrmActions
      summary: タスク（ネクストアクション）一覧を取得する
      description: |
        未返信の検出や会議の宿題などから作られたタスクの一覧を返します。

        鍵に `crm.read` の範囲が要ります（**発行時の既定には入っていません**）。
      security:
        - ApiKeyAuth: []
      parameters:
        - name: status
          in: query
          required: false
          description: pending / completed で絞り込み
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: タスク一覧
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionListResponse"
        "401":
          description: 認証失敗
        "403":
          description: 鍵に `crm.read` の範囲が無い
        "429":
          description: レート制限超過

  /api/crm/followup/customers:
    get:
      operationId: listFollowupCustomers
      summary: 要フォロー顧客（放置日数つき）を取得する
      description: |
        最後の接触から一定日数が経った「止まっている顧客」の一覧を返します。
        接触には、チャットを紐付けた顧客ならそのチャットの実際のやり取りが含まれます。

        鍵に `crm.read` の範囲が要ります（**発行時の既定には入っていません**）。
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: 要フォロー顧客一覧
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FollowupCustomersResponse"
        "401":
          description: 認証失敗
        "403":
          description: 鍵に `crm.read` の範囲が無い
        "429":
          description: レート制限超過

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "ベテランAIで発行したAPIキー（va_ で始まる）"

  schemas:
    ChatRequest:
      type: object
      required: [message]
      properties:
        message:
          type: string
          minLength: 1
          maxLength: 500
          description: 質問文（1〜500文字）
          example: "今月の受注見込みは？"
        filters:
          $ref: "#/components/schemas/ChatFilters"

    ChatFilters:
      type: object
      description: 検索範囲の絞り込み（任意）
      properties:
        sources:
          type: array
          nullable: true
          maxItems: 20
          items:
            type: string
          description: "対象ソース。例: line, line_official, lineworks, slack, discord, chatwork, teams, zoom, google_meet, tldv, circleback, file"
          example: ["circleback", "tldv"]
        file_types:
          type: array
          nullable: true
          maxItems: 20
          items:
            type: string
          description: "ファイル種別。例: pdf, audio, image, document"
          example: ["pdf"]
        date_from:
          type: string
          nullable: true
          description: "この日付以降（ISO形式 YYYY-MM-DD）"
          example: "2026-04-01"
        date_to:
          type: string
          nullable: true
          description: "この日付以前（ISO形式 YYYY-MM-DD）"
          example: "2026-06-30"

    ChatResponse:
      type: object
      properties:
        response:
          type: string
          description: AIの回答本文
        sources:
          type: array
          items:
            $ref: "#/components/schemas/ChatSource"
          description: 回答の根拠になった出典
        response_time_ms:
          type: integer
          nullable: true
          description: 処理時間（ミリ秒）

    ChatSource:
      type: object
      description: |
        出典1件。`response` 本文には `[1]` `[ファイル1]` のような出典番号が入るので、
        `ref_number` と `type` の組で本文の番号と突き合わせてください
        （`type: "message"` なら `[N]`、`type: "file"` なら `[ファイルN]`）。
      properties:
        ref_number:
          type: integer
          nullable: true
          description: 回答本文中の出典番号。`type` と組で本文の `[N]` / `[ファイルN]` に対応する
        type:
          type: string
          nullable: true
          description: "出典の種別。message（会話・議事録）または file（アップロードされたファイル）"
          enum: ["message", "file"]
        source:
          type: string
          nullable: true
          description: "ソース種別（circleback, tldv, slack 等）。type が file の出典では null"
        channel_name:
          type: string
          nullable: true
          description: チャンネル名・会議名・ファイル名など
        user_name:
          type: string
          nullable: true
          description: 投稿者
        section_owner:
          type: string
          nullable: true
          description: 議事録等で投稿者と異なる実発言者がいる場合のみ
        section_type:
          type: string
          nullable: true
          description: "分類（議事録 / 日報 / 報告書 など）"
        created_at:
          type: string
          format: date-time
          nullable: true
        content_preview:
          type: string
          nullable: true
          description: 該当箇所のプレビュー

    FileListResponse:
      type: object
      properties:
        files:
          type: array
          items:
            $ref: "#/components/schemas/FileItem"
          description: 資料の一覧
        total:
          type: integer
          description: 条件に一致した総件数。`offset` を進めて全件取るときの終わり判定に使う

    FileItem:
      type: object
      description: |
        取り込み済みの資料1件。**本文は含みません。**
      properties:
        id:
          type: string
          description: ファイルID
        file_type:
          type: string
          description: "種別（pdf / audio / image / document など）"
        original_filename:
          type: string
          nullable: true
          description: 元のファイル名
        file_size_bytes:
          type: integer
          nullable: true
          description: ファイルサイズ（バイト）
        processing_status:
          type: string
          nullable: true
          description: "取り込み状態。**completed 以外はまだ検索対象に入っていません**"
        processing_progress:
          type: integer
          nullable: true
          description: 取り込みの進捗（％）
        uploaded_at:
          type: string
          format: date-time
          nullable: true
          description: アップロード日時
        metadata:
          type: object
          description: 付帯情報（取り込み元など）
        visibility:
          type: string
          description: "公開範囲（all / custom / owner）"
        approval_status:
          type: string
          description: "承認状態。**pending はまだ検索対象に入っていません**"

    CustomerListItem:
      type: object
      description: 顧客1件
      properties:
        id:
          type: string
        name:
          type: string
        contact_name:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        nurture_status:
          type: string
          nullable: true
          description: regular / on_hold / house_list（放置の段階）
        assignee_user_id:
          type: string
          nullable: true
        last_contact_at:
          type: string
          format: date-time
          nullable: true
          description: 最終接触日時（チャット紐付け済みなら実際のやり取りを反映）

    CustomerListResponse:
      type: object
      properties:
        customers:
          type: array
          items:
            $ref: "#/components/schemas/CustomerListItem"
        total:
          type: integer

    ActionListItem:
      type: object
      description: タスク（ネクストアクション）1件
      properties:
        id:
          type: string
        title:
          type: string
        customer_id:
          type: string
          nullable: true
        customer_name:
          type: string
          nullable: true
        deal_id:
          type: string
          nullable: true
        deal_title:
          type: string
          nullable: true
        user_id:
          type: string
          nullable: true
        user_name:
          type: string
          nullable: true
        action_type:
          type: string
        description:
          type: string
          nullable: true
        due_date:
          type: string
          format: date
          nullable: true
        priority:
          type: string
          description: high / medium / low
        status:
          type: string
          description: pending / completed
        is_completed:
          type: boolean
        ai_generated:
          type: boolean
        created_at:
          type: string
          format: date-time

    ActionListResponse:
      type: object
      properties:
        actions:
          type: array
          items:
            $ref: "#/components/schemas/ActionListItem"
        total:
          type: integer

    FollowupCustomer:
      type: object
      description: 要フォロー顧客（放置日数つき）
      properties:
        customer_id:
          type: string
        customer_name:
          type: string
        last_contact_at:
          type: string
          format: date-time
          nullable: true
        days_since_contact:
          type: integer
          nullable: true
          description: 最終接触からの経過日数
        conversion_score:
          type: string
          nullable: true

    FollowupCustomersResponse:
      type: object
      properties:
        customers:
          type: array
          items:
            $ref: "#/components/schemas/FollowupCustomer"
        total:
          type: integer
        reminder_days:
          type: integer
          description: 何日でフォロー対象とみなすかの設定値
