Lesson 3.5: Practice - AI with API Access
Duration: 90 minutes
Learning Objectives
By the end of this lesson, you will have built:
- A complete AI assistant with multiple tools
- Tools that interact with real external APIs
- A tool loop with streaming support
- Robust error handling and timeout protection
- 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}` +
`¤t=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:
- Add a new tool: Implement a currency converter using a free API
- Conversation memory: Maintain context across multiple turns
- Rate limiting: Track API calls and implement throttling
- Better error messages: Provide more helpful suggestions when tools fail
- 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
- Tools extend AI capabilities beyond text generation
- Free APIs exist for weather, Wikipedia, and more
- Tool registry pattern keeps code organized
- Streaming with tools provides responsive UX
- 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.