Tool にフロントエンドインタラクションを追加する

サーバー側で実行するだけで、カスタム UI を必要としない Tool もあります。一方、ユーザーによる確認、選択、パラメータ編集が必要な Tool には、同名の Tool にカード、モーダル、またはブラウザ側の実行ロジックを登録できます。

2つの概念を区別する

フロントエンドカードはToolCallの表示とユーザーインタラクションのみを担当します。Toolのビジネスロジックが必ずブラウザで実行されることを意味するわけではありません。

suggestionsのようにオプションを表示し、ユーザーの選択後にサーバー側のinvoke()を続行させるだけの場合は、デフォルトのexecution: 'backend'のままで問題ありません。Toolの実際のロジックが現在のブラウザページ、FlowModel、またはエディタの状態にアクセスする必要がある場合にのみ、execution: 'frontend'を設定してフロントエンドのinvokeを実装してください。

まずサーバー側でパラメータと実行ロジックを定義する

内蔵のsuggestions Toolは以下にあります:

packages/plugins/@nocobase/plugin-ai/src/ai/tools/suggestions.ts

そのスキーマには、候補オプションとユーザーの最終的な選択の両方が含まれています:

schema: z.object({
  option: z.string().describe('user selected option, ignore this param').optional(),
  options: z.array(z.string()).describe('A list of suggested prompts for the user to choose from.'),
})

Toolの説明に従い、モデルが最初に呼び出す際はoptionsのみを生成します。このToolにはdefaultPermission: 'ALLOW'が設定されていないため、デフォルトの権限はASKとなり、ToolCallはユーザーの操作を待機するために一時停止します。

ユーザーが選択すると、フロントエンドがdecisions.edit()を通じてoptionを元のパラメータにマージし、ToolCallを再開します。サーバー側のinvoke()は最終的に選択された内容を返します:

return {
  status: 'success',
  content: args?.option,
};

内蔵の実装では、選択結果をaiMessages.toolCallsに書き戻すため、履歴メッセージが再レンダリングされた際にもユーザーがどの項目を選択したかが表示されます。

Tool カードを作成する

フロントエンドカードはToolsUIPropertiesを受け取ります:

import { useState } from 'react';
import type { ToolsUIProperties } from '@nocobase/client-v2';
import { Button, Flex } from 'antd';

interface DeveloperChoiceArgs {
  options?: string[] | string;
  option?: string;
}

const parseOptions = (value: DeveloperChoiceArgs['options']): string[] => {
  if (Array.isArray(value)) {
    return value.filter((option): option is string => typeof option === 'string');
  }
  if (typeof value !== 'string') {
    return [];
  }

  try {
    const parsed = JSON.parse(value) as unknown;
    return Array.isArray(parsed) ? parsed.filter((option): option is string => typeof option === 'string') : [];
  } catch {
    return [];
  }
};

export const DeveloperChoiceCard = ({
  toolCall,
  decisions,
}: ToolsUIProperties<DeveloperChoiceArgs>) => {
  const [submitting, setSubmitting] = useState(false);
  const options = parseOptions(toolCall.args?.options);

  const handleSelect = async (option: string) => {
    if (submitting) {
      return;
    }

    setSubmitting(true);
    try {
      await decisions.edit({
        ...toolCall.args,
        option,
      });
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <Flex gap="small" wrap="wrap">
      {options.map((option, index) => (
        <Button
          key={`${option}-${index}`}
          disabled={toolCall.invokeStatus !== 'interrupted' || submitting}
          onClick={() => handleSelect(option)}
        >
          {option}
        </Button>
      ))}
    </Flex>
  );
};
注意

このコンポーネントはdecisions.edit()の一般的な用法を示しており、重複クリックやJSON文字列パラメータを処理しています。実際に使用する場合は、チャットインターフェースに応じて、読み取り専用の会話、現在のアクティブなメッセージ、および履歴の選択状態を処理する必要があります。完全な実装については、packages/plugins/@nocobase/plugin-ai/src/client-v2/ai-employees/tools/SuggestionsOptionsCard.tsxを参照してください。

decisionsは3つの操作を提供します:

メソッド役割
approve()元のパラメータを使用して実行を続行する
edit(args)パラメータを変更して実行を続行する
reject(message?)実行を拒否し、理由を会話フローに返す

内蔵のSuggestionsOptionsCard.tsxでは、さらに以下の詳細を処理しています:

  • 配列とJSON文字列の両方のoptions形式に対応
  • ToolCallの生成中にローディングを表示
  • interrupted状態のToolCallのみ選択を許可
  • クリック後すぐにボタンを無効化し、重複送信を防止
  • 履歴メッセージ内で選択済みのオプションを保持し、強調表示
  • 現在編集可能な会話でのみ操作をトリガーすることを許可

クライアントプラグインで登録する

フロントエンドの登録名は、サーバー側のTool名と完全に一致している必要があります:

import { Plugin } from '@nocobase/client-v2';
import { DeveloperChoiceCard } from './ai-employees/tools/DeveloperChoiceCard';

export class PluginDeveloperHelperClient extends Plugin {
  async load() {
    this.ai.toolsManager.registerTools('developerChoice', {
      ui: {
        card: DeveloperChoiceCard,
      },
    });
  }
}

export default PluginDeveloperHelperClient;

サーバー側のファイルがsrc/ai/tools/developerChoice.tsである場合、ここではdeveloperChoiceを登録します。

内蔵のsuggestionsの登録プロセスも同様に行われます:

export const suggestionsTool = [
  'suggestions',
  {
    ui: {
      card: SuggestionsOptionsCard,
    },
  },
];

その後、PluginAIClientV2.load()registerPluginAIClientV2BuiltinTools(this.ai.toolsManager)を呼び出し、カードをサーバーから返された同名のTool定義にマージします。

カード、モーダル、フロントエンド実行を選択する

以下にクライアントToolsOptionsの常用設定を列挙します。完全な型定義についてはpackages/core/client-v2/src/ai/tools-manager/types.tsを参照してください。

type ToolsOptions = {
  ui?: {
    card?: ComponentType<ToolsUIProperties>;
    modal?: {
      title?: string;
      okText?: string;
      Component?: ComponentType;
      footer?: ComponentType;
      hideOkButton?: boolean;
      // modal.props、useOnOk 等の設定については完全な型定義を確認してください。
    };
  };
  invoke?: (app, params) => unknown | Promise<unknown>;
  // useHooks 等のその他の設定については完全な型定義を確認してください。
};

カードを使用する

デフォルトでは、まず card を使用します。カードは ToolCall の位置に実行状態、確認ボタン、少数の選択肢を表示する場合に適しています。

モーダルを使用する

コンテンツが多い場合、大きなプレビューや複雑なパラメータ編集が必要な場合に modal を追加します。

ブラウザで Tool を実行する

サーバー側の Tool で execution: 'frontend' が設定されている場合、クライアント側で invoke を提供する必要があります。この種類の Tool は、現在のページコンテキスト、エディタの内容、FlowEngine の状態を読み取る用途に適しています。サーバー側の権限保護が必要なデータ書き込みには適していません。

完全な例:組み込み AI 従業員に選択カードを追加する

完全な例:組み込み AI 従業員の作成を完了した後、Dev Helper の追加質問をクリック可能な選択肢にするには、developerChoice Tool を定義してフロントエンドカードを登録します。サーバー側のファイルは以下に配置します:

src/ai/ai-employees/dev-helper/skills/welcome-developer/tools/developerChoice.ts

このToolは、オプションを宣言し、ユーザーの選択を受け取る役割を担います:

import type { Context } from '@nocobase/actions';
import { defineTools } from '@nocobase/ai';
import { z } from 'zod';

export default defineTools({
  scope: 'SPECIFIED',
  introduction: {
    title: '{{t("ai.tools.developerChoice.title", { ns: "@nocobase/plugin-developer-helper" })}}',
    about: '{{t("ai.tools.developerChoice.about", { ns: "@nocobase/plugin-developer-helper" })}}',
  },
  definition: {
    name: 'developerChoice',
    description: 'Show a short list of plugin-development directions for the user to choose from.',
    schema: z.object({
      options: z.array(z.string()).min(2).max(4),
      option: z.string().optional(),
    }),
  },
  invoke: async (_ctx: Context, args: { options: string[]; option?: string }) => {
    return {
      status: 'success',
      content: args.option,
    };
  },
});

developerChoice.tswelcome-developer Skillのtools/ディレクトリにあるため、自動的に現在のSkillにバインドされます。ただし、バインドされていることはモデルがこのToolを使用できることを意味しており、必ず呼び出されることを保証するものではありません。

また、SKILLS.mdのワークフローを同期的に修正し、元のステップ5〜6を以下に置き換える必要があります:

5. Use `content.name` to write a short welcome message in the same language as the user.
6. Call `developerChoice` exactly once with 2–4 plugin-development directions written in the user's language.
7. Wait for the user to select an option.
8. Continue according to the selected option.

フロントエンドカードは、先に定義したDeveloperChoiceCardを再利用し、以下に保存します:

src/client-v2/ai-employees/tools/DeveloperChoiceCard.tsx

最後にsrc/client-v2/plugin.tsxで登録します:

import { Plugin } from '@nocobase/client-v2';
import { DeveloperChoiceCard } from './ai-employees/tools/DeveloperChoiceCard';

export class PluginDeveloperHelperClient extends Plugin {
  async load() {
    this.ai.toolsManager.registerTools('developerChoice', {
      ui: {
        card: DeveloperChoiceCard,
      },
    });
  }
}

export default PluginDeveloperHelperClient;

カードの登録が完了したら、クライアントを再ビルドしてください。会話の中でdeveloperChoiceが実行されると、ToolCallが一時停止し、クリック可能なオプションが表示されます。

関連リンク