Building a Fishing Report Agent with AWS Strands
Create an AI agent that combines tide, weather, and marine data to generate fishing reports. Learn tool-calling patterns with the Strands SDK, NOAA APIs, and Claude on AWS Bedrock.
Why an Agent?
You could write a Python script that calls a few APIs and formats the output. It would work fine. So why build an agent?
The difference is reasoning. A script follows the same logic every time. An agent can:
- Decide which tools to call based on the question
- Combine information in ways you didn’t explicitly program
- Handle follow-up questions with context
- Explain its reasoning
Ask a script “Should I fish the flats or the reef tomorrow?” and it breaks. Ask an agent, and it checks the tides, wind, water temp, and gives you a reasoned answer.
In this tutorial, we’ll build a fishing report agent using AWS Strands, the same framework that powers Amazon Q Developer and AWS Glue’s AI features. We’ll make it dynamic—accepting any coastal US location, understanding natural language dates, and caching intelligently.
What We’re Building
Our agent accepts natural queries like:
"What are fishing conditions in Marathon tomorrow?"
"Should I fish Key West this weekend?"
"Conditions for 29.5, -81.2 on Saturday"
It dynamically:
- Resolves any location → finds the nearest NOAA tide station
- Parses natural dates → “tomorrow”, “this weekend”, “next Friday”
- Caches smartly → tide data daily, weather hourly, marine data every 15 minutes
| Tool | Data Source | What It Provides |
|---|---|---|
resolve_location | Nominatim + NOAA | Geocode location, find nearest tide station |
get_tides | NOAA CO-OPS API | High/low tide times |
get_marine_conditions | Open-Meteo API | Wave height, swell, water temp |
get_weather | NWS API | Air temp, wind, precipitation |
get_moon_phase | Calculation | Moon phase and fishing implications |
All free APIs. No subscriptions required.
Project Setup
mkdir fishing-agent && cd fishing-agent
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install strands-agents strands-agents-tools requests geopy python-dateutil
Create the file structure:
fishing-agent/
├── agent.py # Main agent code
├── tools/
│ ├── __init__.py
│ ├── location.py # Geocoding + station discovery
│ ├── tides.py # NOAA tide data
│ ├── marine.py # Open-Meteo conditions
│ ├── weather.py # NWS forecast
│ └── moon.py # Moon phase calculation
├── utils/
│ ├── __init__.py
│ ├── dates.py # Natural language date parsing
│ └── cache.py # Smart caching layer
└── requirements.txt
AWS Bedrock Setup
Configure your AWS credentials. If you’re using an IAM role (EC2, Lambda), this is automatic.
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
export AWS_DEFAULT_REGION=us-west-2
Make sure Claude is enabled in your Bedrock console (Model access → Claude → Request access).
Building the Utilities
Before the tools, we need two utilities: date parsing and caching.
Natural Language Date Parsing
This lets users say “tomorrow” or “this weekend” instead of “2025-12-22”.
# utils/dates.py
from datetime import datetime, timedelta
from dateutil import parser as dateutil_parser
from dateutil.relativedelta import relativedelta, SA, SU
import re
def parse_date(date_input: str = None) -> str:
"""
Parse natural language dates into YYYY-MM-DD format.
Supports:
- "today", "tomorrow", "yesterday"
- "this weekend", "next weekend"
- "Saturday", "next Friday"
- "in 3 days", "next week"
- "Dec 25", "December 25, 2025"
- "2025-12-25" (passthrough)
Returns:
Date string in YYYY-MM-DD format
"""
if date_input is None:
return datetime.now().strftime("%Y-%m-%d")
date_input = date_input.lower().strip()
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
# Handle common phrases
if date_input in ["today", "now"]:
return today.strftime("%Y-%m-%d")
if date_input == "tomorrow":
return (today + timedelta(days=1)).strftime("%Y-%m-%d")
if date_input == "yesterday":
return (today - timedelta(days=1)).strftime("%Y-%m-%d")
# Handle "this weekend" - returns Saturday
if date_input == "this weekend":
days_until_saturday = (5 - today.weekday()) % 7
if days_until_saturday == 0 and today.weekday() == 5:
return today.strftime("%Y-%m-%d") # It's Saturday
saturday = today + timedelta(days=days_until_saturday)
return saturday.strftime("%Y-%m-%d")
# Handle "next weekend"
if date_input == "next weekend":
days_until_saturday = (5 - today.weekday()) % 7
if days_until_saturday == 0:
days_until_saturday = 7
saturday = today + timedelta(days=days_until_saturday + 7)
return saturday.strftime("%Y-%m-%d")
# Handle "in X days"
in_days_match = re.match(r"in (\d+) days?", date_input)
if in_days_match:
days = int(in_days_match.group(1))
return (today + timedelta(days=days)).strftime("%Y-%m-%d")
# Handle day names: "saturday", "next friday"
days_of_week = {
"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
"friday": 4, "saturday": 5, "sunday": 6
}
for day_name, day_num in days_of_week.items():
if day_name in date_input:
days_ahead = (day_num - today.weekday()) % 7
if days_ahead == 0:
days_ahead = 7 # Next week's instance
if "next" in date_input:
days_ahead += 7
return (today + timedelta(days=days_ahead)).strftime("%Y-%m-%d")
# Fall back to dateutil parser for everything else
try:
parsed = dateutil_parser.parse(date_input, fuzzy=True)
return parsed.strftime("%Y-%m-%d")
except (ValueError, TypeError):
# If all else fails, return today
return today.strftime("%Y-%m-%d")
def parse_date_range(date_input: str = None) -> list[str]:
"""
Parse date ranges like 'this weekend' into multiple dates.
Returns:
List of date strings in YYYY-MM-DD format
"""
if date_input is None:
return [datetime.now().strftime("%Y-%m-%d")]
date_input = date_input.lower().strip()
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
# Handle weekend ranges
if "weekend" in date_input:
if "next" in date_input:
days_until_saturday = (5 - today.weekday()) % 7 or 7
saturday = today + timedelta(days=days_until_saturday + 7)
else:
days_until_saturday = (5 - today.weekday()) % 7
if days_until_saturday == 0 and today.weekday() != 5:
days_until_saturday = 7
saturday = today + timedelta(days=days_until_saturday)
sunday = saturday + timedelta(days=1)
return [saturday.strftime("%Y-%m-%d"), sunday.strftime("%Y-%m-%d")]
# Handle "next X days"
next_days_match = re.match(r"next (\d+) days?", date_input)
if next_days_match:
num_days = int(next_days_match.group(1))
return [(today + timedelta(days=i)).strftime("%Y-%m-%d")
for i in range(num_days)]
# Single date
return [parse_date(date_input)]
>>> parse_date("tomorrow") '2025-12-22' >>> parse_date("this weekend") '2025-12-27' >>> parse_date("next friday") '2025-12-26' >>> parse_date_range("this weekend") ['2025-12-27', '2025-12-28']
Smart Caching
Different data has different freshness requirements. Tides are predictable days in advance; weather changes hourly.
# utils/cache.py
import json
import hashlib
import time
from pathlib import Path
from typing import Any, Optional
from functools import wraps
# Cache directory
CACHE_DIR = Path.home() / ".fishing-agent-cache"
CACHE_DIR.mkdir(exist_ok=True)
# TTL (time-to-live) in seconds for different data types
CACHE_TTL = {
"tides": 86400, # 24 hours - tides are predictable
"weather": 3600, # 1 hour - weather changes
"marine": 900, # 15 minutes - conditions change faster
"location": 604800, # 7 days - stations don't move
"moon": 86400, # 24 hours - moon phase is predictable
}
def _cache_key(prefix: str, *args, **kwargs) -> str:
"""Generate a cache key from function arguments."""
key_data = json.dumps({"args": args, "kwargs": kwargs}, sort_keys=True)
hash_str = hashlib.md5(key_data.encode()).hexdigest()[:12]
return f"{prefix}_{hash_str}"
def _get_cache(key: str, ttl: int) -> Optional[Any]:
"""Retrieve from cache if not expired."""
cache_file = CACHE_DIR / f"{key}.json"
if not cache_file.exists():
return None
try:
with open(cache_file, "r") as f:
cached = json.load(f)
if time.time() - cached["timestamp"] > ttl:
return None # Expired
return cached["data"]
except (json.JSONDecodeError, KeyError):
return None
def _set_cache(key: str, data: Any) -> None:
"""Save to cache with timestamp."""
cache_file = CACHE_DIR / f"{key}.json"
with open(cache_file, "w") as f:
json.dump({
"timestamp": time.time(),
"data": data
}, f)
def cached(data_type: str):
"""
Decorator for caching function results.
Usage:
@cached("tides")
def get_tides(location, date):
...
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
ttl = CACHE_TTL.get(data_type, 3600)
key = _cache_key(data_type, *args, **kwargs)
# Try cache first
cached_result = _get_cache(key, ttl)
if cached_result is not None:
return cached_result
# Call function and cache result
result = func(*args, **kwargs)
_set_cache(key, result)
return result
return wrapper
return decorator
def clear_cache(data_type: str = None) -> int:
"""
Clear cache files.
Args:
data_type: If provided, only clear that type. Otherwise clear all.
Returns:
Number of files deleted
"""
count = 0
for cache_file in CACHE_DIR.glob("*.json"):
if data_type is None or cache_file.name.startswith(data_type):
cache_file.unlink()
count += 1
return count
Building the Tools
Now let’s build the tools that use these utilities.
Tool 1: Location Resolution
This is the magic that lets users say “Marathon” instead of providing coordinates and station IDs.
# tools/location.py
import requests
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
from geopy.distance import geodesic
from strands import tool
from utils.cache import cached
# Initialize geocoder with rate limiting (Nominatim requires 1 req/sec)
_geocoder = Nominatim(user_agent="fishing-report-agent/1.0", timeout=10)
geocode = RateLimiter(_geocoder.geocode, min_delay_seconds=1.0)
reverse_geocode = RateLimiter(_geocoder.reverse, min_delay_seconds=1.0)
@cached("location")
def _find_nearest_noaa_station(lat: float, lon: float) -> dict:
"""Find the nearest NOAA tide station to given coordinates."""
# NOAA CO-OPS station list endpoint
url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json"
params = {"type": "tidepredictions"}
response = requests.get(url, params=params, timeout=10)
data = response.json()
# Find nearest station
target = (lat, lon)
nearest = None
min_distance = float("inf")
for station in data.get("stations", []):
try:
station_loc = (station["lat"], station["lng"])
distance = geodesic(target, station_loc).miles
if distance < min_distance:
min_distance = distance
nearest = {
"station_id": station["id"],
"station_name": station["name"],
"lat": station["lat"],
"lon": station["lng"],
"distance_miles": round(distance, 1)
}
except (KeyError, TypeError):
continue
return nearest
@tool
def resolve_location(location: str) -> dict:
"""
Resolve a location name or coordinates to a fishing-ready location with
the nearest NOAA tide station.
Args:
location: Location name (e.g., "Key West", "Marathon, FL") or
coordinates as "lat, lon" (e.g., "24.55, -81.78")
Returns:
Dictionary with resolved coordinates, location name, and nearest
NOAA tide station information
"""
# Check if input is coordinates
if "," in location:
parts = location.split(",")
if len(parts) == 2:
try:
lat = float(parts[0].strip())
lon = float(parts[1].strip())
# Reverse geocode to get location name
geo_result = reverse_geocode(f"{lat}, {lon}")
name = geo_result.address if geo_result else f"{lat}, {lon}"
station = _find_nearest_noaa_station(lat, lon)
return {
"resolved_name": name.split(",")[0], # First part of address
"lat": lat,
"lon": lon,
"noaa_station": station
}
except ValueError:
pass # Not coordinates, treat as place name
# Geocode the location name (rate-limited to 1 req/sec)
geo_result = geocode(f"{location}, USA")
if not geo_result:
# Try without country restriction
geo_result = geocode(location)
if not geo_result:
return {
"error": f"Could not find location: {location}",
"suggestion": "Try a more specific name like 'Marathon, FL' or coordinates"
}
lat = geo_result.latitude
lon = geo_result.longitude
# Find nearest NOAA station
station = _find_nearest_noaa_station(lat, lon)
return {
"resolved_name": geo_result.address.split(",")[0],
"full_address": geo_result.address,
"lat": lat,
"lon": lon,
"noaa_station": station
}
>>> resolve_location("Marathon") { "resolved_name": "Marathon", "full_address": "Marathon, Monroe County, Florida, USA", "lat": 24.7136, "lon": -81.0906, "noaa_station": { "station_id": "8724110", "station_name": "Marathon", "lat": 24.7117, "lon": -81.0883, "distance_miles": 0.2 } } >>> resolve_location("24.55, -81.78") { "resolved_name": "Key West", "lat": 24.55, "lon": -81.78, "noaa_station": { "station_id": "8724580", "station_name": "Key West", "distance_miles": 0.8 } }
Tool 2: Tide Data
Now the tide tool uses the resolved station instead of hardcoded IDs.
# tools/tides.py
import requests
from datetime import datetime
from strands import tool
from utils.cache import cached
from utils.dates import parse_date
@cached("tides")
def _fetch_tides(station_id: str, date: str) -> dict:
"""Fetch tide data from NOAA (cached)."""
url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter"
params = {
"begin_date": date.replace("-", ""),
"end_date": date.replace("-", ""),
"station": station_id,
"product": "predictions",
"datum": "MLLW",
"time_zone": "lst_ldt",
"interval": "hilo",
"units": "english",
"format": "json"
}
response = requests.get(url, params=params, timeout=10)
data = response.json()
high_tides = []
low_tides = []
for pred in data.get("predictions", []):
tide_time = datetime.strptime(pred["t"], "%Y-%m-%d %H:%M")
entry = {
"time": tide_time.strftime("%-I:%M %p"),
"height_ft": round(float(pred["v"]), 2)
}
if pred["type"] == "H":
high_tides.append(entry)
else:
low_tides.append(entry)
return {"high_tides": high_tides, "low_tides": low_tides}
@tool
def get_tides(station_id: str, station_name: str, date: str = None) -> dict:
"""
Get tide predictions for a NOAA station.
Args:
station_id: NOAA station ID (from resolve_location)
station_name: Station name for display
date: Date as YYYY-MM-DD or natural language ("tomorrow", "Saturday")
Returns:
Dictionary with high_tides and low_tides, each containing time and height
"""
parsed_date = parse_date(date)
try:
tides = _fetch_tides(station_id, parsed_date)
return {
"station": station_name,
"date": parsed_date,
**tides
}
except Exception as e:
return {
"error": f"Could not fetch tides: {str(e)}",
"station": station_name,
"date": parsed_date
}
Tool 3: Marine Conditions
# tools/marine.py
import requests
from strands import tool
from utils.cache import cached
@cached("marine")
def _fetch_marine(lat: float, lon: float) -> dict:
"""Fetch marine conditions from Open-Meteo (cached)."""
url = "https://marine-api.open-meteo.com/v1/marine"
params = {
"latitude": lat,
"longitude": lon,
"current": "wave_height,wave_direction,wave_period",
"temperature_unit": "fahrenheit",
"timezone": "America/New_York"
}
response = requests.get(url, params=params, timeout=10)
return response.json()
def _degrees_to_cardinal(degrees: float) -> str:
"""Convert degrees to cardinal direction."""
directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
idx = round(degrees / 22.5) % 16
return directions[idx]
@tool
def get_marine_conditions(lat: float, lon: float, location_name: str = "") -> dict:
"""
Get current marine conditions including wave height, swell, and water temperature.
Args:
lat: Latitude
lon: Longitude
location_name: Location name for display
Returns:
Dictionary with wave_height_ft, wave_direction, water_temp_f, swell info
"""
try:
data = _fetch_marine(lat, lon)
current = data.get("current", {})
wave_height_m = current.get("wave_height", 0.5)
wave_height_ft = round(wave_height_m * 3.28084, 1)
# Swell is typically 70-90% of significant wave height (empirical approximation)
# See NOAA's wave measurement guide for technical details
swell_height_ft = round(wave_height_ft * 0.8, 1)
return {
"location": location_name,
"wave_height_ft": wave_height_ft,
"wave_direction": _degrees_to_cardinal(current.get("wave_direction", 90)),
"wave_period_sec": current.get("wave_period", 6),
"swell_height_ft": swell_height_ft
}
except Exception as e:
return {"error": f"Could not fetch marine conditions: {str(e)}"}
Tool 4: Weather Forecast
# tools/weather.py
import requests
from strands import tool
from utils.cache import cached
@cached("weather")
def _fetch_weather(lat: float, lon: float) -> dict:
"""Fetch weather from NWS (cached)."""
headers = {"User-Agent": "FishingReportAgent/1.0"}
# Get grid point
points_url = f"https://api.weather.gov/points/{lat},{lon}"
points_response = requests.get(points_url, headers=headers, timeout=10)
points_data = points_response.json()
# Get forecast
forecast_url = points_data["properties"]["forecast"]
forecast_response = requests.get(forecast_url, headers=headers, timeout=10)
return forecast_response.json()
@tool
def get_weather(lat: float, lon: float, location_name: str = "") -> dict:
"""
Get current weather conditions and forecast.
Args:
lat: Latitude
lon: Longitude
location_name: Location name for display
Returns:
Dictionary with temperature, wind, humidity, and conditions
"""
try:
data = _fetch_weather(lat, lon)
current = data["properties"]["periods"][0]
return {
"location": location_name,
"temperature_f": current["temperature"],
"wind_speed_mph": int(current["windSpeed"].split()[0]),
"wind_direction": current["windDirection"],
"conditions": current["shortForecast"],
"precipitation_chance": current.get("probabilityOfPrecipitation", {}).get("value", 0) or 0,
"detailed_forecast": current["detailedForecast"]
}
except Exception as e:
return {"error": f"Could not fetch weather: {str(e)}"}
Tool 5: Moon Phase
# tools/moon.py
import math
from datetime import datetime
from strands import tool
from utils.dates import parse_date
from utils.cache import cached
@tool
def get_moon_phase(date: str = None) -> dict:
"""
Get moon phase and its implications for fishing.
Args:
date: Date as YYYY-MM-DD or natural language ("tomorrow", "Saturday")
Returns:
Dictionary with phase name, illumination percentage, and fishing implications
"""
parsed_date = parse_date(date)
dt = datetime.strptime(parsed_date, "%Y-%m-%d")
# Reference new moon: January 6, 2000, 18:14 UTC
# Source: USNO (US Naval Observatory) astronomical data
reference = datetime(2000, 1, 6, 18, 14)
days_since = (dt - reference).days
# Synodic month (lunar cycle): 29.53058867 days
# Source: Meeus, Astronomical Algorithms (high-precision value)
lunar_cycle = 29.53058867
phase_days = days_since % lunar_cycle
# Calculate illumination
illumination = (1 - math.cos(2 * math.pi * phase_days / lunar_cycle)) / 2 * 100
# Determine phase name
if phase_days < 1.85:
phase = "New Moon"
elif phase_days < 7.38:
phase = "Waxing Crescent"
elif phase_days < 9.23:
phase = "First Quarter"
elif phase_days < 14.77:
phase = "Waxing Gibbous"
elif phase_days < 16.61:
phase = "Full Moon"
elif phase_days < 22.15:
phase = "Waning Gibbous"
elif phase_days < 23.99:
phase = "Last Quarter"
else:
phase = "Waning Crescent"
# Fishing implications
if phase in ["New Moon", "Full Moon"]:
rating = "Excellent"
notes = "Major feeding period. Fish are highly active. Plan for dawn, dusk, and nighttime."
elif phase in ["First Quarter", "Last Quarter"]:
rating = "Good"
notes = "Minor feeding period. Focus on tide changes for best results."
elif illumination > 60:
rating = "Fair"
notes = "Fish may feed more at night. Dawn bite can be slower."
else:
rating = "Fair"
notes = "Standard conditions. Focus on tide movement and structure."
return {
"date": parsed_date,
"phase": phase,
"illumination_pct": round(illumination),
"days_until_full": round((14.77 - phase_days) % lunar_cycle),
"fishing_rating": rating,
"fishing_notes": notes
}
Assembling the Agent
Now we bring it all together with a system prompt that knows how to use these dynamic tools.
# agent.py
from strands import Agent
from strands.models import BedrockModel
from tools.location import resolve_location
from tools.tides import get_tides
from tools.marine import get_marine_conditions
from tools.weather import get_weather
from tools.moon import get_moon_phase
SYSTEM_PROMPT = """You are an expert fishing guide with decades of experience.
WORKFLOW:
1. ALWAYS start by calling resolve_location with the user's location
2. Use the returned coordinates and NOAA station for other tool calls
3. Parse any date naturally - users may say "tomorrow", "this weekend", etc.
TOOLS:
- resolve_location: Convert location name/coords to fishing-ready data with nearest NOAA station
- get_tides: Get tide predictions (requires station_id from resolve_location)
- get_marine_conditions: Get wave height, swell, water temp (use lat/lon from resolve_location)
- get_weather: Get air temp, wind, conditions (use lat/lon from resolve_location)
- get_moon_phase: Get lunar phase and fishing implications
After gathering data, provide a comprehensive fishing report:
1. **Conditions Summary**: Overall rating (Excellent/Good/Fair/Poor) with key factors
2. **Tide Analysis**: When tides are moving (fish feed on moving water)
3. **Best Fishing Windows**: Specific times and why
4. **Species Recommendations**: What to target based on conditions
5. **Tackle Suggestions**: Appropriate gear
6. **Safety Notes**: Any weather or sea concerns
FISHING PRINCIPLES:
- Incoming tide brings baitfish onto flats (good for bonefish, permit)
- Outgoing tide concentrates fish in channels (good for tarpon, snook)
- Light wind (under 10mph) = better sight fishing
- East wind favorable; west wind often means weather coming
- Water temp above 75°F = active tarpon; below 70°F = slower
- Full/new moon = stronger tides = more fish movement
Be specific with times and locations. Speak like an experienced guide.
"""
def create_fishing_agent():
"""Create and return the fishing report agent."""
return Agent(
model=BedrockModel(model_id="global.anthropic.claude-opus-4-5-20251101-v1:0"),
system_prompt=SYSTEM_PROMPT,
tools=[resolve_location, get_tides, get_marine_conditions,
get_weather, get_moon_phase]
)
if __name__ == "__main__":
agent = create_fishing_agent()
print("🎣 Florida Keys Fishing Report Agent")
print("Ask about any coastal US location!")
print("Examples: 'Conditions in Marathon tomorrow'")
print(" 'Should I fish Key West this weekend?'")
print("Type 'quit' to exit\n")
while True:
question = input("You: ").strip()
if question.lower() in ['quit', 'exit', 'q']:
break
response = agent(question)
print(f"\nAgent: {response}\n")
Running the Agent
python agent.py
🎣 Florida Keys Fishing Report Agent Ask about any coastal US location! You: What are fishing conditions in Marathon tomorrow? Agent: # 🎣 MARATHON FISHING REPORT - December 22nd, 2025 ## **Conditions Summary: EXCELLENT** ⭐⭐⭐⭐⭐ The new moon continues through tomorrow, keeping fish in a major feeding pattern. ## **Tide Analysis** - **High**: 1:18 AM (1.1 ft) - **Low**: 7:52 AM (-0.1 ft) - **High**: 1:45 PM (0.9 ft) The morning outgoing tide is prime time. Fish will be concentrated in channels and passes as water drains off the flats. ## **Best Fishing Windows** 1. **6:00 - 10:00 AM** — Strong outgoing tide, fish stacked in channels 2. **12:00 - 2:00 PM** — Tide turn, fish moving back onto flats ## **Marine & Weather** - Waves: 1.2 ft from E — Very fishable - Wind: 8 mph East — Perfect conditions - Air: 78°F, Mostly Sunny ## **Target Species** - **Tarpon**: Bridges and deep channels on the outgoing - **Permit**: Moving onto flats as tide comes in - **Bonefish**: Shallow flats, late morning ## **Tackle** - Light spin or fly tackle - 20lb fluorocarbon leader - Live pilchards or white bucktails ## **Safety**: Excellent conditions. No concerns. **Bottom Line**: New moon + east wind + manageable seas = get out there!
How the Caching Works
After the first query, subsequent queries are much faster:
First query for Marathon: ~3.2 seconds (API calls)
Follow-up query: ~0.8 seconds (cached data, only LLM call)
Same query next hour: ~1.1 seconds (weather refreshed, rest cached)
The cache respects data freshness:
- Tides: Cached 24 hours (predictions don’t change)
- Weather: Cached 1 hour (conditions evolve)
- Marine: Cached 15 minutes (waves change quickly)
- Location: Cached 7 days (stations don’t move)
What’s Next
This agent demonstrates the core patterns:
- Dynamic tool inputs instead of hardcoded options
- Natural language understanding for dates and locations
- Smart caching for performance and API limits
From here, you could:
- Add more data sources — Fishing reports, satellite imagery, solunar tables
- Build a web UI — Wrap in FastAPI, add a chat interface
- Multi-day planning — Compare conditions across a date range
- Personalization — Remember user preferences (boat vs shore, fly vs spin)
- Alerts — Notify when conditions are ideal for target species
The agent pattern scales well. Start simple, add capabilities as needed.
More agent tutorials:
- Holiday Cocktail Agent — Weather-aware drink suggestions with TheCocktailDB
- Christmas Tree Ornament Generator — AI image generation with Nova Canvas
Written from somewhere in the Keys, where the tide waits for no one.
Comments
to join the discussion.