better-chatbot-patterns

安装量: 49
排名: #14999

安装

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

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:

Server action validators (auth, validation, FormData) Tool abstraction system (multi-type tool handling) Multi-AI provider setup Workflow execution patterns 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 = | { success: true; data: T } | { success: false; error: string }

// Pattern 1: Simple validation (no auth) export function validatedAction( schema: TSchema, handler: ( data: z.infer, formData: FormData ) => Promise> ) { return async (formData: FormData): Promise> => { 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( schema: TSchema, handler: ( data: z.infer, formData: FormData, user: { id: string; email: string } // Adapt to your User type ) => Promise> ) { return async (formData: FormData): Promise> => { 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( schema: TSchema, permission: "admin" | "user-manage" | string, // Your permission types handler: ( data: z.infer, formData: FormData, user: { id: string; email: string; role: string } ) => Promise> ) { return async (formData: FormData): Promise> => { 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 { private readonly _tag: T private readonly _branded: unique symbol

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

static create(tag: TTag) { return new ToolTag(tag) as ToolTag }

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 } 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((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 Vercel AI SDK: https://sdk.vercel.ai/docs Zod: https://zod.dev Zustand: https://zustand-demo.pmnd.rs better-chatbot (source): https://github.com/cgoinglove/better-chatbot 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

返回排行榜