├── src ├── tools │ ├── index.ts │ └── weather-api.ts ├── common │ ├── index.ts │ ├── types.ts │ ├── utils.ts │ └── logger.ts ├── agent │ ├── output-structure.ts │ ├── system-prompt.ts │ └── index.ts ├── auth.ts ├── index.ts └── graph.ts ├── langgraph.json ├── .editorconfig ├── tests └── vitest.config.ts ├── .gitignore ├── tsconfig.json ├── LICENSE ├── docker-compose.yml ├── examples ├── basic-usage.ts ├── forecast-example.ts ├── travel-planning.ts └── custom-prompt.ts ├── Dockerfile ├── eslint.config.mjs ├── package.json ├── docker-compose.full.yml ├── SELF-HOSTING.md ├── README.md └── EXAMPLES.md /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './weather-api'; 2 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './types'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | export type AgentResponse = { 2 | question: string; 3 | response: { 4 | messages: any[]; 5 | structuredResponse: any; 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "node_version": "20", 3 | "graphs": { 4 | "weather_agent": "./src/graph.ts:graph" 5 | }, 6 | "auth": { 7 | "path": "./src/auth.ts:auth" 8 | }, 9 | "env": ".env", 10 | "dependencies": ["."] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | insert_final_newline = false 11 | trim_trailing_whitespace = false 12 | 13 | [*.{js,json,ts,mts,yml,yaml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | include: ['tests/**/*.test.ts'], 8 | coverage: { 9 | provider: 'v8', 10 | reporter: ['text', 'json', 'html'], 11 | exclude: ['node_modules/', 'build/', 'tests/'], 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | const resultDir = 'outputs'; 4 | 5 | export function writeAgentResult( 6 | timestamp: number, 7 | model: string, 8 | results: any, 9 | ): void { 10 | if (!fs.existsSync(resultDir)) { 11 | fs.mkdirSync(resultDir); 12 | } 13 | fs.writeFileSync( 14 | `${resultDir}/${timestamp}-${model}.json`, 15 | JSON.stringify(results, null, 2), 16 | 'utf8', 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/common/logger.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | constructor(private name: string) {} 3 | 4 | info(message: string, ...args: unknown[]): void { 5 | console.log(`[${this.name}] INFO: ${message}`, ...args); 6 | } 7 | 8 | error(message: string, ...args: unknown[]): void { 9 | console.error(`[${this.name}] ERROR: ${message}`, ...args); 10 | } 11 | 12 | warn(message: string, ...args: unknown[]): void { 13 | console.warn(`[${this.name}] WARN: ${message}`, ...args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependencies 7 | node_modules/ 8 | yarn.lock 9 | package-lock.json 10 | 11 | # Coverage 12 | coverage 13 | 14 | # Transpiled files 15 | build/ 16 | dist/ 17 | 18 | # Compiled TypeScript files in tests directories 19 | tests/**/*.js 20 | tests/**/*.js.map 21 | tests/**/*.d.ts 22 | tests/**/*.d.ts.map 23 | __tests__/**/*.js 24 | __tests__/**/*.js.map 25 | __tests__/**/*.d.ts 26 | __tests__/**/*.d.ts.map 27 | 28 | # VS Code 29 | .vscode 30 | !.vscode/tasks.js 31 | 32 | # JetBrains IDEs 33 | .idea/ 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional eslint cache 39 | .eslintcache 40 | 41 | # Misc 42 | .DS_Store 43 | .env 44 | *.tsbuildinfo 45 | 46 | # Agent outputs 47 | outputs -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "ESNext", 5 | "lib": ["ES2022", "DOM"], 6 | "types": ["node"], 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "importHelpers": true, 10 | "alwaysStrict": true, 11 | "sourceMap": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitAny": false, 18 | "noImplicitThis": false, 19 | "strictNullChecks": false, 20 | "skipLibCheck": true, 21 | "declaration": true, 22 | "declarationMap": true, 23 | "rootDir": "src", 24 | "outDir": "build" 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /src/agent/output-structure.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const ResponseSchema = z.object({ 4 | answer: z 5 | .string() 6 | .describe( 7 | 'A natural language response to the user question about weather, including all relevant information.', 8 | ), 9 | location: z 10 | .string() 11 | .describe('The location the weather information is about.'), 12 | summary: z 13 | .string() 14 | .describe('A brief summary of the weather conditions or forecast.'), 15 | recommendations: z 16 | .array(z.string()) 17 | .describe( 18 | 'List of helpful recommendations based on the weather (e.g., what to wear, activities to do/avoid).', 19 | ), 20 | data_source: z 21 | .string() 22 | .describe( 23 | 'Indication of what data was used to answer the question (e.g., "current weather", "3-day forecast").', 24 | ), 25 | }); 26 | 27 | export type ResponseType = z.infer; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Warden Protocol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Simple standalone weather agent 3 | # No database or Redis needed for stateless requests 4 | weather-agent: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: weather-agent 9 | ports: 10 | - "8000:8000" 11 | environment: 12 | # Required API Keys 13 | WEATHER_API_KEY: ${WEATHER_API_KEY} 14 | OPENAI_API_KEY: ${OPENAI_API_KEY} 15 | 16 | # Agent API Key (for authentication) 17 | AGENT_API_KEY: ${AGENT_API_KEY} 18 | 19 | # Optional: Model configuration 20 | MODEL_NAME: ${MODEL_NAME:-gpt-4o-mini} 21 | TEMPERATURE: ${TEMPERATURE:-0} 22 | 23 | # Optional: LangSmith tracing 24 | LANGSMITH_API_KEY: ${LANGSMITH_API_KEY:-} 25 | LANGSMITH_PROJECT: ${LANGSMITH_PROJECT:-weather-agent} 26 | LANGSMITH_TRACING: ${LANGSMITH_TRACING:-false} 27 | 28 | # Server configuration 29 | NODE_ENV: production 30 | restart: unless-stopped 31 | healthcheck: 32 | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8000/"] 33 | interval: 30s 34 | timeout: 3s 35 | retries: 3 36 | start_period: 40s 37 | -------------------------------------------------------------------------------- /examples/basic-usage.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { runAgentWithSaveResults } from '../src/index'; 3 | 4 | // Load environment variables 5 | dotenv.config(); 6 | 7 | /** 8 | * Basic usage example of the Weather Agent 9 | * This demonstrates simple weather queries for different cities 10 | */ 11 | async function main(): Promise { 12 | console.log('🌤️ Basic Weather Agent Usage Example\n'); 13 | 14 | const questions = [ 15 | 'What is the current weather in London?', 16 | 'What is the temperature in Tokyo?', 17 | 'Is it raining in Seattle?', 18 | 'What is the weather like in Sydney?', 19 | ]; 20 | 21 | // Get configuration from environment or use defaults 22 | const modelName = process.env.MODEL_NAME || 'gpt-4o-mini'; 23 | const temperature = process.env.TEMPERATURE 24 | ? parseFloat(process.env.TEMPERATURE) 25 | : 0; 26 | 27 | try { 28 | await runAgentWithSaveResults(questions, { 29 | modelName, 30 | temperature, 31 | delayBetweenQuestionsMs: 1000, // 1 second delay between questions 32 | }); 33 | } catch (error) { 34 | console.error('Error:', error); 35 | process.exit(1); 36 | } 37 | } 38 | 39 | main(); 40 | -------------------------------------------------------------------------------- /examples/forecast-example.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { runAgentWithSaveResults } from '../src/index'; 3 | 4 | // Load environment variables 5 | dotenv.config(); 6 | 7 | /** 8 | * Weather forecast example 9 | * This demonstrates querying weather forecasts for multiple days 10 | */ 11 | async function main(): Promise { 12 | console.log('📅 Weather Forecast Example\n'); 13 | 14 | const questions = [ 15 | 'Give me a 3-day weather forecast for Paris', 16 | 'What will the weather be like in New York over the next 5 days?', 17 | 'Will it rain in Los Angeles this week?', 18 | 'What is the weather forecast for Berlin for the next 3 days?', 19 | 'Should I plan outdoor activities in Miami for the weekend?', 20 | ]; 21 | 22 | // Get configuration from environment or use defaults 23 | const modelName = process.env.MODEL_NAME || 'gpt-4o-mini'; 24 | const temperature = process.env.TEMPERATURE 25 | ? parseFloat(process.env.TEMPERATURE) 26 | : 0; 27 | 28 | try { 29 | await runAgentWithSaveResults(questions, { 30 | modelName, 31 | temperature, 32 | delayBetweenQuestionsMs: 1000, 33 | }); 34 | } catch (error) { 35 | console.error('Error:', error); 36 | process.exit(1); 37 | } 38 | } 39 | 40 | main(); 41 | -------------------------------------------------------------------------------- /examples/travel-planning.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { runAgentWithSaveResults } from '../src/index'; 3 | 4 | // Load environment variables 5 | dotenv.config(); 6 | 7 | /** 8 | * Travel planning example 9 | * This demonstrates using the weather agent for travel planning scenarios 10 | */ 11 | async function main(): Promise { 12 | console.log('✈️ Travel Planning Weather Example\n'); 13 | 14 | const questions = [ 15 | 'I am planning a trip to Barcelona next week. What should I pack based on the weather?', 16 | 'Is it a good time to visit Iceland? What is the weather like there?', 17 | 'Should I bring an umbrella for my trip to London tomorrow?', 18 | 'What will the weather be like in Dubai over the next 3 days? Is it too hot?', 19 | 'I am going hiking in Denver this weekend. What is the weather forecast?', 20 | 'What is the best clothing to wear in Singapore based on current weather?', 21 | ]; 22 | 23 | // Get configuration from environment or use defaults 24 | // Override with slightly higher temperature for more conversational responses 25 | const modelName = process.env.MODEL_NAME || 'gpt-4o-mini'; 26 | const temperature = 0.3; 27 | 28 | try { 29 | await runAgentWithSaveResults(questions, { 30 | modelName, 31 | temperature, 32 | delayBetweenQuestionsMs: 1500, 33 | }); 34 | } catch (error) { 35 | console.error('Error:', error); 36 | process.exit(1); 37 | } 38 | } 39 | 40 | main(); 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build 2 | FROM node:20-alpine AS builder 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files 8 | COPY package.json yarn.lock ./ 9 | 10 | # Install dependencies 11 | RUN yarn install --frozen-lockfile 12 | 13 | # Copy source code 14 | COPY . . 15 | 16 | # Build the TypeScript code 17 | RUN yarn build 18 | 19 | # Stage 2: Production 20 | FROM node:20-alpine AS runner 21 | 22 | # Set working directory 23 | WORKDIR /app 24 | 25 | # Install production dependencies only 26 | COPY package.json yarn.lock ./ 27 | RUN yarn install --frozen-lockfile --production 28 | 29 | # Install TypeScript and type definitions needed for LangGraph CLI 30 | RUN yarn add --dev typescript @types/node 31 | 32 | # Copy built code from builder stage 33 | COPY --from=builder /app/build ./build 34 | COPY --from=builder /app/src ./src 35 | COPY --from=builder /app/langgraph.json ./langgraph.json 36 | COPY --from=builder /app/tsconfig.json ./tsconfig.json 37 | 38 | # Create empty .env file (environment variables are passed via docker-compose) 39 | RUN touch .env 40 | 41 | # Set environment variables 42 | ENV NODE_ENV=production 43 | ENV PORT=8000 44 | 45 | # Expose port 46 | EXPOSE 8000 47 | 48 | # Health check 49 | HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ 50 | CMD node -e "require('http').get('http://localhost:8000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" 51 | 52 | # Start the LangGraph server 53 | CMD ["npx", "@langchain/langgraph-cli", "dev", "--host", "0.0.0.0", "--port", "8000", "--no-browser"] 54 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Authentication handler for the Weather Agent 3 | * 4 | * This module implements API key authentication to protect the self-hosted agent. 5 | * Clients must provide a valid API key in the 'x-api-key' header to access the agent. 6 | */ 7 | 8 | import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth"; 9 | 10 | /** 11 | * Authentication middleware that validates API keys 12 | * 13 | * Expected header: x-api-key: your-secret-api-key 14 | * 15 | * The valid API key should be set in the AGENT_API_KEY environment variable. 16 | * If no key is set, authentication is disabled (not recommended for production). 17 | */ 18 | export const auth = new Auth().authenticate(async (request: Request) => { 19 | const providedKey = request.headers.get("x-api-key"); 20 | const validKey = process.env.AGENT_API_KEY; 21 | 22 | // If no API key is configured, allow access (development mode) 23 | if (!validKey) { 24 | console.warn("⚠️ WARNING: AGENT_API_KEY not set - authentication disabled!"); 25 | return { 26 | identity: "unauthenticated", 27 | permissions: [], 28 | is_authenticated: true, 29 | }; 30 | } 31 | 32 | // Validate the provided API key 33 | if (!providedKey || providedKey !== validKey) { 34 | throw new HTTPException(401, { 35 | message: "Invalid or missing API key. Please provide a valid 'x-api-key' header." 36 | }); 37 | } 38 | 39 | // Authentication successful 40 | return { 41 | identity: "authenticated-user", 42 | permissions: [], 43 | is_authenticated: true, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import vitest from '@vitest/eslint-plugin'; 4 | import eslintConfigPrettier from 'eslint-config-prettier'; 5 | import globals from 'globals'; 6 | import tseslint from 'typescript-eslint'; 7 | 8 | // This is just an example default config for ESLint. 9 | // You should change it to your needs following the documentation. 10 | export default tseslint.config( 11 | { 12 | ignores: ['**/build/**', '**/tmp/**', '**/coverage/**'], 13 | }, 14 | eslint.configs.recommended, 15 | eslintConfigPrettier, 16 | { 17 | extends: [...tseslint.configs.recommended], 18 | 19 | files: ['**/*.ts', '**/*.mts', 'src/**/*.ts'], 20 | 21 | plugins: { 22 | '@typescript-eslint': tseslint.plugin, 23 | }, 24 | 25 | rules: { 26 | '@typescript-eslint/explicit-function-return-type': 'warn', 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | }, 29 | 30 | languageOptions: { 31 | parser: tseslint.parser, 32 | ecmaVersion: 2020, 33 | sourceType: 'module', 34 | 35 | globals: { 36 | ...globals.node, 37 | }, 38 | 39 | parserOptions: { 40 | project: true, 41 | }, 42 | }, 43 | }, 44 | { 45 | files: ['tests/**'], 46 | 47 | plugins: { 48 | vitest, 49 | }, 50 | 51 | rules: { 52 | ...vitest.configs.recommended.rules, 53 | }, 54 | 55 | settings: { 56 | vitest: { 57 | typecheck: true, 58 | }, 59 | }, 60 | 61 | languageOptions: { 62 | globals: { 63 | ...vitest.environments.env.globals, 64 | }, 65 | }, 66 | }, 67 | ); 68 | -------------------------------------------------------------------------------- /src/agent/system-prompt.ts: -------------------------------------------------------------------------------- 1 | export const SystemPrompt = `You are a helpful weather assistant that provides accurate and detailed weather information. 2 | 3 | Your capabilities: 4 | - Get current weather conditions for any location worldwide 5 | - Provide weather forecasts for up to 14 days (3 days with free API key) 6 | - Explain weather patterns and give recommendations 7 | 8 | When providing weather information: 9 | 1. Always specify the location name clearly in the "location" field 10 | 2. Include relevant details like temperature (in both Celsius and Fahrenheit), conditions, humidity, wind, and UV index 11 | 3. For forecasts, highlight key information like temperature ranges and precipitation 12 | 4. Provide helpful context and recommendations based on the weather (e.g., "Bring an umbrella", "Great day for outdoor activities") 13 | 5. Be conversational and friendly in your responses 14 | 15 | Your response MUST include all these fields: 16 | - answer: A complete natural language response with all weather details 17 | - location: The specific location name (e.g., "London, United Kingdom") 18 | - summary: A brief one-line weather summary (e.g., "Partly cloudy, 15°C") 19 | - recommendations: An array of 2-4 helpful suggestions based on the weather 20 | - data_source: What data you used (e.g., "current weather", "3-day forecast") 21 | 22 | If a user's question is unclear, still provide your best answer based on available context, and mention in the answer that clarification would help. 23 | 24 | Available tools: 25 | - get_current_weather: Fetches real-time weather data for a location 26 | - get_weather_forecast: Fetches weather forecast for 1-14 days ahead 27 | 28 | Always use the tools to get accurate, up-to-date weather information. Never make up weather data.`; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weather-agent", 3 | "version": "1.0.0", 4 | "description": "Weather agent for fetching weather forecasts using WeatherAPI.", 5 | "private": true, 6 | "type": "module", 7 | "main": "build/index.js", 8 | "types": "build/index.d.ts", 9 | "scripts": { 10 | "start": "tsx src/index.ts", 11 | "dev": "npx @langchain/langgraph-cli dev", 12 | "clean": "rimraf build", 13 | "prebuild": "yarn lint", 14 | "build": "tsc -p tsconfig.json", 15 | "lint": "eslint src", 16 | "test": "vitest run unit --config tests/vitest.config.ts", 17 | "prettier": "prettier \"src/**/*.{ts,mts}\" --write", 18 | "prettier:check": "prettier \"src/**/*.{ts,mts}\" --check", 19 | "docker:up": "docker compose up -d --build", 20 | "docker:down": "docker compose down", 21 | "docker:logs": "docker compose logs -f", 22 | "docker:full:up": "docker compose -f docker-compose.full.yml up -d", 23 | "docker:full:down": "docker compose -f docker-compose.full.yml down", 24 | "docker:full:logs": "docker compose -f docker-compose.full.yml logs -f" 25 | }, 26 | "dependencies": { 27 | "tslib": "~2.8", 28 | "@langchain/core": "^0.3.78", 29 | "@langchain/langgraph": "^0.4.9", 30 | "@langchain/openai": "^0.6.16", 31 | "@langchain/anthropic": "^0.3.31", 32 | "dotenv": "^16.4.7", 33 | "langchain": "^0.3.36", 34 | "zod": "^4.0.10" 35 | }, 36 | "devDependencies": { 37 | "tsx": "^4.20.6", 38 | "@eslint/js": "~9.17", 39 | "@types/node": "~20", 40 | "@typescript-eslint/parser": "~8.19", 41 | "@vitest/coverage-v8": "~2.1", 42 | "@vitest/eslint-plugin": "~1.1", 43 | "eslint-config-prettier": "~9.1", 44 | "eslint": "~9.17", 45 | "globals": "~15.14", 46 | "prettier": "~3.4", 47 | "rimraf": "~6.0", 48 | "ts-api-utils": "~2.0", 49 | "typescript-eslint": "~8.19", 50 | "typescript": "~5.7", 51 | "vitest": "~2.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/custom-prompt.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { runAgentWithSaveResults } from '../src/index'; 3 | 4 | // Load environment variables 5 | dotenv.config(); 6 | 7 | /** 8 | * Custom prompt example 9 | * This demonstrates using a custom system prompt to change agent behavior 10 | */ 11 | async function main(): Promise { 12 | console.log('🎨 Custom Prompt Example\n'); 13 | 14 | // Custom system prompt that makes the agent more focused on outdoor activities 15 | const customPrompt = `You are a weather assistant specialized in outdoor activity planning. 16 | 17 | Your primary goal is to help users decide what outdoor activities are suitable based on current and forecasted weather conditions. 18 | 19 | When providing weather information: 20 | 1. Always relate the weather to outdoor activities (hiking, cycling, beach activities, sports, etc.) 21 | 2. Provide specific recommendations about which activities are good or bad given the conditions 22 | 3. Warn about any weather hazards for outdoor activities (extreme heat, storms, strong winds, etc.) 23 | 4. Suggest the best times of day for outdoor activities based on the forecast 24 | 5. Be enthusiastic about good weather days and suggest multiple activity options 25 | 26 | Available tools: 27 | - get_current_weather: Fetches real-time weather data 28 | - get_weather_forecast: Fetches weather forecast for up to 14 days 29 | 30 | Always use the tools to get accurate weather information.`; 31 | 32 | const questions = [ 33 | 'What outdoor activities can I do in San Francisco today?', 34 | 'Is it good weather for a picnic in Central Park tomorrow?', 35 | 'Can I go surfing in Hawaii this week?', 36 | 'What is the best day for mountain biking in Colorado over the next 3 days?', 37 | ]; 38 | 39 | try { 40 | await runAgentWithSaveResults(questions, { 41 | modelName: 'gpt-4o-mini', 42 | temperature: 0.5, 43 | systemPrompt: customPrompt, 44 | delayBetweenQuestionsMs: 1000, 45 | }); 46 | } catch (error) { 47 | console.error('Error:', error); 48 | process.exit(1); 49 | } 50 | } 51 | 52 | main(); 53 | -------------------------------------------------------------------------------- /docker-compose.full.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Redis for task orchestration and streaming 3 | redis: 4 | image: redis:7-alpine 5 | container_name: weather-agent-redis 6 | ports: 7 | - "6379:6379" 8 | volumes: 9 | - redis_data:/data 10 | healthcheck: 11 | test: ["CMD", "redis-cli", "ping"] 12 | interval: 5s 13 | timeout: 3s 14 | retries: 5 15 | restart: unless-stopped 16 | 17 | # PostgreSQL for persistent storage 18 | postgres: 19 | image: postgres:15-alpine 20 | container_name: weather-agent-postgres 21 | environment: 22 | POSTGRES_DB: langgraph 23 | POSTGRES_USER: langgraph 24 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-langgraph_password} 25 | ports: 26 | - "5432:5432" 27 | volumes: 28 | - postgres_data:/var/lib/postgresql/data 29 | healthcheck: 30 | test: ["CMD-SHELL", "pg_isready -U langgraph"] 31 | interval: 5s 32 | timeout: 3s 33 | retries: 5 34 | restart: unless-stopped 35 | 36 | # LangGraph API Server 37 | weather-agent: 38 | build: 39 | context: . 40 | dockerfile: Dockerfile 41 | container_name: weather-agent-api 42 | ports: 43 | - "8000:8000" 44 | environment: 45 | # Database connections 46 | REDIS_URI: redis://redis:6379 47 | POSTGRES_URI: postgresql://langgraph:${POSTGRES_PASSWORD:-langgraph_password}@postgres:5432/langgraph 48 | 49 | # API Keys (from .env file) 50 | WEATHER_API_KEY: ${WEATHER_API_KEY} 51 | OPENAI_API_KEY: ${OPENAI_API_KEY} 52 | 53 | # Agent API Key (for authentication) 54 | AGENT_API_KEY: ${AGENT_API_KEY} 55 | 56 | # Optional: Model configuration 57 | MODEL_NAME: ${MODEL_NAME:-gpt-4o-mini} 58 | TEMPERATURE: ${TEMPERATURE:-0} 59 | 60 | # Optional: LangSmith tracing 61 | LANGSMITH_API_KEY: ${LANGSMITH_API_KEY:-} 62 | LANGSMITH_PROJECT: ${LANGSMITH_PROJECT:-weather-agent} 63 | LANGSMITH_TRACING: ${LANGSMITH_TRACING:-false} 64 | 65 | # Server configuration 66 | NODE_ENV: production 67 | depends_on: 68 | redis: 69 | condition: service_healthy 70 | postgres: 71 | condition: service_healthy 72 | restart: unless-stopped 73 | healthcheck: 74 | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8000/health"] 75 | interval: 30s 76 | timeout: 3s 77 | retries: 3 78 | start_period: 40s 79 | 80 | volumes: 81 | redis_data: 82 | driver: local 83 | postgres_data: 84 | driver: local 85 | -------------------------------------------------------------------------------- /src/agent/index.ts: -------------------------------------------------------------------------------- 1 | import { SystemPrompt } from './system-prompt'; 2 | import { ResponseSchema } from './output-structure'; 3 | import { ChatOpenAI } from '@langchain/openai'; 4 | import { createReactAgent } from '@langchain/langgraph/prebuilt'; 5 | import zod from 'zod'; 6 | import { AgentResponse, Logger } from '../common'; 7 | import { createWeatherTools } from '../tools'; 8 | 9 | export async function runWeatherAgent( 10 | questions: string[], 11 | options: { 12 | modelName?: string; 13 | temperature?: number; 14 | systemPrompt?: string; 15 | responseSchema?: zod.Schema; 16 | delayBetweenQuestionsMs?: number; 17 | } = {}, 18 | ): Promise { 19 | const { 20 | modelName = 'gpt-4o-mini', 21 | temperature = 0, 22 | systemPrompt = SystemPrompt, 23 | responseSchema = ResponseSchema, 24 | delayBetweenQuestionsMs = 500, 25 | } = options; 26 | 27 | const logger = new Logger('WeatherAgent'); 28 | logger.info('Starting...'); 29 | 30 | // Get API key from environment 31 | const weatherApiKey = process.env.WEATHER_API_KEY; 32 | if (!weatherApiKey) { 33 | throw new Error( 34 | 'WEATHER_API_KEY environment variable is required. Get your free API key at https://www.weatherapi.com/signup.aspx', 35 | ); 36 | } 37 | 38 | // Create weather tools 39 | const tools = createWeatherTools(weatherApiKey); 40 | 41 | // Create the language model 42 | const model = new ChatOpenAI({ 43 | modelName, 44 | temperature, 45 | }); 46 | 47 | // Create the ReAct agent with tools and response schema 48 | const agent = createReactAgent({ 49 | llm: model, 50 | tools, 51 | responseFormat: responseSchema as any, 52 | }); 53 | 54 | logger.info('Running question processing'); 55 | 56 | const results = []; 57 | for (let i = 0; i < questions.length; i++) { 58 | const question = questions[i]; 59 | logger.info( 60 | `[${i + 1}/${questions.length}] New question to answer: '${question}'`, 61 | ); 62 | try { 63 | const response = await agent.invoke({ 64 | messages: [ 65 | { 66 | role: 'system', 67 | content: systemPrompt, 68 | }, 69 | { role: 'user', content: question }, 70 | ], 71 | }); 72 | results.push({ question, response }); 73 | logger.info( 74 | `[${i + 1}/${questions.length}] Question answered successfully`, 75 | ); 76 | if (i < questions.length - 1 && delayBetweenQuestionsMs > 0) { 77 | logger.info( 78 | `[${i + 1}/${questions.length}] Delaying for ${delayBetweenQuestionsMs}ms`, 79 | ); 80 | await new Promise((resolve) => 81 | setTimeout(resolve, delayBetweenQuestionsMs), 82 | ); 83 | } 84 | } catch (error) { 85 | logger.error('Agent response error:', error.message); 86 | results.push({ question, response: `ERROR: ${error.message}` }); 87 | } 88 | } 89 | 90 | logger.info('Finished Agent'); 91 | return results; 92 | } 93 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is used to test the agent locally through the CLI 3 | */ 4 | 5 | import dotenv from 'dotenv'; 6 | import { runWeatherAgent } from './agent'; 7 | import zod from 'zod'; 8 | 9 | // Load environment variables from .env file 10 | dotenv.config(); 11 | 12 | function formatWeatherResponse(result: any, index: number, total: number): void { 13 | console.log(`\n${'─'.repeat(60)}`); 14 | console.log(`📍 Question ${index + 1}/${total}: ${result.question}`); 15 | console.log(`${'─'.repeat(60)}`); 16 | 17 | if (typeof result.response === 'string' && result.response.startsWith('ERROR:')) { 18 | console.log(`\n❌ ${result.response}\n`); 19 | return; 20 | } 21 | 22 | // LangGraph response structure: check for structured response 23 | let structuredResponse: any; 24 | 25 | // First check if there's a direct structuredResponse field 26 | if (result.response?.structuredResponse) { 27 | structuredResponse = result.response.structuredResponse; 28 | } else if (result.response?.messages) { 29 | // Fall back to checking messages array for the final AI message 30 | const messages = result.response.messages; 31 | const lastMessage = messages[messages.length - 1]; 32 | 33 | // Check for structured output in additional_kwargs or parsed content 34 | if (lastMessage?.additional_kwargs?.parsed) { 35 | structuredResponse = lastMessage.additional_kwargs.parsed; 36 | } else if (lastMessage?.content && typeof lastMessage.content === 'object') { 37 | structuredResponse = lastMessage.content; 38 | } else if (lastMessage?.kwargs?.additional_kwargs?.parsed) { 39 | structuredResponse = lastMessage.kwargs.additional_kwargs.parsed; 40 | } 41 | } 42 | 43 | if (structuredResponse) { 44 | // Display location and summary prominently 45 | console.log(`\n📌 ${structuredResponse.location || 'Unknown Location'}`); 46 | console.log(`☁️ ${structuredResponse.summary || 'N/A'}`); 47 | 48 | // Display the main answer 49 | console.log(`\n${structuredResponse.answer}`); 50 | 51 | // Display recommendations if available 52 | if (structuredResponse.recommendations && structuredResponse.recommendations.length > 0) { 53 | console.log(`\n💡 Recommendations:`); 54 | structuredResponse.recommendations.forEach((rec: string, i: number) => { 55 | console.log(` ${i + 1}. ${rec}`); 56 | }); 57 | } 58 | 59 | // Display data source 60 | if (structuredResponse.data_source) { 61 | console.log(`\n📊 Data source: ${structuredResponse.data_source}`); 62 | } 63 | } else { 64 | console.log('\n⚠️ No structured response available'); 65 | console.log('Raw response structure:', JSON.stringify(result.response, null, 2).substring(0, 500)); 66 | } 67 | } 68 | 69 | export async function runAgentWithSaveResults( 70 | questions: string[], 71 | options: { 72 | modelName?: string; 73 | temperature?: number; 74 | systemPrompt?: string; 75 | responseSchema?: zod.Schema; 76 | delayBetweenQuestionsMs?: number; 77 | } = {}, 78 | ): Promise { 79 | console.log(`\n${'='.repeat(60)}`); 80 | console.log(`🌤️ Running Weather Agent`); 81 | console.log(`${'='.repeat(60)}\n`); 82 | 83 | const startTime = Date.now(); 84 | 85 | try { 86 | const results = await runWeatherAgent(questions, options); 87 | 88 | // Display results 89 | console.log(`\n${'═'.repeat(60)}`); 90 | console.log(`📋 WEATHER RESULTS`); 91 | console.log(`${'═'.repeat(60)}`); 92 | 93 | results.forEach((result, index) => { 94 | formatWeatherResponse(result, index, results.length); 95 | }); 96 | 97 | const duration = Date.now() - startTime; 98 | console.log(`\n${'═'.repeat(60)}`); 99 | console.log(`✅ Agent completed in ${duration}ms`); 100 | console.log(`${'═'.repeat(60)}\n`); 101 | } catch (error) { 102 | const duration = Date.now() - startTime; 103 | console.error(`\n❌ Agent failed after ${duration}ms`); 104 | throw error; 105 | } 106 | } 107 | 108 | async function main(): Promise { 109 | const questions = [ 110 | 'What is the current weather in London?', 111 | 'What will the weather be like in New York over the next 3 days?', 112 | 'Should I bring an umbrella in Paris today?', 113 | // 'What is the temperature in Tokyo right now?', 114 | // 'Give me a 5-day weather forecast for San Francisco', 115 | // 'Is it a good day for outdoor activities in Sydney?', 116 | ]; 117 | 118 | // Get model configuration from environment variables 119 | const modelName = process.env.MODEL_NAME || 'gpt-4o-mini'; 120 | const temperature = process.env.TEMPERATURE 121 | ? parseFloat(process.env.TEMPERATURE) 122 | : 0; 123 | 124 | console.log(`Using model: ${modelName}`); 125 | console.log(`Temperature: ${temperature}\n`); 126 | 127 | try { 128 | await runAgentWithSaveResults(questions, { 129 | modelName, 130 | temperature, 131 | }); 132 | } catch (error) { 133 | console.error('Error running Weather agent:', error); 134 | } 135 | } 136 | 137 | main(); 138 | -------------------------------------------------------------------------------- /SELF-HOSTING.md: -------------------------------------------------------------------------------- 1 | # Self-Hosting Guide 2 | 3 | Deploy the Weather Agent on your own infrastructure using Docker. 4 | 5 | ## Quick Start (No Databases) 6 | 7 | **For stateless weather queries - the simplest deployment!** 8 | 9 | This agent responds to instant requests without needing persistent conversation history. Perfect for quick weather lookups and stateless API responses. 10 | 11 | ### Prerequisites 12 | 13 | - Docker installed ([Get Docker](https://docs.docker.com/get-docker/)) 14 | - Your API keys ready: 15 | - WeatherAPI key from [weatherapi.com](https://www.weatherapi.com/signup.aspx) 16 | - OpenAI API key from [platform.openai.com](https://platform.openai.com/) 17 | 18 | ### Setup 19 | 20 | 1. **Configure environment:** 21 | ```bash 22 | cp .env.example .env 23 | ``` 24 | 25 | Edit `.env` and add your API keys: 26 | ```bash 27 | WEATHER_API_KEY=your_actual_weather_api_key 28 | OPENAI_API_KEY=your_actual_openai_api_key 29 | AGENT_API_KEY=your_secure_agent_api_key 30 | ``` 31 | 32 | **Generate a secure API key:** 33 | ```bash 34 | # On macOS/Linux 35 | openssl rand -base64 32 36 | 37 | # Or use a strong random password 38 | ``` 39 | 40 | 2. **Start the agent:** 41 | ```bash 42 | docker compose up -d 43 | ``` 44 | 45 | 3. **Access Studio UI:** 46 | - Go to [https://smith.langchain.com/studio](https://smith.langchain.com/studio) 47 | - Click "Connect to Server" 48 | - Enter: `http://localhost:8000` 49 | - **Add authentication header:** 50 | - Click on "Headers" or "Advanced" 51 | - Add header: `x-api-key: your_secure_agent_api_key` 52 | - Start asking weather questions! 53 | 54 | ### Managing Your Deployment 55 | 56 | ```bash 57 | # View logs 58 | docker compose logs -f 59 | 60 | # Stop the agent 61 | docker compose stop 62 | 63 | # Remove completely 64 | docker compose down 65 | ``` 66 | 67 | --- 68 | 69 | ## Adding Persistent History 70 | 71 | **Enable conversation history that survives restarts.** 72 | 73 | Use persistent storage if you need multi-turn conversations, conversation history across restarts, or separate conversation threads. 74 | 75 | ### Setup 76 | 77 | 1. **Start full stack (Agent + Redis + PostgreSQL):** 78 | ```bash 79 | docker compose -f docker-compose.full.yml up -d 80 | ``` 81 | 82 | 2. **Verify services are healthy:** 83 | ```bash 84 | docker compose -f docker-compose.full.yml ps 85 | ``` 86 | 87 | 3. **Test persistence:** 88 | - Ask a question in Studio 89 | - Restart: `docker compose -f docker-compose.full.yml restart weather-agent` 90 | - Reconnect - your conversation history is preserved! 91 | 92 | ### Managing the Full Stack 93 | 94 | ```bash 95 | # View logs 96 | docker compose -f docker-compose.full.yml logs -f 97 | 98 | # Stop all services 99 | docker compose -f docker-compose.full.yml down 100 | 101 | # Remove all data (⚠️ deletes conversation history) 102 | docker compose -f docker-compose.full.yml down -v 103 | ``` 104 | 105 | ### Backup 106 | 107 | ```bash 108 | # Backup database 109 | docker compose -f docker-compose.full.yml exec postgres pg_dump \ 110 | -U langgraph langgraph > backup-$(date +%Y%m%d).sql 111 | 112 | # Restore 113 | docker compose -f docker-compose.full.yml exec -T postgres psql \ 114 | -U langgraph langgraph < backup-20250107.sql 115 | ``` 116 | 117 | --- 118 | 119 | ## Security 120 | 121 | ### API Key Authentication 122 | 123 | The agent is protected with API key authentication. All requests must include the `x-api-key` header: 124 | 125 | ```bash 126 | # Test with curl 127 | curl -H "x-api-key: your_secure_agent_api_key" http://localhost:8000/ 128 | 404 Not Found # this is normal! 129 | ``` 130 | 131 | If you do not provide the API key header, you should see a message like: 132 | 133 | ``` 134 | Invalid or missing API key. Please provide a valid 'x-api-key' header. 135 | ``` 136 | 137 | **Security Best Practices:** 138 | - ✅ Generate a strong, random API key (use `openssl rand -base64 32`) 139 | - ✅ Keep your API key secret (never commit `.env` to git) 140 | - ✅ Rotate keys periodically 141 | - ✅ Use HTTPS in production (not covered in this setup) 142 | - ⚠️ If `AGENT_API_KEY` is not set, authentication is disabled (not recommended!) 143 | 144 | --- 145 | 146 | ## Configuration 147 | 148 | All configuration via `.env` file: 149 | 150 | ```bash 151 | # Required 152 | WEATHER_API_KEY=your_weather_api_key_here 153 | OPENAI_API_KEY=your_openai_api_key_here 154 | AGENT_API_KEY=your_secure_agent_api_key # To protect your agent 155 | 156 | # Optional 157 | MODEL_NAME=gpt-4o-mini # AI model (gpt-4o, gpt-3.5-turbo) 158 | TEMPERATURE=0 # Response creativity (0-1) 159 | POSTGRES_PASSWORD=secure_password # Change in production! 160 | ``` 161 | 162 | **Change default port:** 163 | Edit `docker-compose.yml`: 164 | ```yaml 165 | services: 166 | weather-agent: 167 | ports: 168 | - "3000:8000" # Access on port 3000 169 | ``` 170 | 171 | --- 172 | 173 | ## Troubleshooting 174 | 175 | ### Port Already in Use 176 | ```bash 177 | # Find what's using port 8000 178 | lsof -i :8000 179 | 180 | # Change port in docker-compose.yml 181 | ports: 182 | - "8001:8000" 183 | ``` 184 | 185 | ### Cannot Connect to the Agent 186 | 1. Check container is running: `docker compose ps` 187 | 2. Check logs: `docker compose logs weather-agent` 188 | 3. Test connection: `curl -H "x-api-key: your_api_key" http://localhost:8000/` 189 | 4. Verify you're providing the correct `x-api-key` header 190 | 191 | ### Build Failures 192 | ```bash 193 | # Clean build 194 | docker compose build --no-cache 195 | 196 | # Check disk space 197 | df -h 198 | ``` 199 | 200 | ### Database Connection Errors 201 | ```bash 202 | # Check Postgres health 203 | docker compose -f docker-compose.full.yml ps postgres 204 | 205 | # Test connection 206 | docker compose -f docker-compose.full.yml exec postgres \ 207 | psql -U langgraph -c "SELECT 1" 208 | 209 | # View logs 210 | docker compose -f docker-compose.full.yml logs postgres 211 | ``` 212 | 213 | ### Agent Returns Errors 214 | 1. Check API keys: `docker compose exec weather-agent env | grep API_KEY` 215 | 2. Verify keys are valid at provider websites 216 | 3. Check rate limits 217 | 4. Review logs: `docker compose logs -f weather-agent` 218 | 219 | --- 220 | 221 | ## Deployment Comparison 222 | 223 | | Feature | Simple | Full Stack | 224 | |---------|--------|------------| 225 | | **Containers** | 1 (agent) | 3 (agent + Redis + Postgres) | 226 | | **Startup** | ~10 seconds | ~30 seconds | 227 | | **Memory** | ~200MB | ~500MB | 228 | | **Persistent History** | ❌ | ✅ | 229 | | **Best For** | Quick queries | Multi-turn conversations | 230 | 231 | --- 232 | 233 | ## Resources 234 | 235 | - [Main README](README.md) 236 | - [LangGraph Documentation](https://langchain-ai.github.io/langgraph/) 237 | - [Docker Documentation](https://docs.docker.com/) 238 | 239 | **Ready to deploy?** Start with the [Quick Start](#quick-start-no-databases) above! 240 | -------------------------------------------------------------------------------- /src/graph.ts: -------------------------------------------------------------------------------- 1 | import { StateGraph, Annotation, messagesStateReducer, MemorySaver } from '@langchain/langgraph'; 2 | import { BaseMessage, AIMessage, ToolMessage } from '@langchain/core/messages'; 3 | import { ChatOpenAI } from '@langchain/openai'; 4 | import { DynamicStructuredTool } from '@langchain/core/tools'; 5 | import { z } from 'zod'; 6 | 7 | /** 8 | * Step 1: Define the state 9 | * The state keeps track of all messages in the conversation 10 | */ 11 | export const StateAnnotation = Annotation.Root({ 12 | messages: Annotation({ 13 | reducer: messagesStateReducer, 14 | default: () => [], 15 | }), 16 | }); 17 | 18 | /** 19 | * Step 2: Create the weather tools 20 | * These tools fetch real weather data from WeatherAPI 21 | */ 22 | function getWeatherTools(apiKey: string) { 23 | // Tool 1: Get current weather 24 | const currentWeatherTool = new DynamicStructuredTool({ 25 | name: 'get_current_weather', 26 | description: 'Get the current weather for a location', 27 | schema: z.object({ 28 | location: z.string().describe('The city name, e.g. London'), 29 | }), 30 | func: async (input: { location: string }) => { 31 | const { location } = input; 32 | const url = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${encodeURIComponent(location)}`; 33 | const response = await fetch(url); 34 | const data = await response.json(); 35 | 36 | return `Current weather in ${data.location.name}, ${data.location.country}: 37 | Temperature: ${data.current.temp_c}°C (${data.current.temp_f}°F) 38 | Condition: ${data.current.condition.text} 39 | Humidity: ${data.current.humidity}% 40 | Wind: ${data.current.wind_kph} km/h`; 41 | }, 42 | }); 43 | 44 | // Tool 2: Get weather forecast 45 | const forecastTool = new DynamicStructuredTool({ 46 | name: 'get_forecast', 47 | description: 'Get the weather forecast for a location', 48 | schema: z.object({ 49 | location: z.string().describe('The city name, e.g. London'), 50 | days: z.number().optional().describe('Number of days (1-14), defaults to 3'), 51 | }), 52 | func: async (input: { location: string; days?: number }) => { 53 | const { location, days } = input; 54 | const numDays = days || 3; 55 | const url = `https://api.weatherapi.com/v1/forecast.json?key=${apiKey}&q=${encodeURIComponent(location)}&days=${numDays}`; 56 | const response = await fetch(url); 57 | const data = await response.json(); 58 | 59 | let forecast = `Weather forecast for ${data.location.name}, ${data.location.country}:\n\n`; 60 | data.forecast.forecastday.forEach((day: any) => { 61 | forecast += `${day.date}: 62 | High: ${day.day.maxtemp_c}°C, Low: ${day.day.mintemp_c}°C 63 | Condition: ${day.day.condition.text} 64 | Rain chance: ${day.day.daily_chance_of_rain}%\n\n`; 65 | }); 66 | 67 | return forecast; 68 | }, 69 | }); 70 | 71 | return [currentWeatherTool, forecastTool]; 72 | } 73 | 74 | /** 75 | * Step 3: Create the agent node 76 | * This node calls the AI model with access to weather tools 77 | */ 78 | async function callAgent(state: typeof StateAnnotation.State) { 79 | // Get the API key 80 | const apiKey = process.env.WEATHER_API_KEY || ''; 81 | 82 | // Create the tools 83 | const tools = getWeatherTools(apiKey); 84 | 85 | // Create the AI model 86 | const model = new ChatOpenAI({ 87 | modelName: process.env.MODEL_NAME || 'gpt-4o-mini', 88 | temperature: 0, 89 | }); 90 | 91 | // Bind the tools to the model 92 | const modelWithTools = model.bindTools(tools); 93 | 94 | // Build the messages array with a system message 95 | const messages = [ 96 | { 97 | role: 'system', 98 | content: `You are a helpful weather assistant. Use the available tools to get accurate weather information. 99 | 100 | When responding to weather queries: 101 | 1. Use the tools to fetch current weather data 102 | 2. Present the information in a friendly, conversational way 103 | 3. Include helpful recommendations based on the weather (e.g., "Bring an umbrella", "Great day for outdoor activities", "Dress warmly") 104 | 4. Explain what the weather means for the user's activities 105 | 106 | Always provide context and be conversational in your responses.`, 107 | } as any, 108 | ...state.messages, 109 | ]; 110 | 111 | // Call the model with the conversation history 112 | const response = await modelWithTools.invoke(messages); 113 | 114 | // Return the response to add it to the state 115 | return { messages: [response] }; 116 | } 117 | 118 | /** 119 | * Step 4: Create the tools node 120 | * This node executes the tools that the AI requested 121 | */ 122 | async function callTools(state: typeof StateAnnotation.State) { 123 | const apiKey = process.env.WEATHER_API_KEY || ''; 124 | const tools = getWeatherTools(apiKey); 125 | 126 | // Get the last message (which should be from the AI) 127 | const lastMessage = state.messages[state.messages.length - 1] as AIMessage; 128 | 129 | // Get the tool calls from the AI's message 130 | const toolCalls = lastMessage.tool_calls || []; 131 | 132 | // Execute each tool call 133 | const toolMessages: ToolMessage[] = []; 134 | for (const toolCall of toolCalls) { 135 | // Find the tool 136 | const tool = tools.find((t) => t.name === toolCall.name); 137 | 138 | if (tool) { 139 | // Execute the tool 140 | const result = await tool.invoke(toolCall.args); 141 | 142 | // Create a tool message with the result 143 | toolMessages.push( 144 | new ToolMessage({ 145 | content: result, 146 | tool_call_id: toolCall.id, 147 | }), 148 | ); 149 | } 150 | } 151 | 152 | return { messages: toolMessages }; 153 | } 154 | 155 | /** 156 | * Step 5: Create the routing function 157 | * This decides whether to call tools or end the conversation 158 | */ 159 | function shouldContinue(state: typeof StateAnnotation.State) { 160 | const lastMessage = state.messages[state.messages.length - 1]; 161 | 162 | // If the AI wants to use tools, route to the tools node 163 | if (lastMessage._getType() === 'ai') { 164 | const aiMessage = lastMessage as AIMessage; 165 | if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) { 166 | return 'tools'; 167 | } 168 | } 169 | 170 | // Otherwise, end the conversation 171 | return '__end__'; 172 | } 173 | 174 | /** 175 | * Step 6: Build the graph 176 | * This connects all the pieces together 177 | */ 178 | const workflow = new StateGraph(StateAnnotation) 179 | // Add the nodes 180 | .addNode('agent', callAgent) 181 | .addNode('tools', callTools) 182 | 183 | // Define the flow 184 | .addEdge('__start__', 'agent') // Start with the agent 185 | .addConditionalEdges('agent', shouldContinue) // Agent decides what's next 186 | .addEdge('tools', 'agent'); // After tools, go back to agent 187 | 188 | /** 189 | * Step 7: Compile the graph 190 | * This creates the final runnable agent 191 | */ 192 | export const graph = workflow.compile({ 193 | checkpointer: new MemorySaver(), // Saves conversation history 194 | }); 195 | 196 | graph.name = 'Weather Agent'; 197 | -------------------------------------------------------------------------------- /src/tools/weather-api.ts: -------------------------------------------------------------------------------- 1 | import { DynamicStructuredTool } from '@langchain/core/tools'; 2 | import { z } from 'zod'; 3 | import { Logger } from '../common'; 4 | 5 | const logger = new Logger('WeatherAPI'); 6 | 7 | // WeatherAPI base URL 8 | const WEATHER_API_BASE_URL = 'https://api.weatherapi.com/v1'; 9 | 10 | // Types for WeatherAPI responses 11 | interface WeatherCondition { 12 | text: string; 13 | icon: string; 14 | code: number; 15 | } 16 | 17 | interface CurrentWeather { 18 | temp_c: number; 19 | temp_f: number; 20 | condition: WeatherCondition; 21 | wind_mph: number; 22 | wind_kph: number; 23 | wind_dir: string; 24 | pressure_mb: number; 25 | pressure_in: number; 26 | precip_mm: number; 27 | precip_in: number; 28 | humidity: number; 29 | cloud: number; 30 | feelslike_c: number; 31 | feelslike_f: number; 32 | vis_km: number; 33 | vis_miles: number; 34 | uv: number; 35 | gust_mph: number; 36 | gust_kph: number; 37 | } 38 | 39 | interface Location { 40 | name: string; 41 | region: string; 42 | country: string; 43 | lat: number; 44 | lon: number; 45 | tz_id: string; 46 | localtime: string; 47 | } 48 | 49 | interface ForecastDay { 50 | date: string; 51 | day: { 52 | maxtemp_c: number; 53 | maxtemp_f: number; 54 | mintemp_c: number; 55 | mintemp_f: number; 56 | avgtemp_c: number; 57 | avgtemp_f: number; 58 | maxwind_mph: number; 59 | maxwind_kph: number; 60 | totalprecip_mm: number; 61 | totalprecip_in: number; 62 | avgvis_km: number; 63 | avgvis_miles: number; 64 | avghumidity: number; 65 | condition: WeatherCondition; 66 | uv: number; 67 | }; 68 | astro: { 69 | sunrise: string; 70 | sunset: string; 71 | moonrise: string; 72 | moonset: string; 73 | moon_phase: string; 74 | }; 75 | hour: Array<{ 76 | time: string; 77 | temp_c: number; 78 | temp_f: number; 79 | condition: WeatherCondition; 80 | wind_mph: number; 81 | wind_kph: number; 82 | wind_dir: string; 83 | pressure_mb: number; 84 | pressure_in: number; 85 | precip_mm: number; 86 | precip_in: number; 87 | humidity: number; 88 | cloud: number; 89 | feelslike_c: number; 90 | feelslike_f: number; 91 | windchill_c: number; 92 | windchill_f: number; 93 | heatindex_c: number; 94 | heatindex_f: number; 95 | dewpoint_c: number; 96 | dewpoint_f: number; 97 | will_it_rain: number; 98 | chance_of_rain: number; 99 | will_it_snow: number; 100 | chance_of_snow: number; 101 | vis_km: number; 102 | vis_miles: number; 103 | gust_mph: number; 104 | gust_kph: number; 105 | uv: number; 106 | }>; 107 | } 108 | 109 | interface CurrentWeatherResponse { 110 | location: Location; 111 | current: CurrentWeather; 112 | } 113 | 114 | interface ForecastWeatherResponse { 115 | location: Location; 116 | current: CurrentWeather; 117 | forecast: { 118 | forecastday: ForecastDay[]; 119 | }; 120 | } 121 | 122 | /** 123 | * Fetches current weather for a location from WeatherAPI 124 | */ 125 | async function getCurrentWeather( 126 | location: string, 127 | apiKey: string, 128 | ): Promise { 129 | logger.info(`Fetching current weather for: ${location}`); 130 | 131 | const url = `${WEATHER_API_BASE_URL}/current.json?key=${apiKey}&q=${encodeURIComponent(location)}&aqi=no`; 132 | 133 | try { 134 | const response = await fetch(url); 135 | 136 | if (!response.ok) { 137 | const error = await response.json(); 138 | throw new Error( 139 | `WeatherAPI error: ${error.error?.message || response.statusText}`, 140 | ); 141 | } 142 | 143 | const data: CurrentWeatherResponse = await response.json(); 144 | 145 | const result = { 146 | location: { 147 | name: data.location.name, 148 | region: data.location.region, 149 | country: data.location.country, 150 | localtime: data.location.localtime, 151 | }, 152 | current: { 153 | temp_c: data.current.temp_c, 154 | temp_f: data.current.temp_f, 155 | condition: data.current.condition.text, 156 | wind_kph: data.current.wind_kph, 157 | wind_dir: data.current.wind_dir, 158 | humidity: data.current.humidity, 159 | cloud: data.current.cloud, 160 | feelslike_c: data.current.feelslike_c, 161 | feelslike_f: data.current.feelslike_f, 162 | uv: data.current.uv, 163 | }, 164 | }; 165 | 166 | return JSON.stringify(result, null, 2); 167 | } catch (error) { 168 | logger.error('Error fetching current weather:', error); 169 | throw error; 170 | } 171 | } 172 | 173 | /** 174 | * Fetches weather forecast for a location from WeatherAPI 175 | */ 176 | async function getForecastWeather( 177 | location: string, 178 | days: number, 179 | apiKey: string, 180 | ): Promise { 181 | logger.info(`Fetching ${days}-day forecast for: ${location}`); 182 | 183 | // WeatherAPI allows up to 14 days forecast 184 | const forecastDays = Math.min(Math.max(days, 1), 14); 185 | 186 | const url = `${WEATHER_API_BASE_URL}/forecast.json?key=${apiKey}&q=${encodeURIComponent(location)}&days=${forecastDays}&aqi=no&alerts=no`; 187 | 188 | try { 189 | const response = await fetch(url); 190 | 191 | if (!response.ok) { 192 | const error = await response.json(); 193 | throw new Error( 194 | `WeatherAPI error: ${error.error?.message || response.statusText}`, 195 | ); 196 | } 197 | 198 | const data: ForecastWeatherResponse = await response.json(); 199 | 200 | const result = { 201 | location: { 202 | name: data.location.name, 203 | region: data.location.region, 204 | country: data.location.country, 205 | localtime: data.location.localtime, 206 | }, 207 | current: { 208 | temp_c: data.current.temp_c, 209 | temp_f: data.current.temp_f, 210 | condition: data.current.condition.text, 211 | humidity: data.current.humidity, 212 | }, 213 | forecast: data.forecast.forecastday.map((day) => ({ 214 | date: day.date, 215 | maxtemp_c: day.day.maxtemp_c, 216 | mintemp_c: day.day.mintemp_c, 217 | avgtemp_c: day.day.avgtemp_c, 218 | maxtemp_f: day.day.maxtemp_f, 219 | mintemp_f: day.day.mintemp_f, 220 | avgtemp_f: day.day.avgtemp_f, 221 | condition: day.day.condition.text, 222 | maxwind_kph: day.day.maxwind_kph, 223 | totalprecip_mm: day.day.totalprecip_mm, 224 | avghumidity: day.day.avghumidity, 225 | uv: day.day.uv, 226 | sunrise: day.astro.sunrise, 227 | sunset: day.astro.sunset, 228 | })), 229 | }; 230 | 231 | logger.info( 232 | `Successfully fetched ${forecastDays}-day forecast for ${data.location.name}`, 233 | ); 234 | return JSON.stringify(result, null, 2); 235 | } catch (error) { 236 | logger.error('Error fetching weather forecast:', error); 237 | throw error; 238 | } 239 | } 240 | 241 | /** 242 | * Creates LangChain tools for WeatherAPI 243 | */ 244 | export function createWeatherTools(apiKey: string): DynamicStructuredTool[] { 245 | const currentWeatherTool = new DynamicStructuredTool({ 246 | name: 'get_current_weather', 247 | description: 248 | 'Get current weather conditions for a specific location. Use this tool to fetch real-time weather information including temperature, humidity, wind, and conditions. Location can be city name, US zipcode, UK postcode, Canada postal code, IP address, or lat/lon coordinates.', 249 | schema: z.object({ 250 | location: z 251 | .string() 252 | .describe( 253 | 'Location to get weather for (e.g., "London", "New York", "48.8567,2.3508")', 254 | ), 255 | }), 256 | func: async ({ location }): Promise => { 257 | return await getCurrentWeather(location, apiKey); 258 | }, 259 | }); 260 | 261 | const forecastWeatherTool = new DynamicStructuredTool({ 262 | name: 'get_weather_forecast', 263 | description: 264 | 'Get weather forecast for a specific location for up to 14 days. Use this tool to fetch future weather predictions including daily min/max temperatures, conditions, precipitation, and astronomical data. Location can be city name, US zipcode, UK postcode, Canada postal code, IP address, or lat/lon coordinates.', 265 | schema: z.object({ 266 | location: z 267 | .string() 268 | .describe( 269 | 'Location to get forecast for (e.g., "London", "New York", "48.8567,2.3508")', 270 | ), 271 | days: z 272 | .number() 273 | .min(1) 274 | .max(14) 275 | .default(3) 276 | .describe( 277 | 'Number of days to forecast (1-14, default: 3). Free API key supports up to 3 days.', 278 | ), 279 | }), 280 | func: async ({ location, days }): Promise => { 281 | return await getForecastWeather(location, days, apiKey); 282 | }, 283 | }); 284 | 285 | return [currentWeatherTool, forecastWeatherTool]; 286 | } 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Weather Agent: Your First LangGraph Agent 2 | 3 | A beginner-friendly agent that shows you how to build AI agents using **LangGraph** and **TypeScript**. This agent fetches real-time weather data and provides helpful recommendations. 4 | 5 | ## What Does This Agent Do? 6 | 7 | Ask questions like: 8 | - "What's the weather in London?" 9 | - "Give me a 5-day forecast for New York" 10 | - "Should I bring an umbrella in Paris?" 11 | 12 | The agent will: 13 | 1. ✅ Understand your question 14 | 2. ✅ Fetch real weather data from WeatherAPI 15 | 3. ✅ Give you friendly answers with recommendations 16 | 17 | ## Quick Start (2 minutes) 18 | 19 | ### Step 1: Get Your API Keys 20 | 21 | You need two free API keys: 22 | 23 | **1. WeatherAPI** (for weather data) 24 | - Sign up: https://www.weatherapi.com/signup.aspx 25 | - Copy your API key from the dashboard 26 | 27 | **2. OpenAI** (for the AI brain) 28 | - Sign up: https://platform.openai.com/ 29 | - Create an API key in your account settings 30 | 31 | ### Step 2: Install and Configure 32 | 33 | ```bash 34 | # Navigate to the weather-agent directory 35 | cd agents/weather-agent 36 | 37 | # Install dependencies 38 | yarn install 39 | 40 | # Create your environment file 41 | cp .env.example .env 42 | ``` 43 | 44 | Now edit the `.env` file and add your API keys. The file looks like this: 45 | 46 | ```bash 47 | # ============================================ 48 | # REQUIRED: API Keys 49 | # ============================================ 50 | 51 | # WeatherAPI Key 52 | WEATHER_API_KEY=your_weather_api_key_here 53 | 54 | # OpenAI API Key 55 | OPENAI_API_KEY=your_openai_api_key_here 56 | 57 | # ============================================ 58 | # OPTIONAL: Model Configuration (already set to good defaults) 59 | # ============================================ 60 | 61 | MODEL_NAME=gpt-4o-mini 62 | TEMPERATURE=0 63 | ``` 64 | 65 | **Just replace the two API keys** with your actual keys. The other settings have good defaults already! 66 | 67 | ### Step 3: Run the Agent 68 | 69 | ```bash 70 | # Run the agent in your terminal 71 | yarn start 72 | ``` 73 | 74 | You'll see the agent answer weather questions! 75 | 76 | ## Self-Hosting with Docker 77 | 78 | Want to deploy this agent on your own server? Check out the **[Self-Hosting Guide](SELF-HOSTING.md)** for: 79 | 80 | - 🐳 **Quick Start** - Deploy with Docker in 3 commands (no databases needed!) 81 | - 💾 **Persistent History** - Add Redis + PostgreSQL for conversation storage 82 | - 🔒 **Production Setup** - Security, scaling, and monitoring best practices 83 | 84 | **Quick deploy:** 85 | ```bash 86 | cp .env.example .env # Add your API keys 87 | docker compose up -d # Start the agent 88 | # Open Studio: https://smith.langchain.com/studio → Connect to http://localhost:8000 89 | ``` 90 | 91 | See **[SELF-HOSTING.md](SELF-HOSTING.md)** for complete documentation. 92 | 93 | ## How to Use the Interactive UI 94 | 95 | Want to chat with your agent in a nice web interface? Use **LangSmith Studio**: 96 | 97 | ```bash 98 | # Start the development server 99 | yarn dev 100 | ``` 101 | 102 | Then open your browser to: 103 | - **LangSmith Studio**: https://smith.langchain.com/studio?baseUrl=http://localhost:2024 104 | 105 | In the Studio, you can: 106 | - Chat with the agent in real-time 107 | - See how it thinks and makes decisions 108 | - Watch it call the weather API 109 | - Debug any issues 110 | 111 | ## Understanding the Code 112 | 113 | ### The Main File: `src/graph.ts` 114 | 115 | This file contains the entire agent in **7 simple steps**: 116 | 117 | ```typescript 118 | // Step 1: Define the state (conversation memory) 119 | // Step 2: Create weather tools (API connections) 120 | // Step 3: Create the agent node (AI brain) 121 | // Step 4: Create the tools node (executes API calls) 122 | // Step 5: Create routing (decides what to do next) 123 | // Step 6: Build the graph (connect everything) 124 | // Step 7: Compile (make it ready to run) 125 | ``` 126 | 127 | #### What is a "Graph"? 128 | 129 | Think of the agent as a flowchart: 130 | 1. User asks a question → **Agent** thinks about it 131 | 2. Agent needs weather data? → Call **Tools** to get it 132 | 3. Got the data? → Go back to **Agent** to answer 133 | 4. Answer ready? → **End** 134 | 135 | This flowchart is called a "graph" in LangGraph! 136 | 137 | ### The Weather Tools 138 | 139 | The agent has two tools: 140 | 141 | **Tool 1: `get_current_weather`** 142 | - Gets current weather for a location 143 | - Returns: temperature, conditions, humidity, wind 144 | 145 | **Tool 2: `get_forecast`** 146 | - Gets weather forecast (1-14 days) 147 | - Returns: daily high/low temps, rain chance, conditions 148 | 149 | ### How the Agent Thinks 150 | 151 | 1. **User**: "What's the weather in Paris?" 152 | 2. **Agent** (thinking): "I need to call get_current_weather for Paris" 153 | 3. **Tools Node**: *Calls WeatherAPI* 154 | 4. **Agent**: "Got the data! Let me write a nice response with recommendations" 155 | 5. **User**: Gets friendly answer like: 156 | > "It's 15°C and sunny in Paris! Perfect weather for sightseeing. Don't forget your sunglasses!" 157 | 158 | ## Project Structure 159 | 160 | ``` 161 | weather-agent/ 162 | ├── src/ 163 | │ └── graph.ts # Main agent code (the whole agent in one file!) 164 | ├── .env # Your API keys (you create this) 165 | ├── .env.example # Template for .env 166 | ├── package.json # Dependencies 167 | └── README.md # This file 168 | ``` 169 | 170 | Simple, right? One main file to understand! 171 | 172 | ## Configuration Options 173 | 174 | Edit your `.env` file to change settings: 175 | 176 | ```bash 177 | # Required 178 | WEATHER_API_KEY=your_key 179 | OPENAI_API_KEY=your_key 180 | 181 | # Optional: Choose different AI models 182 | MODEL_NAME=gpt-4o-mini # Fast and cheap (default) 183 | # MODEL_NAME=gpt-4o # Smarter but costs more 184 | # MODEL_NAME=gpt-3.5-turbo # Cheaper alternative 185 | 186 | # Optional: Creativity level (0 = consistent, 1 = creative) 187 | TEMPERATURE=0 188 | 189 | # Optional: For LangSmith tracing 190 | LANGSMITH_API_KEY=your_key 191 | LANGSMITH_PROJECT=weather-agent 192 | LANGSMITH_TRACING=true 193 | ``` 194 | 195 | ## Common Questions 196 | 197 | ### Can I use a different AI model? 198 | 199 | Yes! Just change `MODEL_NAME` in your `.env` file: 200 | - `gpt-4o-mini` - Default, fast, cheap ($0.15 per 1M tokens) 201 | - `gpt-4o` - Smarter ($2.50 per 1M tokens) 202 | - `gpt-3.5-turbo` - Budget option ($0.50 per 1M tokens) 203 | 204 | ### How much does it cost to run? 205 | 206 | With the free tiers: 207 | - **WeatherAPI**: 1 million calls/month for free 208 | - **OpenAI**: You pay per request (around $0.0001 per weather question with gpt-4o-mini) 209 | 210 | A typical weather question costs less than **1 cent**! 211 | 212 | ### Can I customize the responses? 213 | 214 | Yes! Open [src/graph.ts](src/graph.ts:95-103) and edit the system prompt to change how the agent talks. 215 | 216 | ### Where does the weather data come from? 217 | 218 | [WeatherAPI.com](https://www.weatherapi.com/) - a free weather data service. The free tier gives you: 219 | - Current weather for any location 220 | - 3-day forecasts 221 | - 1 million API calls per month 222 | 223 | ### What locations can I ask about? 224 | 225 | Almost anything: 226 | - City names: "London", "New York", "Tokyo" 227 | - Zip codes: "10001", "SW1" 228 | - Coordinates: "48.8567,2.3508" 229 | - Even: "auto:ip" (your current location) 230 | 231 | ## Troubleshooting 232 | 233 | **"WEATHER_API_KEY is required" error** 234 | - Make sure your `.env` file exists in the `weather-agent` directory 235 | - Check that you copied the API key correctly (no spaces or quotes) 236 | 237 | **Agent gives weird responses** 238 | - Try lowering the `TEMPERATURE` in `.env` (set it to 0) 239 | - Make sure you're using a model like `gpt-4o-mini` that is good with tool calling 240 | 241 | **"Cannot find module" errors** 242 | - Run `yarn install` again 243 | - Make sure you're in the `weather-agent` directory 244 | 245 | ## Next Steps 246 | 247 | ### Learn More About LangGraph 248 | 249 | - **Official Docs**: https://langchain-ai.github.io/langgraph/ 250 | - **Tutorials**: https://docs.langchain.com/oss/javascript/langgraph/quickstart 251 | - **Thinking in LangGraph**: https://docs.langchain.com/oss/javascript/langgraph/thinking-in-langgraph 252 | 253 | ### Customize Your Agent 254 | 255 | 1. **Add More Tools**: Create tools for other APIs (news, local events, etc.) 256 | 2. **Change Personality**: Edit the system prompt to make it funny, serious, or professional 257 | 3. **Add Memory**: Make it remember previous conversations 258 | 4. **Deploy It**: Put it online so anyone can use it 259 | 260 | ### Build Your Own Agent 261 | 262 | Use this weather agent as a template! The pattern is: 263 | 1. Define your state (what to remember) 264 | 2. Create tools (what the agent can do) 265 | 3. Build the graph (how it flows) 266 | 4. Compile and run! 267 | 268 | ## Help and Support 269 | 270 | - **Questions?** Open an issue on GitHub 271 | - **Found a bug?** Submit a pull request 272 | - **Want to learn more?** Check the LangGraph documentation 273 | 274 | ## Contributing 275 | 276 | This is a learning project! Contributions that make it easier to understand are especially welcome: 277 | - Better comments 278 | - More examples 279 | - Clearer explanations 280 | - Bug fixes 281 | 282 | ## License 283 | 284 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 285 | 286 | --- 287 | 288 | **Happy learning! 🚀** 289 | 290 | Built with ❤️ using [LangGraph](https://github.com/langchain-ai/langgraph) and [WeatherAPI](https://www.weatherapi.com/) 291 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Weather Agent Examples 2 | 3 | This document provides detailed examples of how to use the Weather Agent with various scenarios and configurations. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Basic Usage](#basic-usage) 8 | 2. [Weather Forecasts](#weather-forecasts) 9 | 3. [Travel Planning](#travel-planning) 10 | 4. [Custom Prompts](#custom-prompts) 11 | 5. [Direct API Usage](#direct-api-usage) 12 | 13 | --- 14 | 15 | ## Basic Usage 16 | 17 | The simplest way to use the Weather Agent is to ask for current weather conditions. 18 | 19 | ### Running the Example 20 | 21 | ```bash 22 | cd agents/weather-agent 23 | tsx examples/basic-usage.ts 24 | ``` 25 | 26 | ### Example Questions 27 | 28 | ```typescript 29 | const questions = [ 30 | 'What is the current weather in London?', 31 | 'What is the temperature in Tokyo?', 32 | 'Is it raining in Seattle?', 33 | 'What is the weather like in Sydney?', 34 | ]; 35 | ``` 36 | 37 | ### Expected Output 38 | 39 | The agent will provide structured responses with: 40 | - Current temperature (°C and °F) 41 | - Weather conditions (sunny, cloudy, rainy, etc.) 42 | - Humidity and wind information 43 | - UV index 44 | - Helpful recommendations 45 | 46 | **Example Response:** 47 | 48 | ```json 49 | { 50 | "answer": "The current weather in London is partly cloudy with a temperature of 15°C (59°F). The humidity is at 72% with light winds from the southwest at 15 km/h. The UV index is moderate at 4. It's a pleasant day - perfect for a walk in the park, though you might want to bring a light jacket.", 51 | "location": "London, United Kingdom", 52 | "summary": "Partly cloudy, 15°C", 53 | "recommendations": [ 54 | "Light jacket recommended", 55 | "Good day for outdoor activities", 56 | "Moderate UV - consider sunscreen" 57 | ], 58 | "data_source": "current weather" 59 | } 60 | ``` 61 | 62 | --- 63 | 64 | ## Weather Forecasts 65 | 66 | Get multi-day weather forecasts to plan ahead. 67 | 68 | ### Running the Example 69 | 70 | ```bash 71 | tsx examples/forecast-example.ts 72 | ``` 73 | 74 | ### Example Questions 75 | 76 | ```typescript 77 | const questions = [ 78 | 'Give me a 3-day weather forecast for Paris', 79 | 'What will the weather be like in New York over the next 5 days?', 80 | 'Will it rain in Los Angeles this week?', 81 | 'What is the weather forecast for Berlin for the next 3 days?', 82 | ]; 83 | ``` 84 | 85 | ### Expected Output 86 | 87 | The agent provides detailed forecasts including: 88 | - Daily temperature ranges (min/max) 89 | - Expected conditions for each day 90 | - Precipitation chances 91 | - Sunrise/sunset times 92 | - Day-by-day recommendations 93 | 94 | **Example Response:** 95 | 96 | ```json 97 | { 98 | "answer": "Here's the 3-day forecast for Paris:\n\nDay 1 (2025-11-08): Partly cloudy with temperatures ranging from 12°C to 18°C. Low chance of rain (10%). Sunrise at 7:42 AM, sunset at 5:23 PM.\n\nDay 2 (2025-11-09): Mostly sunny, 10°C to 16°C. Perfect day for sightseeing!\n\nDay 3 (2025-11-10): Cloudy with possible light showers, 11°C to 15°C. 60% chance of rain. Bring an umbrella.", 99 | "location": "Paris, France", 100 | "summary": "Mixed conditions over 3 days with possible rain on day 3", 101 | "recommendations": [ 102 | "Pack layers for varying temperatures", 103 | "Umbrella needed for day 3", 104 | "Day 2 is best for outdoor activities" 105 | ], 106 | "data_source": "3-day forecast" 107 | } 108 | ``` 109 | 110 | ### Notes 111 | 112 | - Free API tier supports up to 3-day forecasts 113 | - Paid plans support up to 14-day forecasts 114 | - Forecasts include hourly data (not shown in structured output but used by agent) 115 | 116 | --- 117 | 118 | ## Travel Planning 119 | 120 | Use the Weather Agent to help plan trips and decide what to pack. 121 | 122 | ### Running the Example 123 | 124 | ```bash 125 | tsx examples/travel-planning.ts 126 | ``` 127 | 128 | ### Example Questions 129 | 130 | ```typescript 131 | const questions = [ 132 | 'I am planning a trip to Barcelona next week. What should I pack based on the weather?', 133 | 'Is it a good time to visit Iceland? What is the weather like there?', 134 | 'Should I bring an umbrella for my trip to London tomorrow?', 135 | 'What will the weather be like in Dubai over the next 3 days? Is it too hot?', 136 | 'I am going hiking in Denver this weekend. What is the weather forecast?', 137 | ]; 138 | ``` 139 | 140 | ### Expected Output 141 | 142 | The agent provides travel-specific advice: 143 | - What to pack (clothing, accessories) 144 | - Activity recommendations 145 | - Safety considerations 146 | - Best times to visit attractions 147 | 148 | **Example Response:** 149 | 150 | ```json 151 | { 152 | "answer": "For your trip to Barcelona next week, here's what you should pack based on the forecast:\n\nTemperatures will range from 16°C to 22°C (61-72°F) with mostly sunny conditions. Pack:\n- Light layers (t-shirts, light sweaters)\n- One light jacket for evenings\n- Sunglasses and sunscreen (UV index 6-7)\n- Comfortable walking shoes\n- Light rain jacket just in case (20% chance of showers on one day)\n\nIt's excellent weather for sightseeing, beach visits, and outdoor dining!", 153 | "location": "Barcelona, Spain", 154 | "summary": "Warm and sunny, ideal travel weather", 155 | "recommendations": [ 156 | "Pack light layers", 157 | "Bring sunscreen (high UV)", 158 | "Perfect for beach and outdoor activities", 159 | "Evening temperatures mild, light jacket sufficient" 160 | ], 161 | "data_source": "7-day forecast" 162 | } 163 | ``` 164 | 165 | --- 166 | 167 | ## Custom Prompts 168 | 169 | Customize the agent's behavior with custom system prompts. 170 | 171 | ### Running the Example 172 | 173 | ```bash 174 | tsx examples/custom-prompt.ts 175 | ``` 176 | 177 | ### Custom Prompt for Outdoor Activities 178 | 179 | This example uses a specialized prompt that focuses on outdoor activity recommendations: 180 | 181 | ```typescript 182 | const customPrompt = `You are a weather assistant specialized in outdoor activity planning. 183 | 184 | Your primary goal is to help users decide what outdoor activities are suitable based on current and forecasted weather conditions. 185 | 186 | When providing weather information: 187 | 1. Always relate the weather to outdoor activities 188 | 2. Provide specific recommendations about which activities are suitable 189 | 3. Warn about any weather hazards for outdoor activities 190 | 4. Suggest the best times of day for activities 191 | 5. Be enthusiastic about good weather days 192 | 193 | Available tools: 194 | - get_current_weather: Fetches real-time weather data 195 | - get_weather_forecast: Fetches weather forecast for up to 14 days`; 196 | ``` 197 | 198 | ### Example Questions 199 | 200 | ```typescript 201 | const questions = [ 202 | 'What outdoor activities can I do in San Francisco today?', 203 | 'Is it good weather for a picnic in Central Park tomorrow?', 204 | 'Can I go surfing in Hawaii this week?', 205 | 'What is the best day for mountain biking in Colorado over the next 3 days?', 206 | ]; 207 | ``` 208 | 209 | ### Expected Output 210 | 211 | With the custom prompt, responses are tailored to outdoor activities: 212 | 213 | ```json 214 | { 215 | "answer": "San Francisco has fantastic weather for outdoor activities today! At 20°C (68°F) with partly cloudy skies and light winds at 12 km/h, here's what you can do:\n\n🚴‍♂️ GREAT for: Cycling across the Golden Gate Bridge, hiking in the Presidio, beach walks at Ocean Beach\n\n⚠️ GOOD for: Kayaking in the Bay (winds are light), outdoor photography, picnics in Golden Gate Park\n\n🌊 MAYBE: Swimming (water will be cold despite nice air temp)\n\nBest time: 11 AM - 4 PM when it's warmest. Morning fog should clear by 10 AM. UV index is 5, so wear sunscreen!", 216 | "location": "San Francisco, California", 217 | "summary": "Perfect conditions for most outdoor activities", 218 | "recommendations": [ 219 | "Best window: 11 AM - 4 PM", 220 | "Bring sunscreen (UV 5)", 221 | "Light layers for fog/sun transition", 222 | "Great visibility for photography" 223 | ], 224 | "data_source": "current weather" 225 | } 226 | ``` 227 | 228 | --- 229 | 230 | ## Direct API Usage 231 | 232 | For programmatic use, you can call the agent functions directly: 233 | 234 | ```typescript 235 | import { runWeatherAgent } from './src/agent'; 236 | import { createWeatherTools } from './src/tools'; 237 | 238 | // Using the main agent function 239 | const results = await runWeatherAgent( 240 | ['What is the weather in Berlin?'], 241 | { 242 | modelName: 'gpt-4o-mini', 243 | temperature: 0, 244 | } 245 | ); 246 | 247 | // Using tools directly (without LLM) 248 | const tools = createWeatherTools(process.env.WEATHER_API_KEY); 249 | const currentWeatherTool = tools[0]; 250 | const result = await currentWeatherTool.invoke({ 251 | location: 'Berlin' 252 | }); 253 | console.log(result); 254 | ``` 255 | 256 | ### Output Format 257 | 258 | Each result includes: 259 | - `question`: The original question asked 260 | - `response`: Object containing: 261 | - `messages`: Full conversation history 262 | - `structuredResponse`: Structured output matching ResponseSchema 263 | 264 | --- 265 | 266 | ## Configuration Options 267 | 268 | All examples support these configuration options: 269 | 270 | ```typescript 271 | await runAgentWithSaveResults(questions, { 272 | // LLM model to use 273 | modelName: 'gpt-4o-mini', // or 'gpt-4', 'gpt-3.5-turbo' 274 | 275 | // Temperature for response generation (0-1) 276 | temperature: 0, // 0 = deterministic, 1 = creative 277 | 278 | // Custom system prompt 279 | systemPrompt: 'Your custom prompt here', 280 | 281 | // Custom response schema (Zod schema) 282 | responseSchema: CustomSchema, 283 | 284 | // Delay between questions (milliseconds) 285 | delayBetweenQuestionsMs: 1000, 286 | }); 287 | ``` 288 | 289 | --- 290 | 291 | ## Tips and Best Practices 292 | 293 | ### Location Formats 294 | 295 | WeatherAPI accepts multiple location formats: 296 | - **City name**: `"London"`, `"New York"` 297 | - **City + Country**: `"Paris, France"` 298 | - **US Zipcode**: `"10001"` 299 | - **UK Postcode**: `"SW1"` 300 | - **Coordinates**: `"48.8567,2.3508"` (latitude,longitude) 301 | - **IP address**: `"auto:ip"` (auto-detect from IP) 302 | 303 | ### Rate Limiting 304 | 305 | To avoid hitting API rate limits: 306 | ```typescript 307 | delayBetweenQuestionsMs: 1000 // Add 1 second delay 308 | ``` 309 | 310 | ### Error Handling 311 | 312 | The agent gracefully handles errors: 313 | - Invalid locations return helpful error messages 314 | - API failures are logged and returned in results 315 | - Network issues are caught and reported 316 | 317 | ### Output Files 318 | 319 | Results are saved to `outputs/` directory: 320 | ``` 321 | outputs/ 322 | └── 1699372800000-gpt-4o-mini.json 323 | ``` 324 | 325 | Filename format: `{timestamp}-{model}.json` 326 | 327 | --- 328 | 329 | ## Advanced Usage 330 | 331 | ### Comparing Weather Across Cities 332 | 333 | ```typescript 334 | const questions = [ 335 | 'Compare the weather in London and Paris today', 336 | 'Which city has better weather: Tokyo or Seoul?', 337 | 'Is it warmer in Miami or Los Angeles right now?', 338 | ]; 339 | ``` 340 | 341 | ### Weather-Based Decisions 342 | 343 | ```typescript 344 | const questions = [ 345 | 'Should I schedule an outdoor wedding in Rome on June 15th?', 346 | 'Is tomorrow a good day for painting the exterior of my house in Seattle?', 347 | 'Can I safely fly a drone in Chicago today?', 348 | ]; 349 | ``` 350 | 351 | ### Historical Context (requires forecast data) 352 | 353 | ```typescript 354 | const questions = [ 355 | 'How does today\'s weather in Boston compare to the forecast?', 356 | 'Is the weather in Singapore typical for this time of year?', 357 | ]; 358 | ``` 359 | 360 | --- 361 | 362 | ## Troubleshooting Examples 363 | 364 | If you encounter issues, try these debug examples: 365 | 366 | ### Test API Key 367 | 368 | ```typescript 369 | import { createWeatherTools } from './src/tools'; 370 | 371 | const tools = createWeatherTools(process.env.WEATHER_API_KEY); 372 | const result = await tools[0].invoke({ location: 'London' }); 373 | console.log('API test result:', result); 374 | ``` 375 | 376 | ### Verbose Logging 377 | 378 | The Logger class outputs helpful information. Check console for: 379 | ``` 380 | [WeatherAgent] INFO: Starting... 381 | [WeatherAPI] INFO: Fetching current weather for: London 382 | [WeatherAgent] INFO: [1/1] New question to answer: 'What is the weather in London?' 383 | ``` 384 | 385 | --- 386 | 387 | ## Next Steps 388 | 389 | - Modify the examples to test different locations 390 | - Create custom system prompts for specialized use cases 391 | - Integrate the agent into your applications 392 | - Extend with additional weather data sources 393 | 394 | For more information, see the main [README.md](README.md). 395 | --------------------------------------------------------------------------------