mcpField
B
better-chatbot-patterns

better-chatbot-patterns

github.com/jackspace開発1693 回インストール13 週間前に更新
リポジトリ

better-chatbot-patterns について

better-chatbot-patternsは、カスタムAIチャットボット向けの再利用可能な実装パターンを提供するスキルです。開発者を対象に、検証、ツールの型付け、プロバイダー設定、ワークフロー、状態管理を扱います。

主な機能

  1. 01サーバーアクションの認証・検証パターン
  2. 02ツール処理向けのブランド型ランタイム型
  3. 03複数プロバイダーの統一設定
  4. 04Zustandによるワークフロー状態パターン
  5. 05Zodによるフィールド間検証

活用シーン

  1. 01認証が必要なサーバーアクションへの検証追加
  2. 02複数のツール型の安全な処理
  3. 03選択可能なAIプロバイダーのサポート
  4. 04ネストされたワークフロー状態の管理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jackspace/claudeskillz --skill better-chatbot-patterns

インストール後、Claude Code で/better-chatbot-patternsを入力して読み込まれたか確認します。

同梱ファイルREADME.mdSKILL.json

詳細

better-chatbot-patterns

ステータス:本番環境で使用可能 最終更新日:2025-10-29 依存関係:なし 最新バージョン:[email protected], [email protected], [email protected], [email protected]


概要

このスキルは、カスタム AI チャットボットの実装に活用できる、better-chatbot プロジェクトの再利用可能なパターンを抽出したものです。プロジェクトの規約を説明する better-chatbot スキルとは異なり、このスキルでは、どのプロジェクトにも適用できるポータブルなテンプレートを提供します。

含まれるパターン:

  1. サーバーアクションのバリデーター(認証、バリデーション、FormData)
  2. ツール抽象化システム(複数種類のツールの処理)
  3. 複数 AI プロバイダーのセットアップ
  4. ワークフロー実行パターン
  5. 状態管理の規約

パターン 1:サーバーアクションのバリデーター

問題

サーバーアクションの認証とバリデーションを手動で行うと、次の問題が発生します。

  • 認証チェックが一貫しない
  • FormData の解析に関する定型コードが繰り返される
  • 標準化されていないエラー処理
  • 型安全性の問題
解決策:バリデーション済みアクションユーティリティ

lib/action-utils.ts を作成します。

import { z } from "zod"

// Type for action result
type ActionResult<T> =
  | { success: true; data: T }
  | { success: false; error: string }

// Pattern 1: Simple validation (no auth)
export function validatedAction<TSchema extends z.ZodType>(
  schema: TSchema,
  handler: (
    data: z.infer<TSchema>,
    formData: FormData
  ) => Promise<ActionResult<any>>
) {
  return async (formData: FormData): Promise<ActionResult<any>> => {
    try {
      const rawData = Object.fromEntries(formData.entries())
      const parsed = schema.safeParse(rawData)

      if (!parsed.success) {
        return { success: false, error: parsed.error.errors[0].message }
      }

      return await handler(parsed.data, formData)
    } catch (error) {
      return { success: false, error: String(error) }
    }
  }
}

// Pattern 2: With user context (adapt getUser() to your auth system)
export function validatedActionWithUser<TSchema extends z.ZodType>(
  schema: TSchema,
  handler: (
    data: z.infer<TSchema>,
    formData: FormData,
    user: { id: string; email: string } // Adapt to your User type
  ) => Promise<ActionResult<any>>
) {
  return async (formData: FormData): Promise<ActionResult<any>> => {
    try {
      // Adapt this to your auth system (Better Auth, Clerk, Auth.js, etc.)
      const user = await getUser()
      if (!user) {
        return { success: false, error: "Unauthorized" }
      }

      const rawData = Object.fromEntries(formData.entries())
      const parsed = schema.safeParse(rawData)

      if (!parsed.success) {
        return { success: false, error: parsed.error.errors[0].message }
      }

      return await handler(parsed.data, formData, user)
    } catch (error) {
      return { success: false, error: String(error) }
    }
  }
}

// Pattern 3: With permission check (adapt to your roles system)
export function validatedActionWithPermission<TSchema extends z.ZodType>(
  schema: TSchema,
  permission: "admin" | "user-manage" | string, // Your permission types
  handler: (
    data: z.infer<TSchema>,
    formData: FormData,
    user: { id: string; email: string; role: string }
  ) => Promise<ActionResult<any>>
) {
  return async (formData: FormData): Promise<ActionResult<any>> => {
    try {
      const user = await getUser()
      if (!user) {
        return { success: false, error: "Unauthorized" }
      }

      // Adapt this to your permission system
      const hasPermission = await checkPermission(user, permission)
      if (!hasPermission) {
        return { success: false, error: "Forbidden" }
      }

      const rawData = Object.fromEntries(formData.entries())
      const parsed = schema.safeParse(rawData)

      if (!parsed.success) {
        return { success: false, error: parsed.error.errors[0].message }
      }

      return await handler(parsed.data, formData, user)
    } catch (error) {
      return { success: false, error: String(error) }
    }
  }
}

// Placeholder functions - replace with your auth system
async function getUser() {
  // Better Auth: await auth()
  // Clerk: const { userId } = auth(); if (!userId) return null; return await currentUser()
  // Auth.js: const session = await getServerSession(); return session?.user
  throw new Error("Implement getUser() with your auth provider")
}

async function checkPermission(user: any, permission: string) {
  // Implement based on your role system
  throw new Error("Implement checkPermission() with your role system")
}
使用例
// app/actions/profile.ts
"use server"

import { validatedActionWithUser } from "@/lib/action-utils"
import { z } from "zod"
import { db } from "@/lib/db"

const updateProfileSchema = z.object({
  name: z.string().min(1),
  email: z.string().email()
})

export const updateProfile = validatedActionWithUser(
  updateProfileSchema,
  async (data, formData, user) => {
    // user is guaranteed authenticated
    // data is validated and typed
    await db.update(users).set(data).where(eq(users.id, user.id))
    return { success: true, data: { updated: true } }
  }
)

使用する場面:

  • 認証が必要なサーバーアクション全般
  • バリデーションが必要なフォーム送信
  • 一貫性のないエラー処理の防止

パターン 2:ツール抽象化システム

問題

複数種類のツール(MCP、ワークフロー、デフォルト)を、それぞれ異なる実行パターンで処理すると、次の問題が発生します。

  • 実行時の型の不一致
  • 型チェックの定型コードが繰り返される
  • 新しいツールの種類を追加しにくい
解決策:ブランド型タグ

lib/tool-tags.ts を作成します。

// Branded type system for runtime type narrowing
export class ToolTag<T extends string> {
  private readonly _tag: T
  private readonly _branded: unique symbol

  private constructor(tag: T) {
    this._tag = tag
  }

  static create<TTag extends string>(tag: TTag) {
    return new ToolTag(tag) as ToolTag<TTag>
  }

  is(tag: string): boolean {
    return this._tag === tag
  }

  get tag(): T {
    return this._tag
  }
}

// Define your tool types
export type MCPTool = { type: "mcp"; name: string; execute: (...args: any[]) => Promise<any> }
export type WorkflowTool = { type: "workflow"; id: string; nodes: any[] }
export type DefaultTool = { type: "default"; name: string }

// Branded tag system
export const VercelAIMcpToolTag = {
  create: (tool: any) => ({ ...tool, _tag: ToolTag.create("mcp") }),
  isMaybe: (tool: any): tool is MCPTool & { _tag: ToolTag<"mcp"> } =>
    tool?._tag?.is("mcp")
}

export const VercelAIWorkflowToolTag = {
  create: (tool: any) => ({ ...tool, _tag: ToolTag.create("workflow") }),
  isMaybe: (tool: any): tool is WorkflowTool & { _tag: ToolTag<"workflow"> } =>
    tool?._tag?.is("workflow")
}

export const VercelAIDefaultToolTag = {
  create: (tool: any) => ({ ...tool, _tag: ToolTag.create("default") }),
  isMaybe: (tool: any): tool is DefaultTool & { _tag: ToolTag<"default"> } =>
    tool?._tag?.is("default")
}
使用例
// lib/ai/tool-executor.ts
import {
  VercelAIMcpToolTag,
  VercelAIWorkflowToolTag,
  VercelAIDefaultToolTag
} from "@/lib/tool-tags"

async function executeTool(tool: unknown) {
  // Runtime type narrowing with branded tags
  if (VercelAIMcpToolTag.isMaybe(tool)) {
    console.log("Executing MCP tool:", tool.name)
    return await tool.execute()
  } else if (VercelAIWorkflowToolTag.isMaybe(tool)) {
    console.log("Executing workflow:", tool.id)
    return await executeWorkflow(tool.nodes)
  } else if (VercelAIDefaultToolTag.isMaybe(tool)) {
    console.log("Executing default tool:", tool.name)
    return await executeDefault(tool)
  }

  throw new Error("Unknown tool type")
}

// When creating tools, tag them
const mcpTool = VercelAIMcpToolTag.create({
  type: "mcp",
  name: "search",
  execute: async () => { /* ... */ }
})

const workflowTool = VercelAIWorkflowToolTag.create({
  type: "workflow",
  id: "workflow-123",
  nodes: []
})

使用する場面:

  • 複数種類のツールを扱うシステム
  • 実行時の型チェックが必要な場合
  • 拡張可能なツールの種類を追加する場合

パターン 3:複数 AI プロバイダーのセットアップ

問題

複数の AI プロバイダー(OpenAI、Anthropic、Google、xAI など)をサポートするには、次の対応が必要です。

  • SDK ごとに異なる初期化パターン
  • プロバイダー固有の設定
  • プロバイダーを切り替えるための統一インターフェース
解決策:プロバイダーレジストリ

lib/ai/providers.ts を作成します。

import { createOpenAI } from "@ai-sdk/openai"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createGoogleGenerativeAI } from "@ai-sdk/google"

export type AIProvider = "openai" | "anthropic" | "google" | "xai" | "groq"

export const providers = {
  openai: createOpenAI({
    apiKey: process.env.OPENAI_API_KEY,
    compatibility: "strict"
  }),

  anthropic: createAnthropic({
    apiKey: process.env.ANTHROPIC_API_KEY
  }),

  google: createGoogleGenerativeAI({
    apiKey: process.env.GOOGLE_API_KEY
  }),

  xai: createOpenAI({
    apiKey: process.env.XAI_API_KEY,
    baseURL: "https://api.x.ai/v1"
  }),

  groq: createOpenAI({
    apiKey: process.env.GROQ_API_KEY,
    baseURL: "https://api.groq.com/openai/v1"
  })
}

// Model registry
export const models = {
  openai: {
    "gpt-5": providers.openai("gpt-5"),
    "gpt-5-mini": providers.openai("gpt-5-mini")
  },
  anthropic: {
    "claude-sonnet-4-5": providers.anthropic("claude-sonnet-4-5"),
    "claude-haiku-4-5": providers.anthropic("claude-haiku-4-5")
  },
  google: {
    "gemini-2.5-pro": providers.google("gemini-2.5-pro"),
    "gemini-2.5-flash": providers.google("gemini-2.5-flash")
  }
}

// Helper to get model
export function getModel(provider: AIProvider, modelName: string) {
  const providerModels = models[provider]
  if (!providerModels || !providerModels[modelName]) {
    throw new Error(`Model ${modelName} not found for provider ${provider}`)
  }
  return providerModels[modelName]
}
使用例
import { streamText } from "ai"
import { getModel } from "@/lib/ai/providers"

// In your API route
export async function POST(req: Request) {
  const { messages, provider, model } = await req.json()

  const selectedModel = getModel(provider, model)

  const result = await streamText({
    model: selectedModel,
    messages
  })

  return result.toDataStreamResponse()
}

使用する場面:

  • 複数プロバイダーのサポートが必要な場合
  • ユーザーが AI モデルを選択する場合
  • プロバイダー間のフォールバック

パターン 4:状態管理(Zustand)

問題

ミューテーションを使わずに、複雑なネストされた状態(ワークフロー、UI 設定など)を管理すること

解決策:浅い更新パターン

app/store/workflow.ts を作成します。

import { create } from "zustand"

type WorkflowNode = {
  id: string
  status: "pending" | "running" | "complete" | "error"
  data: any
}

type WorkflowStore = {
  workflow: {
    id: string
    nodes: WorkflowNode[]
  } | null
  updateNodeStatus: (nodeId: string, status: WorkflowNode["status"]) => void
  updateNodeData: (nodeId: string, data: any) => void
}

export const useWorkflowStore = create<WorkflowStore>((set) => ({
  workflow: null,

  // Shallow update pattern - no deep mutation
  updateNodeStatus: (nodeId, status) =>
    set(state => ({
      workflow: state.workflow ? {
        ...state.workflow,
        nodes: state.workflow.nodes.map(node =>
          node.id === nodeId ? { ...node, status } : node
        )
      } : null
    })),

  updateNodeData: (nodeId, data) =>
    set(state => ({
      workflow: state.workflow ? {
        ...state.workflow,
        nodes: state.workflow.nodes.map(node =>
          node.id === nodeId ? { ...node, data: { ...node.data, ...data } } : node
        )
      } : null
    }))
}))

使用する場面:

  • 複雑なネストされた状態
  • ミューテーションを使わない頻繁な更新
  • 再レンダリングの問題の回避

パターン 5:フィールド間バリデーション(Zod)

問題

関連するフィールド(パスワードの確認、日付範囲など)をバリデーションすること

解決策:Zod superRefine
import { z } from "zod"

// Password match validation
const passwordSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      path: ["confirmPassword"],
      code: z.ZodIssueCode.custom,
      message: "Passwords must match"
    })
  }
})

// Date range validation
const dateRangeSchema = z.object({
  startDate: z.string().datetime(),
  endDate: z.string().datetime()
}).superRefine((data, ctx) => {
  if (new Date(data.endDate) < new Date(data.startDate)) {
    ctx.addIssue({
      path: ["endDate"],
      code: z.ZodIssueCode.custom,
      message: "End date must be after start date"
    })
  }
})

// Conditional required fields
const conditionalSchema = z.object({
  type: z.enum(["email", "sms"]),
  email: z.string().email().optional(),
  phone: z.string().optional()
}).superRefine((data, ctx) => {
  if (data.type === "email" && !data.email) {
    ctx.addIssue({
      path: ["email"],
      code: z.ZodIssueCode.custom,
      message: "Email is required when type is 'email'"
    })
  }
  if (data.type === "sms" && !data.phone) {
    ctx.addIssue({
      path: ["phone"],
      code: z.ZodIssueCode.custom,
      message: "Phone is required when type is 'sms'"
    })
  }
})

使用する場面:

  • パスワードの確認
  • 日付範囲のバリデーション
  • 条件付き必須フィールド
  • フィールド間のビジネスルール

重要なルール

必ず行うこと

✅ パターンを使用する認証システムに合わせて調整する(Better Auth、Clerk、Auth.js など) ✅ 実行時の型チェックにはブランド型タグを使用する ✅ ネストされた Zustand の状態には浅い更新を使用する ✅ フィールド間バリデーションには Zod の superRefine を使用する ✅ ツールの抽象化に適切な型を付ける

絶対に行わないこと

❌ 認証/ロールシステムに合わせずにコードをコピーする ❌ 実行時のチェックなしにツールの種類を決めつける ❌ Zustand の状態を直接ミューテーションする ❌ 関連するフィールドに個別のバリデーターを使用する ❌ 拡張可能なシステムで型のブランド付けを省略する


よくある問題の予防

このスキルは、よくある5つの問題を防ぎます。

問題 #1:一貫性のない認証チェック

予防策:validatedActionWithUser パターンを使用します(認証システムに合わせて調整してください)。

問題 #2:ツールの型の不一致

予防策:ブランド型タグと .isMaybe() チェックを使用します。

問題 #3:状態のミューテーションによるバグ

予防策:Zustand の浅い更新パターンを使用します。

問題 #4:フィールド間バリデーションの失敗

予防策:関連するフィールドには Zod の superRefine を使用します。

問題 #5:プロバイダー設定エラー

予防策:統一インターフェースを備えたプロバイダーレジストリを使用します。


同梱リソースの使用

テンプレート(templates/)
  • templates/action-utils.ts - サーバーアクションバリデーター一式
  • templates/tool-tags.ts - ツール抽象化システム一式
  • templates/providers.ts - マルチ AI プロバイダーのセットアップ
  • templates/workflow-store.ts - Zustand ワークフローストア

プロジェクトにコピーして、プレースホルダー(getUser()、checkPermission() など)を適宜変更してください。


依存関係

必須:

任意(使用するパターンによって異なります):

  • @ai-sdk/openai - OpenAI プロバイダー
  • @ai-sdk/anthropic - Anthropic プロバイダー
  • @ai-sdk/google - Google プロバイダー

公式ドキュメント


本番環境での実例

これらのパターンは better-chatbot から抽出されています。

  • 稼働中: https://betterchatbot.vercel.app
  • テスト: E2E テスト 48 件以上に合格
  • エラー: 0 件(パターンは本番環境で実証済み)
  • 検証: ✅ マルチユーザー、マルチプロバイダー、ワークフロー実行

トークン効率: 約 65% 削減 | 防止したエラー: 5 件 | 本番環境で検証済み: はい

コメント