DEV Community

shashank ms
shashank ms

Posted on

Building Environmental Science Tools with LLM

We are going to build a command-line environmental analyst on Oxlo.ai that fetches live air quality data and returns a structured risk assessment. It is useful for researchers, policy tool builders, or anyone who needs to turn raw sensor readings into actionable summaries without managing token-based billing surprises.

What you'll need

  • Python 3.10+
  • The OpenAI SDK: pip install openai
  • The requests library: pip install requests
  • An Oxlo.ai API key from https://portal.oxlo.ai

Step 1: Initialize the Oxlo.ai client and data fetcher

We start by importing the dependencies and creating an OpenAI-compatible client pointed at Oxlo.ai. We also add a helper that calls the Open-Meteo Air Quality API for current PM2.5, PM10, ozone, and UV index values.

import requests
from openai import OpenAI

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

def fetch_air_quality(lat: float, lon: float):
    url = "https://air-quality-api.open-meteo.com/v1/air-quality"
    params = {
        "latitude": lat,
        "longitude": lon,
        "current": "pm10,pm2_5,ozone,uv_index",
        "timezone": "auto",
    }
    resp = requests.get(url, params=params, timeout=30)
    resp.raise_for_status()
    return resp.json()["current"]

Step 2: Resolve city names to coordinates

Hard-coding coordinates is brittle, so we use Open-Meteo's free geocoding endpoint to translate a city name into latitude and longitude.

def geocode_city(name: str):
    url = "https://geocoding-api.open-meteo.com/v1/search"
    params = {"name": name, "count": 1}
    resp = requests.get(url, params=params, timeout=30)
    resp.raise_for_status()
    data = resp.json()
    results = data.get("results")
    if not results:
        raise ValueError(f"City not found: {name}")
    r = results[0]
    return r["latitude"], r["longitude"], r["name"]

Step 3: Define the agent system prompt

The system prompt encodes the evaluation rubric and the exact JSON schema we want back. Keeping the prompt explicit keeps the model deterministic.

SYSTEM_PROMPT = """
You are an environmental science analyst. A user will provide current air quality readings for a location. Evaluate overall health risk as low, moderate, or high. Return a JSON object with exactly these keys: summary (one sentence), risk_level (low | moderate | high), pm2_5_note (string), ozone_note (string), uv_note (string), recommendation (one practical recommendation). Be concise. Use EPA category thresholds where PM2.5 0-12 is low, 12.1-35.4 moderate, 35.5+ high. Ozone 0-50 low, 51-100 moderate, 101+ high.
"""

Step 4: Wire fetch and inference together

This function geocodes the city, pulls the latest readings, and sends them to Oxlo.ai for structured analysis. We use JSON mode so the response parses cleanly into a Python dictionary.

import json

def analyze_city(city_name: str):
    lat, lon, display_name = geocode_city(city_name)
    data = fetch_air_quality(lat, lon)

    user_message = (
        f"Location: {display_name} ({lat}, {lon})\n"
        f"Current readings:\n"
        f"- PM2.5: {data['pm2_5']} µg/m³\n"
        f"- PM10: {data['pm10']} µg/m³\n"
        f"- Ozone: {data['ozone']} µg/m³\n"
        f"- UV Index: {data['uv_index']}\n"
    )

    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
        response_format={"type": "json_object"},
        temperature=0.2,
    )

    return json.loads(response.choices[0].message.content)

Run it

Call the agent from the command line and print the structured report.

if __name__ == "__main__":
    report = analyze_city("Berlin")
    print(json.dumps(report, indent=2))

Example output:

{
  "summary": "Air quality in Berlin is currently moderate due to elevated PM2.5 levels.",
  "risk_level": "moderate",
  "pm2_5_note": "PM2.5 at 18.4 µg/m³ falls in the moderate range.",
  "ozone_note": "Ozone at 42 µg/m³ is low.",
  "uv_note": "UV index is 2, minimal risk.",
  "recommendation": "Sensitive individuals should limit prolonged outdoor exertion."
}

Next steps

Try extending the tool to accept a date range and fetch historical time-series from Open-Meteo. Because Oxlo.ai uses flat per-request pricing, you can pass long context windows filled with multi-day sensor logs without the cost scaling with input length. See https://oxlo.ai/pricing for plan details.

Another practical upgrade is wrapping the function in a FastAPI endpoint so a frontend or a cron job can query it on demand. The OpenAI-compatible SDK makes that migration trivial.

Top comments (0)