Claude
Skills
Sign in
Back

ai-tools

Included with Lifetime
$97 forever

Tool calling framework with built-in calculator and datetime tools. Renders tool invocations as collapsible cards in the chat UI. Use this skill when the user says "add tools", "tool calling", "calculator", or "add ai tools".

Design

What this skill does


# AI Tools Skill

Adds a tool calling framework to the AI chat system with built-in calculator and datetime tools. Tool invocations render as collapsible cards in the message stream showing tool name, input parameters, and output result.

## Prerequisites

- `ai-chat` skill applied (provides `route.ts` with comment slots, `message.tsx` with part switch)
- `ai-core` skill applied (provides `getModel()`)

## Installation

No additional packages required. Uses `tool()` from `ai` and `z` from `zod`, both already installed by `ai-core` and `ai-chat`.

## What Gets Created

```
src/
├── lib/
│   └── ai/
│       └── tools/
│           ├── calculator.ts    # Safe math evaluator (no eval())
│           ├── datetime.ts      # Date/time with timezone support
│           └── index.ts         # Tool registry — exports allTools
└── components/
    └── ai/
        └── message.tsx          # MODIFIED — add tool-invocation case
```

Plus modification to:

```
src/app/api/ai/chat/route.ts    # MODIFIED — spread allTools into tools object
```

## Comment Slots

- **route.ts**: `// [ai-tools]: spread registered tools here` — where `allTools` is merged into the tools object
- **message.tsx**: `// [ai-tools]: handle tool UI parts` — default case renders `ToolInvocationCard` for tool invocations

## Setup Steps

### Step 1: Create `src/lib/ai/tools/calculator.ts`

```typescript
import { tool } from "ai";
import { z } from "zod";

type Operator = "+" | "-" | "*" | "/" | "^" | "%";

interface NumberNode {
  type: "number";
  value: number;
}

interface BinaryOpNode {
  type: "binary";
  operator: Operator;
  left: ExprNode;
  right: ExprNode;
}

interface UnaryMinusNode {
  type: "unary";
  operand: ExprNode;
}

type ExprNode = NumberNode | BinaryOpNode | UnaryMinusNode;

interface Token {
  type: "number" | "operator" | "lparen" | "rparen";
  value: string;
}

function tokenize(expression: string): Token[] {
  const tokens: Token[] = [];
  let i = 0;
  const input = expression.replace(/\s+/g, "");

  while (i < input.length) {
    const ch = input[i];

    if (ch >= "0" && ch <= "9" || ch === ".") {
      let num = "";
      while (i < input.length && (input[i] >= "0" && input[i] <= "9" || input[i] === ".")) {
        num += input[i];
        i++;
      }
      tokens.push({ type: "number", value: num });
      continue;
    }

    if ("+-*/%^".includes(ch)) {
      tokens.push({ type: "operator", value: ch });
      i++;
      continue;
    }

    if (ch === "(") {
      tokens.push({ type: "lparen", value: ch });
      i++;
      continue;
    }

    if (ch === ")") {
      tokens.push({ type: "rparen", value: ch });
      i++;
      continue;
    }

    throw new Error(`Unexpected character: ${ch}`);
  }

  return tokens;
}

function parse(tokens: Token[]): ExprNode {
  let pos = 0;

  function peek(): Token | undefined {
    return tokens[pos];
  }

  function consume(): Token {
    const token = tokens[pos];
    pos++;
    return token;
  }

  function parseExpression(): ExprNode {
    return parseAddSub();
  }

  function parseAddSub(): ExprNode {
    let left = parseMulDiv();
    while (peek()?.type === "operator" && (peek()?.value === "+" || peek()?.value === "-")) {
      const op = consume().value as Operator;
      const right = parseMulDiv();
      left = { type: "binary", operator: op, left, right };
    }
    return left;
  }

  function parseMulDiv(): ExprNode {
    let left = parsePower();
    while (peek()?.type === "operator" && (peek()?.value === "*" || peek()?.value === "/" || peek()?.value === "%")) {
      const op = consume().value as Operator;
      const right = parsePower();
      left = { type: "binary", operator: op, left, right };
    }
    return left;
  }

  function parsePower(): ExprNode {
    let left = parseUnary();
    while (peek()?.type === "operator" && peek()?.value === "^") {
      consume();
      const right = parseUnary();
      left = { type: "binary", operator: "^", left, right };
    }
    return left;
  }

  function parseUnary(): ExprNode {
    if (peek()?.type === "operator" && peek()?.value === "-") {
      consume();
      const operand = parsePrimary();
      return { type: "unary", operand };
    }
    return parsePrimary();
  }

  function parsePrimary(): ExprNode {
    const token = peek();

    if (token?.type === "number") {
      consume();
      return { type: "number", value: parseFloat(token.value) };
    }

    if (token?.type === "lparen") {
      consume();
      const expr = parseExpression();
      const closing = consume();
      if (closing?.type !== "rparen") {
        throw new Error("Missing closing parenthesis");
      }
      return expr;
    }

    throw new Error(`Unexpected token: ${token?.value ?? "end of input"}`);
  }

  const result = parseExpression();
  if (pos < tokens.length) {
    throw new Error(`Unexpected token after expression: ${tokens[pos].value}`);
  }
  return result;
}

function evaluate(node: ExprNode): number {
  switch (node.type) {
    case "number":
      return node.value;
    case "unary":
      return -evaluate(node.operand);
    case "binary": {
      const left = evaluate(node.left);
      const right = evaluate(node.right);
      switch (node.operator) {
        case "+": return left + right;
        case "-": return left - right;
        case "*": return left * right;
        case "/": {
          if (right === 0) throw new Error("Division by zero");
          return left / right;
        }
        case "%": {
          if (right === 0) throw new Error("Modulo by zero");
          return left % right;
        }
        case "^": return Math.pow(left, right);
      }
    }
  }
}

function evaluateSafely(expression: string): number {
  const tokens = tokenize(expression);
  const ast = parse(tokens);
  return evaluate(ast);
}

export const calculator = tool({
  description:
    "Evaluate a mathematical expression. Supports +, -, *, /, ^, %, and parentheses. " +
    "Examples: '15 * 4 + 10', '(3 + 5) * 2', '2 ^ 10', '17 % 5'.",
  inputSchema: z.object({
    expression: z
      .string()
      .describe("The mathematical expression to evaluate, e.g. '(3 + 5) * 2'"),
  }),
  execute: async ({ expression }) => {
    try {
      const result = evaluateSafely(expression);
      return { expression, result, success: true as const };
    } catch (error) {
      const message = error instanceof Error ? error.message : "Unknown error";
      return { expression, error: message, success: false as const };
    }
  },
});
```

### Step 2: Create `src/lib/ai/tools/datetime.ts`

```typescript
import { tool } from "ai";
import { z } from "zod";

interface DateTimeSuccess {
  timezone: string;
  iso: string;
  formatted: string;
  date: string;
  time: string;
  dayOfWeek: string;
  utcOffset: string;
  success: true;
}

interface DateTimeError {
  timezone: string;
  error: string;
  success: false;
}

type DateTimeResult = DateTimeSuccess | DateTimeError;

function getDateTimeInTimezone(timezone: string): DateTimeResult {
  try {
    const now = new Date();

    const formatter = new Intl.DateTimeFormat("en-US", {
      timeZone: timezone,
      year: "numeric",
      month: "long",
      day: "numeric",
      weekday: "long",
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
      hour12: true,
      timeZoneName: "short",
    });

    const dateFormatter = new Intl.DateTimeFormat("en-CA", {
      timeZone: timezone,
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
    });

    const timeFormatter = new Intl.DateTimeFormat("en-US", {
      timeZone: timezone,
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
      hour12: true,
    });

    const dayFormatter = new Intl.DateTimeFormat("en-US", {
      timeZone: timezone,
      weekday: "long",
    });

    const offsetFormatter = new Intl.DateTimeFormat("en-US", {
      timeZone: timezone,
      timeZoneName: "longOffset",
    });

    const offsetParts = offsetForma
Files: 1
Size: 15.7 KB
Complexity: 25/100
Category: Design

Related in Design