Agent SkillsAgent Skills
majiayu000

better-chatbot-patterns

@majiayu000/better-chatbot-patterns
majiayu000
180
35 forks
Updated 4/7/2026
View on GitHub

This skill provides reusable implementation patterns extracted from the better-chatbot project for custom AI chatbot deployments. Use this skill when building AI chatbots with server action validators, tool abstraction systems, workflow execution, or multi-AI provider integration in your own projects (not contributing to better-chatbot itself). Use when: building AI chatbot features, implementing server action validators, creating tool abstraction layers, setting up multi-AI provider support, building workflow execution systems, adapting better-chatbot patterns to custom projects Keywords: AI chatbot patterns, server action validators, tool abstraction, multi-AI providers, workflow execution, MCP integration, validated actions, tool type checking, Vercel AI SDK patterns, chatbot architecture

Installation

$npx agent-skills-cli install @majiayu000/better-chatbot-patterns
Claude Code
Cursor
Copilot
Codex
Antigravity

Details

Pathskills/development/better-chatbot-patterns/SKILL.md
Branchmain
Scoped Name@majiayu000/better-chatbot-patterns

Usage

After installing, this skill will be available to your AI coding assistant.

Verify installation:

npx agent-skills-cli list

Skill Instructions


name: better-chatbot-patterns description: | This skill provides reusable implementation patterns extracted from the better-chatbot project for custom AI chatbot deployments. Use this skill when building AI chatbots with server action validators, tool abstraction systems, workflow execution, or multi-AI provider integration in your own projects (not contributing to better-chatbot itself).

Use when: building AI chatbot features, implementing server action validators, creating tool abstraction layers, setting up multi-AI provider support, building workflow execution systems, adapting better-chatbot patterns to custom projects

Keywords: AI chatbot patterns, server action validators, tool abstraction, multi-AI providers, workflow execution, MCP integration, validated actions, tool type checking, Vercel AI SDK patterns, chatbot architecture license: MIT metadata: version: 1.0.0 author: Jeremy Dawes (Jez) | Jezweb based_on: https://github.com/cgoinglove/better-chatbot last_verified: 2025-10-29 tech_stack: Next.js 15, Vercel AI SDK 5, Zod, Zustand token_savings: ~65% errors_prevented: 5

better-chatbot-patterns

Status: Production Ready Last Updated: 2025-10-29 Dependencies: None Latest Versions: next@15.3.2, ai@5.0.82, zod@3.24.2, zustand@5.0.3


Overview

This skill extracts reusable patterns from the better-chatbot project for use in custom AI chatbot implementations. Unlike the better-chatbot skill (which teaches project conventions), this skill provides portable templates you can adapt to any project.

Patterns included:

  1. Server action validators (auth, validation, FormData)
  2. Tool abstraction system (multi-type tool handling)
  3. Multi-AI provider setup
  4. Workflow execution patterns
  5. State management conventions

Pattern 1: Server Action Validators

The Problem

Manual server action auth and validation leads to:

  • Inconsistent auth checks
  • Repeated FormData parsing boilerplate
  • Non-standard error handling
  • Type safety issues

The Solution: Validated Action Utilities

Create 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")
}

Usage Example

// 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 } }
  }
)

When to use:

  • Any server action requiring auth
  • Form submissions needing validation
  • Preventing inconsistent error handling

Pattern 2: Tool Abstraction System

The Problem

Handling multiple tool types (MCP, Workflow, Default) with different execution patterns leads to:

  • Type mismatches at runtime
  • Repeated type checking boilerplate
  • Difficulty adding new tool types

The Solution: Branded Type Tags

Create 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")
}

Usage Example

// 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: []
})

When to use:

  • Multi-type tool systems
  • Runtime type checking needed
  • Adding extensible tool types

Pattern 3: Multi-AI Provider Setup

The Problem

Supporting multiple AI providers (OpenAI, Anthropic, Google, xAI, etc.) requires:

  • Different SDK initialization patterns
  • Provider-specific configurations
  • Unified interface for switching providers

The Solution: Provider Registry

Create 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]
}

Usage Example

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()
}

When to use:

  • Multi-provider support needed
  • User choice of AI model
  • Fallback between providers

Pattern 4: State Management (Zustand)

The Problem

Managing complex nested state (workflows, UI config) without mutations

The Solution: Shallow Update Pattern

Create 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
    }))
}))

When to use:

  • Complex nested state
  • Frequent updates without mutations
  • Avoiding re-render issues

Pattern 5: Cross-Field Validation (Zod)

The Problem

Validating related fields (password confirmation, date ranges, etc.)

The Solution: 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'"
    })
  }
})

When to use:

  • Password confirmation
  • Date range validation
  • Conditional required fields
  • Cross-field business rules

Critical Rules

Always Do

✅ Adapt patterns to your auth system (Better Auth, Clerk, Auth.js, etc.) ✅ Use branded type tags for runtime type checking ✅ Use shallow updates for nested Zustand state ✅ Use Zod superRefine for cross-field validation ✅ Type your tool abstractions properly

Never Do

❌ Copy code without adapting to your auth/role system ❌ Assume tool type without runtime check ❌ Mutate Zustand state directly ❌ Use separate validators for related fields ❌ Skip type branding for extensible systems


Known Issues Prevention

This skill prevents 5 common issues:

Issue #1: Inconsistent Auth Checks

Prevention: Use validatedActionWithUser pattern (adapt to your auth)

Issue #2: Tool Type Mismatches

Prevention: Use branded type tags with .isMaybe() checks

Issue #3: State Mutation Bugs

Prevention: Use shallow Zustand update pattern

Issue #4: Cross-Field Validation Failures

Prevention: Use Zod superRefine for related fields

Issue #5: Provider Configuration Errors

Prevention: Use provider registry with unified interface


Using Bundled Resources

Templates (templates/)

  • templates/action-utils.ts - Complete server action validators
  • templates/tool-tags.ts - Complete tool abstraction system
  • templates/providers.ts - Multi-AI provider setup
  • templates/workflow-store.ts - Zustand workflow store

Copy to your project and adapt placeholders (getUser(), checkPermission(), etc.)


Dependencies

Required:

  • zod@3.24.2 - Validation (all patterns)
  • zustand@5.0.3 - State management (Pattern 4)
  • ai@5.0.82 - Vercel AI SDK (Pattern 3)

Optional (based on patterns used):

  • @ai-sdk/openai - OpenAI provider
  • @ai-sdk/anthropic - Anthropic provider
  • @ai-sdk/google - Google provider

Official Documentation


Production Example

These patterns are extracted from better-chatbot:

  • Live: https://betterchatbot.vercel.app
  • Tests: 48+ E2E tests passing
  • Errors: 0 (patterns proven in production)
  • Validation: ✅ Multi-user, multi-provider, workflow execution

Token Efficiency: ~65% savings | Errors Prevented: 5 | Production Verified: Yes

More by majiayu000

View all
deploying-postgres-k8s
180

Deploys PostgreSQL on Kubernetes using the CloudNativePG operator with automated failover. Use when setting up PostgreSQL for production workloads, high availability, or local K8s development. Covers operator installation, cluster creation, connection secrets, and backup configuration. NOT when using managed Postgres (Neon, RDS, Cloud SQL) or simple Docker containers.

energize-denver-proposals
180

Use when creating or updating Energize Denver compliance proposals including benchmarking, energy audits, compliance pathways, and performance target analysis. Handles proposal generation from template, cost estimation based on building size and service type, timeline planning, and compliance verification against Denver Article XIV requirements for commercial and multifamily buildings (project)

css-styling-standards
180

CSS styling standards and best practices for responsive, accessible, and maintainable web interfaces with special considerations for multilingual content and Chuukese text display. Use when creating or modifying stylesheets and CSS components.

testing-n8n-testing
180

Testing N8n Testing: Test n8n nodes using Jest, nock for HTTP mocking, and local testing workflows with npm link. Use this skill when writing test files in __tests__ folders, mocking HTTP requests with nock, creating mock IExecuteFunctions contexts, testing helper functions, setting up golden file tests, running the linter before publishing, or locally testing nodes in the n8n UI. Apply when organizing test files, mocking external APIs, validating node execution output, or following the pre-publish checklist.