【修正案あり】Dify GoogleCalendar Pluginで予定を作成しようとすると、「array schema missing items」が発生する

未分類

この記事で扱うこと

Dify(セルフホスト版)で Google Calendarプラグイン(Marketplace版) を使い、エージェントから create_event を呼ぶと、

  • OpenAI API から 400
  • Invalid schema for function 'create_event'
  • ('properties', 'attendees'), array schema missing items

というエラーで止まる問題の原因と、現実的な直し方(回避策〜根治まで)をまとめます。


発生した症状

Difyのエージェント(LLM: gpt-5.2-mini)に「予定を作って」と依頼し、Google Calendarツール create_event を使う構成にすると、実行時に以下のようなエラーで失敗します。

Bad Request Error, Error code: 400
Invalid schema for function 'create_event':
In context=('properties', 'attendees'), array schema missing items.
type: invalid_request_error
code: invalid_function_parameters
param: tools[2].function.parameters

ポイントは LLMが何かを間違えたのではなく、OpenAIが「ツール定義(function schema)が壊れている」と判断してリクエスト自体を拒否していることです。


環境(筆者の再現条件)

  • Dify: 1.11.2(セルフホスト)
  • Google Calendar plugin(langgenius/google_calendar): 0.1.0 Difyアプリ上のマーケットプレイスより入手
  • Difyでのアプリ種類:Agent
    • Tool:上記langgenius/google_calendar
    • LLM:LLM: gpt-5.2-mini

再現手順

  1. Difyでエージェント作成
  2. Google Calendarツール create_event を有効化
  3. LLMに「予定の追加」を依頼して実行
  4. 上記エラーが発生

原因:attendees の schema が不正(array なのに items がない)

AIくんに丸投げして質問したところ、以下が原因な様です。プラグインの更新もないため、手動でプラグインコードを修正してアップロードして対応をします。

エラー文が示す通り、attendees が配列(array)として扱われているにもかかわらず、OpenAIに渡る function schema 内で items(配列要素の型定義)が欠落しています。

たとえば、正しいJSON Schemaなら配列はこうなります(概念例):

“`json
“attendees”: {
“type”: “array”,
“items”: { “type”: “string” }
}
しかし実際には items がなく、OpenAI側のスキーマ検証で弾かれて 400 になります。

なぜ「items を足す」だけで直らないケースがあるのか
Difyのプラグインパラメータ定義(YAML)と、OpenAIに渡す function schema(JSON Schema)には変換が挟まります。

この変換で、

プラグイン側の type: array を正しく扱えない

または items を落としてしまう

といった挙動があると、YAML側で頑張って items を書いても最終的に OpenAIへ届かないことがあります。その場合、最も堅い回避策は 「配列をやめて、文字列として受ける」 です。

対応方針(結論)

:attendees を string に落として実装側で配列化する
方針
attendees を tool 定義では string として受ける

ツール実装(Python)でカンマ区切り、セミコロン区切り、改行区切り

JSON配列文字列([“a@x”,”b@x”])を許容して メール配列に変換し、Google Calendar API に渡す

これなら、OpenAIの function schema では attendees は単なる string なので、array/items 問題が完全に回避できます。

具体的なコード修正箇所

修正箇所1

langgenius-google_calendar_0.1.0.difypkg\tools\create_event.py 42行目付近
※difypkgファイルはZIPファイルとして展開できます

修正前
        # Extract parameters
        calendar_id = tool_parameters.get("calendar_id", "primary")
        end_time = tool_parameters.get("end_time")
        all_day = tool_parameters.get("all_day", False)
        description = tool_parameters.get("description")
        location = tool_parameters.get("location")
        attendees = tool_parameters.get("attendees", [])
        time_zone = tool_parameters.get("time_zone")
        visibility = tool_parameters.get("visibility", "default")
        send_notifications = tool_parameters.get("send_notifications", True)

        access_token = self.runtime.credentials.get("access_token")
修正後コード
        # Extract parameters
        calendar_id = tool_parameters.get("calendar_id", "primary")
        end_time = tool_parameters.get("end_time")
        all_day = tool_parameters.get("all_day", False)
        description = tool_parameters.get("description")
        location = tool_parameters.get("location")
        attendees_raw = tool_parameters.get("attendees") 
        attendees = [] 
        if isinstance(attendees_raw, list):
            attendees = attendees_raw
        elif isinstance(attendees_raw, str) and attendees_raw.strip():
            s = attendees_raw.strip()
            # Accept JSON array string or comma/semicolon/newline separated emails
            try:
                if s.startswith('['):
                    v = json.loads(s)
                    if isinstance(v, list):
                        attendees = v
            except Exception:
                pass
            if not attendees:
                parts = re.split(r'[;,\s]+', s)
                attendees = [p for p in parts if p and '@' in p]

        time_zone = tool_parameters.get("time_zone")
        visibility = tool_parameters.get("visibility", "default")
        send_notifications = tool_parameters.get("send_notifications", True)

        access_token = self.runtime.credentials.get("access_token")

修正箇所2

\langgenius-google_calendar_0.1.0.difypkg\tools\create_event.yaml

identity:
  name: create_event
  author: langgenius
  label:
    en_US: Create Event
    zh_Hans: 创建事件
    ja_JP: イベントを作成
  icon: icon.png
description:
  human:
    en_US: Create a new event in Google Calendar
    zh_Hans: 在Google日历中创建新事件
    ja_JP: Googleカレンダーに新しいイベントを作成
  llm: Creates a new calendar event with specified details including title, time,
    location, attendees, and description
parameters:
- name: title
  type: string
  required: true
  form: llm
  label:
    en_US: Event Title
    zh_Hans: 事件标题
    ja_JP: イベントタイトル
  human_description:
    en_US: The title/summary of the event
    zh_Hans: 事件的标题/摘要
    ja_JP: イベントのタイトル/概要
  llm_description: The title or summary of the calendar event (required)
- name: start_time
  type: string
  required: true
  form: llm
  label:
    en_US: Start Time
    zh_Hans: 开始时间
    ja_JP: 開始時間
  human_description:
    en_US: Event start time (ISO format, e.g., 2024-01-01T10:00:00Z or 2024-01-01
      10:00)
    zh_Hans: 事件开始时间(ISO格式,如:2024-01-01T10:00:00Z 或 2024-01-01 10:00)
    ja_JP: イベント開始時間(ISO形式、例:2024-01-01T10:00:00Z または 2024-01-01 10:00)
  llm_description: Event start time in ISO format (e.g., 2024-01-01T10:00:00Z) (required)
- name: end_time
  type: string
  required: false
  form: llm
  label:
    en_US: End Time
    zh_Hans: 结束时间
    ja_JP: 終了時間
  human_description:
    en_US: Event end time (ISO format). If not provided, defaults to 1 hour after
      start time
    zh_Hans: 事件结束时间(ISO格式)。如未提供,默认为开始时间后1小时
    ja_JP: イベント終了時間(ISO形式)。提供されない場合、開始時間の1時間後がデフォルト
  llm_description: Event end time in ISO format. Defaults to 1 hour after start time
    if not specified
- name: calendar_id
  type: string
  required: false
  default: primary
  form: llm
  label:
    en_US: Calendar ID
    zh_Hans: 日历ID
    ja_JP: カレンダーID
  human_description:
    en_US: The calendar ID to create the event in (use 'primary' for main calendar)
    zh_Hans: 要创建事件的日历ID(使用'primary'表示主日历)
    ja_JP: イベントを作成するカレンダーID(メインカレンダーは'primary'を使用)
  llm_description: Target calendar identifier or 'primary' for the user's primary
    calendar
- name: all_day
  type: boolean
  required: false
  default: false
  form: llm
  label:
    en_US: All Day Event
    zh_Hans: 全天事件
    ja_JP: 終日イベント
  human_description:
    en_US: Whether this is an all-day event
    zh_Hans: 是否为全天事件
    ja_JP: これが終日イベントかどうか
  llm_description: Create as an all-day event (true/false)
- name: description
  type: string
  required: false
  form: llm
  label:
    en_US: Description
    zh_Hans: 描述
    ja_JP: 説明
  human_description:
    en_US: Detailed description of the event
    zh_Hans: 事件的详细描述
    ja_JP: イベントの詳細説明
  llm_description: Detailed description or notes for the event
- name: location
  type: string
  required: false
  form: llm
  label:
    en_US: Location
    zh_Hans: 地点
    ja_JP: 場所
  human_description:
    en_US: Event location (address, meeting room, etc.)
    zh_Hans: 事件地点(地址、会议室等)
    ja_JP: イベントの場所(住所、会議室など)
  llm_description: Event location such as address, meeting room, or venue
- name: attendees
  type: string
  required: false
  form: llm
  label:
    en_US: Attendees
    zh_Hans: 参加者
    ja_JP: 出席者
  human_description:
    en_US: Attendee emails. Comma/semicolon/newline separated, or JSON array string.
    zh_Hans: 参加者邮箱。可用逗号/分号/换行分隔,或使用JSON数组字符串。
    ja_JP: 出席者メール。カンマ/セミコロン/改行区切り、またはJSON配列文字列。
  llm_description: Attendee emails as a single string. Accepts comma/semicolon/newline
    separated emails, or JSON array string like ["a@example.com","b@example.com"].
- name: time_zone
  type: string
  required: false
  form: llm
  label:
    en_US: Time Zone
    zh_Hans: 时区
    ja_JP: タイムゾーン
  human_description:
    en_US: Time zone for the event (e.g., America/New_York, Asia/Tokyo)
    zh_Hans: 事件的时区(如:America/New_York、Asia/Tokyo)
    ja_JP: イベントのタイムゾーン(例:America/New_York、Asia/Tokyo)
  llm_description: IANA time zone identifier (e.g., America/New_York, UTC)
- name: visibility
  type: select
  required: false
  default: default
  form: llm
  label:
    en_US: Visibility
    zh_Hans: 可见性
    ja_JP: 可視性
  human_description:
    en_US: Event visibility setting
    zh_Hans: 事件可见性设置
    ja_JP: イベントの可視性設定
  llm_description: Event visibility level (default, public, private)
  options:
  - value: default
    label:
      en_US: Default
      zh_Hans: 默认
      ja_JP: デフォルト
  - value: public
    label:
      en_US: Public
      zh_Hans: 公开
      ja_JP: 公開
  - value: private
    label:
      en_US: Private
      zh_Hans: 私有
      ja_JP: プライベート
- name: send_notifications
  type: boolean
  required: false
  default: true
  form: llm
  label:
    en_US: Send Notifications
    zh_Hans: 发送通知
    ja_JP: 通知を送信
  human_description:
    en_US: Whether to send email notifications to attendees
    zh_Hans: 是否向参加者发送邮件通知
    ja_JP: 出席者にメール通知を送信するかどうか
  llm_description: Send email notifications to attendees (true/false)
extra:
  python:
    source: tools/create_event.py
output_schema:
  type: object
  properties:
    success:
      type: boolean
      description: Whether the event was created successfully
    message:
      type: string
      description: Human-readable result message
    event:
      type: object
      description: Created event details
      properties:
        id:
          type: string
          description: Event ID
        title:
          type: string
          description: Event title
        description:
          type: string
          description: Event description
        location:
          type: string
          description: Event location
        start_time:
          type: string
          description: Event start time
        end_time:
          type: string
          description: Event end time
        all_day:
          type: boolean
          description: Whether this is an all-day event
        status:
          type: string
          description: Event status
        created:
          type: string
          description: Event creation timestamp
        updated:
          type: string
          description: Event update timestamp
        creator:
          type: object
          description: Event creator information
        organizer:
          type: object
          description: Event organizer information
        attendees:
          type: array
          description: List of attendees
        html_link:
          type: string
          description: Link to event in Google Calendar
        ical_uid:
          type: string
          description: iCal UID of the event
        visibility:
          type: string
          description: Event visibility
    event_id:
      type: string
      description: Unique identifier of the created event
    html_link:
      type: string
      description: Direct link to the event in Google Calendar
    ical_uid:
      type: string
      description: iCal UID for the event

検証方法

Difyのアプリから修正したdifypkgファイルをアップロードしてテスト実行

途中でハマった点1:署名検証でパッチプラグインが入らないプラグイン検証が有効だと、改変した .difypkg は署名が一致せずインストールできません。

A: 署名検証を無効化(手早いがセキュリティは下がる)
B: 第三者署名(自分の鍵で署名して許可する)(おすすめ)
※運用ポリシーに合わせて選択してください。

私の場合はローカル環境で実行していたのでAを採用し、Dify Dockerの.envに以下を追加して回避しました。

FORCE_VERIFYING_SIGNATURE=false

まとめ

OpenAI 400 array schema missing items は function schemaが無効でAPIが受付前に拒否してい
根本原因は create_event の attendees が array扱いなのに items が欠落
確実な回避策は attendees を string にして、ツール実装側で配列へ変換