From Zero to AI

Lesson 3.5: Practice - AI with API Access

Duration: 90 minutes

Learning Objectives

By the end of this lesson, you will have built:

  1. A complete AI assistant with multiple tools
  2. Tools that interact with real external APIs
  3. A tool loop with streaming support
  4. Robust error handling and timeout protection
  5. A CLI interface for testing the assistant

Project Overview

You will build an AI assistant with these tools:

  • Weather: Get current weather using Open-Meteo API (free, no key needed)
  • Calculator: Perform mathematical calculations
  • Wikipedia: Search and retrieve Wikipedia summaries
  • Time: Get current time in any timezone

Project Setup

Create the project structure:

mkdir ai-assistant-tools
cd ai-assistant-tools
npm init -y
npm install typescript tsx @types/node openai dotenv zod

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"]
}

Create .env:

OPENAI_API_KEY=sk-proj-your-key-here

Create .gitignore:

node_modules
dist
.env

Step 1: Type Definitions

Create src/types.ts:

import OpenAI from 'openai';

export interface ToolDefinition {
  tool: OpenAI.ChatCompletionTool;
  execute: (args: string) => Promise<string>;
}

export interface WeatherData {
  location: string;
  temperature: number;
  feelsLike: number;
  humidity: number;
  description: string;
  windSpeed: number;
}

export interface WikipediaResult {
  title: string;
  extract: string;
  url: string;
}

export interface CalculationResult {
  expression: string;
  result: number | string;
  error?: string;
}

export interface TimeResult {
  timezone: string;
  localTime: string;
  utcOffset: string;
}

Step 2: Weather Tool

Create src/tools/weather.ts:

import type OpenAI from 'openai';
import { z } from 'zod';

import type { ToolDefinition, WeatherData } from '../types';

const argsSchema = z.object({
  location: z.string().describe("City name, e.g., 'London' or 'Tokyo'"),
});

// Open-Meteo geocoding API (free, no key needed)
async function getCoordinates(
  city: string
): Promise<{ lat: number; lon: number; name: string } | null> {
  const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`;

  const response = await fetch(url);
  const data = await response.json();

  if (!data.results || data.results.length === 0) {
    return null;
  }

  const result = data.results[0];
  return {
    lat: result.latitude,
    lon: result.longitude,
    name: result.name,
  };
}

// Open-Meteo weather API (free, no key needed)
async function fetchWeather(lat: number, lon: number): Promise<object> {
  const url =
    `https://api.open-meteo.com/v1/forecast?` +
    `latitude=${lat}&longitude=${lon}` +
    `&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m`;

  const response = await fetch(url);
  return response.json();
}

function getWeatherDescription(code: number): string {
  const descriptions: Record<number, string> = {
    0: 'Clear sky',
    1: 'Mainly clear',
    2: 'Partly cloudy',
    3: 'Overcast',
    45: 'Foggy',
    48: 'Depositing rime fog',
    51: 'Light drizzle',
    53: 'Moderate drizzle',
    55: 'Dense drizzle',
    61: 'Slight rain',
    63: 'Moderate rain',
    65: 'Heavy rain',
    71: 'Slight snow',
    73: 'Moderate snow',
    75: 'Heavy snow',
    80: 'Slight rain showers',
    81: 'Moderate rain showers',
    82: 'Violent rain showers',
    95: 'Thunderstorm',
  };
  return descriptions[code] ?? 'Unknown';
}

async function execute(argsJson: string): Promise<string> {
  try {
    const args = argsSchema.parse(JSON.parse(argsJson));

    const coords = await getCoordinates(args.location);
    if (!coords) {
      return JSON.stringify({
        error: `Could not find location: ${args.location}`,
        suggestion: 'Try a different city name or add the country',
      });
    }

    const weather = (await fetchWeather(coords.lat, coords.lon)) as {
      current: {
        temperature_2m: number;
        apparent_temperature: number;
        relative_humidity_2m: number;
        weather_code: number;
        wind_speed_10m: number;
      };
    };

    const result: WeatherData = {
      location: coords.name,
      temperature: Math.round(weather.current.temperature_2m),
      feelsLike: Math.round(weather.current.apparent_temperature),
      humidity: weather.current.relative_humidity_2m,
      description: getWeatherDescription(weather.current.weather_code),
      windSpeed: Math.round(weather.current.wind_speed_10m),
    };

    return JSON.stringify(result);
  } catch (error) {
    return JSON.stringify({
      error: 'Failed to fetch weather',
      message: error instanceof Error ? error.message : 'Unknown error',
    });
  }
}

export const weatherTool: ToolDefinition = {
  tool: {
    type: 'function',
    function: {
      name: 'get_weather',
      description:
        'Get current weather conditions for a city. Returns temperature, humidity, ' +
        'wind speed, and conditions. Use this when users ask about weather, temperature, ' +
        'or if they should bring an umbrella/jacket.',
      parameters: {
        type: 'object',
        properties: {
          location: {
            type: 'string',
            description: "City name, e.g., 'London', 'Tokyo', 'New York'",
          },
        },
        required: ['location'],
      },
    },
  },
  execute,
};

Step 3: Calculator Tool

Create src/tools/calculator.ts:

import type OpenAI from 'openai';
import { z } from 'zod';

import type { CalculationResult, ToolDefinition } from '../types';

const argsSchema = z.object({
  expression: z.string().describe('Mathematical expression to evaluate'),
});

// Safe math evaluation (no eval!)
function evaluateExpression(expr: string): number {
  // Remove whitespace
  const cleaned = expr.replace(/\s+/g, '');

  // Only allow safe characters
  if (!/^[\d+\-*/().%^sqrt]+$/i.test(cleaned)) {
    throw new Error('Invalid characters in expression');
  }

  // Handle percentage
  let processed = cleaned.replace(/(\d+)%/g, '($1/100)');

  // Handle sqrt
  processed = processed.replace(/sqrt\(([^)]+)\)/gi, 'Math.sqrt($1)');

  // Handle power (^)
  processed = processed.replace(/\^/g, '**');

  // Evaluate using Function constructor (safer than eval)
  const fn = new Function(`"use strict"; return (${processed})`);
  const result = fn();

  if (typeof result !== 'number' || !isFinite(result)) {
    throw new Error('Invalid result');
  }

  return result;
}

async function execute(argsJson: string): Promise<string> {
  try {
    const args = argsSchema.parse(JSON.parse(argsJson));

    const result = evaluateExpression(args.expression);

    const output: CalculationResult = {
      expression: args.expression,
      result: Number.isInteger(result) ? result : Number(result.toFixed(6)),
    };

    return JSON.stringify(output);
  } catch (error) {
    return JSON.stringify({
      expression: JSON.parse(argsJson).expression ?? 'unknown',
      error: 'Calculation failed',
      message: error instanceof Error ? error.message : 'Unknown error',
    });
  }
}

export const calculatorTool: ToolDefinition = {
  tool: {
    type: 'function',
    function: {
      name: 'calculate',
      description:
        'Perform mathematical calculations. Supports basic operations (+, -, *, /), ' +
        "percentages (e.g., '15% of 200' as '200*15%'), square roots (sqrt), " +
        'and exponents (^). Use this for any math the user needs computed.',
      parameters: {
        type: 'object',
        properties: {
          expression: {
            type: 'string',
            description: "Math expression, e.g., '2+2', '15%*200', 'sqrt(144)', '2^8'",
          },
        },
        required: ['expression'],
      },
    },
  },
  execute,
};

Step 4: Wikipedia Tool

Create src/tools/wikipedia.ts:

import type OpenAI from 'openai';
import { z } from 'zod';

import type { ToolDefinition, WikipediaResult } from '../types';

const argsSchema = z.object({
  query: z.string().describe('Search term or topic'),
});

async function searchWikipedia(query: string): Promise<WikipediaResult | null> {
  // Search for the page
  const searchUrl =
    `https://en.wikipedia.org/w/api.php?` +
    `action=query&list=search&srsearch=${encodeURIComponent(query)}` +
    `&format=json&origin=*`;

  const searchResponse = await fetch(searchUrl);
  const searchData = (await searchResponse.json()) as {
    query: { search: Array<{ title: string }> };
  };

  if (!searchData.query?.search?.length) {
    return null;
  }

  const title = searchData.query.search[0].title;

  // Get the summary
  const summaryUrl =
    `https://en.wikipedia.org/api/rest_v1/page/summary/` + encodeURIComponent(title);

  const summaryResponse = await fetch(summaryUrl);
  const summaryData = (await summaryResponse.json()) as {
    title: string;
    extract: string;
    content_urls?: { desktop?: { page?: string } };
  };

  return {
    title: summaryData.title,
    extract: summaryData.extract,
    url: summaryData.content_urls?.desktop?.page ?? '',
  };
}

async function execute(argsJson: string): Promise<string> {
  try {
    const args = argsSchema.parse(JSON.parse(argsJson));

    const result = await searchWikipedia(args.query);

    if (!result) {
      return JSON.stringify({
        error: 'No Wikipedia article found',
        query: args.query,
        suggestion: 'Try a different search term',
      });
    }

    return JSON.stringify(result);
  } catch (error) {
    return JSON.stringify({
      error: 'Wikipedia search failed',
      message: error instanceof Error ? error.message : 'Unknown error',
    });
  }
}

export const wikipediaTool: ToolDefinition = {
  tool: {
    type: 'function',
    function: {
      name: 'search_wikipedia',
      description:
        'Search Wikipedia and get a summary of an article. Use this when users ask ' +
        'about facts, history, people, places, concepts, or want to learn about a topic.',
      parameters: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'The topic to search for on Wikipedia',
          },
        },
        required: ['query'],
      },
    },
  },
  execute,
};

Step 5: Time Tool

Create src/tools/time.ts:

import type OpenAI from 'openai';
import { z } from 'zod';

import type { TimeResult, ToolDefinition } from '../types';

const argsSchema = z.object({
  timezone: z.string().optional().describe("IANA timezone, e.g., 'America/New_York'"),
});

// Common timezone mappings
const timezoneAliases: Record<string, string> = {
  est: 'America/New_York',
  edt: 'America/New_York',
  cst: 'America/Chicago',
  cdt: 'America/Chicago',
  mst: 'America/Denver',
  mdt: 'America/Denver',
  pst: 'America/Los_Angeles',
  pdt: 'America/Los_Angeles',
  gmt: 'Europe/London',
  utc: 'UTC',
  jst: 'Asia/Tokyo',
  tokyo: 'Asia/Tokyo',
  london: 'Europe/London',
  paris: 'Europe/Paris',
  berlin: 'Europe/Berlin',
  sydney: 'Australia/Sydney',
  'new york': 'America/New_York',
  'los angeles': 'America/Los_Angeles',
  chicago: 'America/Chicago',
};

function resolveTimezone(input?: string): string {
  if (!input) return Intl.DateTimeFormat().resolvedOptions().timeZone;

  const lower = input.toLowerCase().trim();
  return timezoneAliases[lower] ?? input;
}

async function execute(argsJson: string): Promise<string> {
  try {
    const args = argsSchema.parse(JSON.parse(argsJson));
    const timezone = resolveTimezone(args.timezone);

    const now = new Date();

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

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

    const offsetParts = offsetFormatter.formatToParts(now);
    const offsetPart = offsetParts.find((p) => p.type === 'timeZoneName');

    const result: TimeResult = {
      timezone,
      localTime: formatter.format(now),
      utcOffset: offsetPart?.value ?? 'Unknown',
    };

    return JSON.stringify(result);
  } catch (error) {
    return JSON.stringify({
      error: 'Failed to get time',
      message: error instanceof Error ? error.message : 'Unknown error',
      suggestion: "Use IANA timezone format like 'America/New_York'",
    });
  }
}

export const timeTool: ToolDefinition = {
  tool: {
    type: 'function',
    function: {
      name: 'get_time',
      description:
        'Get the current time in a specific timezone. Supports IANA timezones ' +
        "(e.g., 'America/New_York', 'Europe/London') and common abbreviations " +
        "(e.g., 'EST', 'PST', 'GMT'). Use when users ask about current time.",
      parameters: {
        type: 'object',
        properties: {
          timezone: {
            type: 'string',
            description: 'Timezone name or abbreviation. Defaults to local time if not specified.',
          },
        },
        required: [],
      },
    },
  },
  execute,
};

Step 6: Tool Registry

Create src/tools/index.ts:

import type OpenAI from 'openai';

import type { ToolDefinition } from '../types';
import { calculatorTool } from './calculator';
import { timeTool } from './time';
import { weatherTool } from './weather';
import { wikipediaTool } from './wikipedia';

// Registry of all tools
const toolRegistry: Record<string, ToolDefinition> = {
  get_weather: weatherTool,
  calculate: calculatorTool,
  search_wikipedia: wikipediaTool,
  get_time: timeTool,
};

// Get tool definitions for API calls
export function getToolDefinitions(): OpenAI.ChatCompletionTool[] {
  return Object.values(toolRegistry).map((def) => def.tool);
}

// Execute a tool by name
export async function executeTool(name: string, args: string): Promise<string> {
  const tool = toolRegistry[name];

  if (!tool) {
    return JSON.stringify({
      error: `Unknown tool: ${name}`,
      availableTools: Object.keys(toolRegistry),
    });
  }

  // Add timeout protection
  const timeoutMs = 10000;
  const timeoutPromise = new Promise<string>((_, reject) => {
    setTimeout(() => reject(new Error('Tool execution timed out')), timeoutMs);
  });

  try {
    return await Promise.race([tool.execute(args), timeoutPromise]);
  } catch (error) {
    return JSON.stringify({
      error: 'Tool execution failed',
      tool: name,
      message: error instanceof Error ? error.message : 'Unknown error',
    });
  }
}

// List available tools
export function listTools(): string[] {
  return Object.keys(toolRegistry);
}

Step 7: Assistant with Tool Loop

Create src/assistant.ts:

import OpenAI from 'openai';

import { executeTool, getToolDefinitions } from './tools';

const openai = new OpenAI();

const SYSTEM_PROMPT = `You are a helpful AI assistant with access to several tools:

1. get_weather: Get current weather for any city
2. calculate: Perform mathematical calculations
3. search_wikipedia: Look up information on Wikipedia
4. get_time: Get current time in any timezone

Guidelines:
- Use tools when you need current information or calculations
- Always summarize tool results in a natural, conversational way
- If a tool fails, explain the issue and suggest alternatives
- You can use multiple tools to answer complex questions
- Be concise but informative in your responses`;

export async function chat(userMessage: string): Promise<string> {
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    { role: 'user', content: userMessage },
  ];

  const tools = getToolDefinitions();
  const maxIterations = 5;

  for (let i = 0; i < maxIterations; i++) {
    const response = await openai.chat.completions.create({
      model: 'gpt-4o',
      messages,
      tools,
    });

    const assistantMessage = response.choices[0].message;

    // No tool calls - return the response
    if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
      return assistantMessage.content ?? '';
    }

    // Add assistant message to history
    messages.push(assistantMessage);

    // Execute all tool calls
    for (const toolCall of assistantMessage.tool_calls) {
      console.log(`  [Tool: ${toolCall.function.name}]`);

      const result = await executeTool(toolCall.function.name, toolCall.function.arguments);

      messages.push({
        role: 'tool',
        tool_call_id: toolCall.id,
        content: result,
      });
    }
  }

  return 'I was unable to complete the request. Please try a simpler question.';
}

// Streaming version
export async function chatStream(
  userMessage: string,
  onToken: (token: string) => void
): Promise<void> {
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    { role: 'user', content: userMessage },
  ];

  const tools = getToolDefinitions();
  const maxIterations = 5;

  for (let i = 0; i < maxIterations; i++) {
    const stream = await openai.chat.completions.create({
      model: 'gpt-4o',
      messages,
      tools,
      stream: true,
    });

    let content = '';
    const toolCalls: Map<number, { id: string; name: string; arguments: string }> = new Map();

    for await (const chunk of stream) {
      const delta = chunk.choices[0].delta;

      if (delta.content) {
        onToken(delta.content);
        content += delta.content;
      }

      if (delta.tool_calls) {
        for (const tc of delta.tool_calls) {
          const existing = toolCalls.get(tc.index) ?? {
            id: '',
            name: '',
            arguments: '',
          };
          if (tc.id) existing.id = tc.id;
          if (tc.function?.name) existing.name = tc.function.name;
          if (tc.function?.arguments) existing.arguments += tc.function.arguments;
          toolCalls.set(tc.index, existing);
        }
      }
    }

    // No tool calls - done
    if (toolCalls.size === 0) {
      return;
    }

    // Process tool calls
    const toolCallsArray = Array.from(toolCalls.values());

    messages.push({
      role: 'assistant',
      content: content || null,
      tool_calls: toolCallsArray.map((tc) => ({
        id: tc.id,
        type: 'function' as const,
        function: { name: tc.name, arguments: tc.arguments },
      })),
    });

    for (const tc of toolCallsArray) {
      onToken(`\n[Using ${tc.name}...]\n`);
      const result = await executeTool(tc.name, tc.arguments);
      messages.push({
        role: 'tool',
        tool_call_id: tc.id,
        content: result,
      });
    }
  }
}

Step 8: CLI Interface

Create src/main.ts:

import 'dotenv/config';
import * as readline from 'readline';

import { chatStream } from './assistant';
import { listTools } from './tools';

async function main(): Promise<void> {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  console.log('AI Assistant with Tools');
  console.log('=======================');
  console.log(`Available tools: ${listTools().join(', ')}`);
  console.log('Type "exit" to quit\n');

  const prompt = (): void => {
    rl.question('You: ', async (input) => {
      const trimmed = input.trim();

      if (!trimmed) {
        prompt();
        return;
      }

      if (trimmed.toLowerCase() === 'exit') {
        console.log('Goodbye!');
        rl.close();
        return;
      }

      process.stdout.write('\nAssistant: ');

      try {
        await chatStream(trimmed, (token) => {
          process.stdout.write(token);
        });
        console.log('\n');
      } catch (error) {
        console.log('\n');
        if (error instanceof Error) {
          console.error(`Error: ${error.message}\n`);
        }
      }

      prompt();
    });
  };

  prompt();
}

main().catch(console.error);

Step 9: Running the Assistant

Add scripts to package.json:

{
  "type": "module",
  "scripts": {
    "start": "tsx src/main.ts",
    "build": "tsc"
  }
}

Run the assistant:

npm start

Example Conversations

Try these prompts:

You: What's the weather in Tokyo and Paris?
Assistant: [Using get_weather...]
[Using get_weather...]
The weather in Tokyo is currently 22°C (feels like 21°C) with partly cloudy
skies and 65% humidity. In Paris, it's 15°C (feels like 13°C) with light
rain and 78% humidity. Tokyo is warmer and drier today!

You: Calculate 15% of 2500 plus 350
Assistant: [Using calculate...]
15% of 2500 is 375, plus 350 equals 725.

You: Tell me about the Eiffel Tower
Assistant: [Using search_wikipedia...]
The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in
Paris, France. Named after engineer Gustave Eiffel, it was constructed
from 1887 to 1889 and stands 330 meters tall...

You: What time is it in Tokyo and New York?
Assistant: [Using get_time...]
[Using get_time...]
In Tokyo, it's currently 10:30 PM on Thursday. In New York, it's 9:30 AM
on Thursday - there's a 13-hour difference between the two cities.

Challenges

Extend the assistant with these features:

  1. Add a new tool: Implement a currency converter using a free API
  2. Conversation memory: Maintain context across multiple turns
  3. Rate limiting: Track API calls and implement throttling
  4. Better error messages: Provide more helpful suggestions when tools fail
  5. Logging: Add detailed logging for debugging

Final Project Structure

ai-assistant-tools/
├── src/
│   ├── types.ts
│   ├── tools/
│   │   ├── index.ts
│   │   ├── weather.ts
│   │   ├── calculator.ts
│   │   ├── wikipedia.ts
│   │   └── time.ts
│   ├── assistant.ts
│   └── main.ts
├── .env
├── .gitignore
├── package.json
└── tsconfig.json

Key Takeaways

  1. Tools extend AI capabilities beyond text generation
  2. Free APIs exist for weather, Wikipedia, and more
  3. Tool registry pattern keeps code organized
  4. Streaming with tools provides responsive UX
  5. Error handling is critical for production applications

Resources

Resource Type Level
Open-Meteo API API Beginner
Wikipedia API Documentation Beginner
OpenAI Function Calling Documentation Intermediate
Zod Documentation Documentation Intermediate

Module Complete

Congratulations! You have completed Module 3: Function Calling. You now understand:

  • What function calling is and when to use it
  • How to define tools with proper schemas
  • How to handle tool calls and execute functions
  • How to manage multi-turn conversations with tools
  • How to build a complete AI assistant with external API access

In the next module, you will learn about RAG (Retrieval Augmented Generation) - enabling AI to answer questions using your own documents and data.

Continue to Module 4: RAG