Building a Fishing Report Agent with AWS Strands

Open Seas 40 min read December 21, 2025 |
0

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:

  1. Resolves any location → finds the nearest NOAA tide station
  2. Parses natural dates → “tomorrow”, “this weekend”, “next Friday”
  3. Caches smartly → tide data daily, weather hourly, marine data every 15 minutes
ToolData SourceWhat It Provides
resolve_locationNominatim + NOAAGeocode location, find nearest tide station
get_tidesNOAA CO-OPS APIHigh/low tide times
get_marine_conditionsOpen-Meteo APIWave height, swell, water temp
get_weatherNWS APIAir temp, wind, precipitation
get_moon_phaseCalculationMoon 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)]
Date Parsing Examples
>>> 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
    }
Location Resolution Examples
>>> 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
Example Session
🎣 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:

  1. Add more data sources — Fishing reports, satellite imagery, solunar tables
  2. Build a web UI — Wrap in FastAPI, add a chat interface
  3. Multi-day planning — Compare conditions across a date range
  4. Personalization — Remember user preferences (boat vs shore, fly vs spin)
  5. Alerts — Notify when conditions are ideal for target species

The agent pattern scales well. Start simple, add capabilities as needed.

More agent tutorials:


Written from somewhere in the Keys, where the tide waits for no one.

Found this helpful?
0

Comments

Loading comments...