
From Polygon to Insight: Building an Open-Source Geospatial AI Agent on AWS
An open-source AI geospatial agent that lets you draw a polygon on a map, ask a question in plain English, and get back satellite analysis — NDVI, burn severity, water detection, thermal maps — as interactive overlays. Built as a coding agent (not tool-calling) so satellite data stays in memory and never floods the LLM context. Deployed on Amazon Bedrock AgentCore with a serverless React UI. Fully extensible: add a Python function with a docstring, and the agent discovers it automatically.
Series: Strands Agents CodeAct (3 articles)
- 3From Polygon to Insight: Building an Open-Source Geospatial AI Agent on AWS This article
You draw a polygon over a wildfire-affected region in California. You type: "What is the burn severity here?"
Within seconds, the agent fetches the most recent cloud-free Landsat scene, computes the Normalized Burn Ratio before and after the fire, classifies the result into severity categories, and overlays a color-coded map directly on your browser's interactive map — with statistics showing that 34% of the area experienced high-severity burns.
No configuration. No code. No GIS software.
This post explains how we built this — the key architectural decisions, the code that makes it work, and how to deploy and extend it yourself. The project is open-source on GitHub .
The Core Decision: Coding Agent, Not Tool-Calling Agent
Most AI agents are tool-calling agents: the LLM picks a function, calls it with arguments, gets a result back in its context, and decides what to do next. This works well when tools return small, structured values. It gets awkward when tools return large data structures.
For geospatial analysis, the practical workaround is to pass data through files — write intermediate results to GeoTIFF, NetCDF, or NumPy
.npy files and pass file paths between tool calls. Efficient binary formats exist, so raw serialization cost is not prohibitive. But the pattern adds friction: every handoff requires an explicit write-to-file and read-from-file step, temporary files need to be tracked and cleaned up, and for a multi-step pipeline — fetch bands, compute a vegetation index, run a thermal analysis, correlate the two — each step is a separate LLM round trip, each waiting for the previous tool to return before the next call can be made.Coding agents take a different approach. Instead of a fixed set of tools, the LLM gets a Python interpreter. It writes a script; the interpreter runs it; intermediate results stay in Python variables that persist across code blocks within the same turn. There are no files to write, no paths to track, no round trips between steps. The LLM only sees what the code explicitly prints — statistics, summaries, confirmations. A multi-step analysis that would require several sequential tool calls executes as a single script, with data flowing directly from one function to the next in memory.
We built this with the Strands Agents SDK and the
strands-code-agent library:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.agent = CodeAgent(
system_prompt="You are an expert geospatial analyst proficient in Python...",
tools=[visualize_image, visualize_map_raster_layer, share_file_with_client],
toolkits=[
VISUALIZATION_TOOLKIT,
DATA_ANALYSIS_TOOLKIT,
Toolkit(
libraries=['PIL.*', 'pycocotools.*'],
initialization_code=INITIALIZATION_CODE.render(AOI_COORDINATES=coordinates),
domain_specific_code=[
get_satellite_data, compute_NDVI, compute_NDWI,
compute_NBR, compute_dNBR, generate_overlay,
convert_lwir11_to_celsius,
],
),
],
model=BedrockModel(model_id=model_id, temperature=0.1),
)The four tools attached to the agent are notable:
python_repl (the primary tool — executes Python), visualize_image (sends a PNG to the UI), visualize_map_raster_layer (adds a geo-referenced raster to the map), and share_file_with_client (uploads a file to S3 and returns a pre-signed URL). The visualization tools are "fire-and-forget" — they stream content to the client but return nothing to the LLM, keeping context clean.The Geospatial Library: Where the Real Intelligence Lives
The LLM doesn't understand satellite imagery. It writes Python code that calls functions from a purpose-built geospatial library. That library is where the domain expertise lives, and extending the agent means extending the library.
Satellite Data Retrieval
Fetching satellite imagery follows a four-step pipeline built on open standards:
1. STAC search. The SpatioTemporal Asset Catalog standard is a search engine for geospatial data. We query the Element84 Earth Search API , filtering by polygon intersection, date range, and cloud cover threshold. The result is a list of scenes grouped by satellite grid cell.
2. Scene selection. We pick the best scene in two steps: first, find the grid cell with the largest overlap with the user's area of interest; then, among scenes in that cell, pick the one with the lowest cloud cover percentage.
3. Band fetching. We use stackstac to load spectral bands from Cloud-Optimized GeoTIFFs on S3. COGs are the key technology here — they allow partial reads, so we download only the bounding box region we care about, not the full 100×100 km scene. The result is an
xarray.DataArray with dimensions (band, y, x).4. Polygon clip. We clip the loaded data precisely to the user's drawn polygon using
rioxarray, masking everything outside as NaN.The whole pipeline is a single function call for the agent:
1
2
3
4
5
6
7
scene = get_satellite_data(
satellite_name="sentinel",
bands=["red", "green", "blue", "nir"],
polygon_coordinates=AOI_COORDINATES,
max_cloud=20
)
# scene['data'] is an xr.DataArray ready for analysisWe support three satellites: Sentinel-2 (10–20m, visible + NIR/SWIR bands), Landsat (30m, includes the thermal
lwir11 band), and NAIP (0.3–0.6m aerial imagery, CONUS only, used for object detection).Spectral Indices
Spectral indices are mathematical combinations of bands that highlight surface properties. NDVI uses NIR and red bands to measure vegetation density; NDWI uses green and NIR for water bodies; NBR uses NIR and SWIR2 for fire damage.
We built a small framework to make adding new indices trivial. An
Index dataclass carries all the metadata:1
2
3
4
5
6
7
8
9
10
11
12
13
14
NDVI = Index(
name="NDVI (Normalized Difference Vegetation Index)",
bands=["red", "nir"],
valid_range=(-1.0, 1.0),
index_cmap='RdYlGn',
classes=[
("Water/No Data", (-1.00, 0.00), "#0000FF"),
("Bare Soil/Rock", ( 0.00, 0.15), "#8B4513"),
("Sparse Vegetation", ( 0.15, 0.33), "#DEB887"),
("Moderate Vegetation", ( 0.33, 0.50), "#90EE90"),
("Dense Vegetation", ( 0.50, 0.66), "#228B22"),
("Very Dense Vegetation", ( 0.66, 1.00), "#006400")
]
)A
compute_ function wraps the math:1
2
3
4
5
6
7
8
9
10
def compute_NDVI(data: xr.DataArray) -> ComputedIndex:
"""
Compute Normalized Difference Vegetation Index (NDVI).
NDVI = (NIR - Red) / (NIR + Red)
...
"""
bands = get_bands(data, NDVI.bands)
return ComputedIndex(
NDVI, safe_divide(bands['nir'] - bands['red'], bands['nir'] + bands['red'])
)The
ComputedIndex object that comes back provides get_statistics() (min, max, mean, median, percentiles), get_class_percentages() (pixel distribution per class), and class_to_rgba() (an RGBA array ready for visualization). The pattern is the same for every index — you define the metadata, write the formula, and everything else just works.To add a completely new index, it's about fifteen lines of code. You define an
Index instance, write a compute_ function with a docstring, export it from __init__.py, and register it in the agent's toolkit list. That's the whole extension process.Auto-Documentation via inspect
Here's a subtle but important design detail. The
strands-code-agent library uses Python's inspect module to automatically extract docstrings and type hints from every function in the toolkit's domain_specific_code list. It then injects this documentation into the agent's system prompt.This means the LLM always has accurate, up-to-date documentation of every available function — including the exact argument names, types, and what the function returns. You never need to maintain a separate schema. Write a good docstring, and the agent knows how to use your function.
Infrastructure: Serverless and Managed End-to-End
The infrastructure is defined entirely in AWS CDK (Python), across three stacks that deploy in dependency order.
1
VPCStack → AgentCoreStack → WebAppStackAmazon Bedrock AgentCore
AgentCore is the managed service that hosts the agent. You provide a Docker image; AgentCore handles running it, exposing an HTTPS endpoint, managing sessions, and scaling. No server configuration, no Kubernetes, no load balancer to manage.
The container is built automatically. When you run
cdk deploy, CDK uploads the agent/ source directory to S3, computes a content hash, and triggers a CodeBuild project if the hash has changed. CodeBuild builds the ARM64 Docker image and pushes it to ECR. A Lambda-backed Custom Resource waits for the build to complete before the AgentCore runtime is created. The result: code changes automatically trigger a fresh container build with no manual steps.Security Model
The security model is deliberately minimal. Cognito authenticates users. On successful login, an Identity Pool issues temporary IAM credentials. Those credentials have exactly one permission:
bedrock-agentcore:InvokeAgentRuntime scoped to the specific ARN of this agent runtime. Users cannot invoke any other AWS resource.The agent container runs in private VPC subnets. Its security group allows only HTTPS outbound (TCP 443) via a NAT Gateway — necessary to reach the Element84 STAC API and download COG files. Nothing else is reachable.
UI Hosting
The React frontend is a serverless static site. S3 stores the build artifacts. CloudFront serves them with TLS 1.2, HTTPS redirect, and SPA routing (404s and 403s redirect to
index.html so React Router handles them). Zero backend servers.Deploying the entire stack, from infrastructure to uploaded UI, is a single command:
1
./scripts/deploy.sh --cdkThe script deploys CDK stacks, reads CloudFormation outputs, generates the React
.env file, runs npm run build, syncs to S3, and invalidates the CloudFront cache.The UI: Map and Chat, Streaming in Real Time
The user interface has two panels: an interactive map and a chat sidebar.
The map is built with MapLibre GL JS . Users switch between base maps (CARTO Dark, Google Satellite, Esri Satellite), draw polygons defining their area of interest, and see analysis results overlaid as geo-referenced raster layers. When the agent calls
visualize_map_raster_layer, the PNG and its geographic bounds arrive in the event stream and are placed at exactly the right position on the map.The chat panel renders Markdown with syntax-highlighted code blocks — the agent's generated Python code is fully visible to the user, making the analysis transparent and auditable. Images arrive inline. Download links appear as clickable buttons. After each response, the interface displays the on-demand cost of that invocation in USD.
The agent streams results as Server-Sent Events. The frontend's
api.ts is an async generator that yields typed events:| Event | What it carries |
|---|---|
text | LLM reasoning narrative |
python_code | Generated Python script |
execution_output | Python stdout |
image | Base64-encoded PNG |
file_link | S3 pre-signed download URL |
result | Final answer + token counts + cost |
This event model means the UI can react immediately to each piece of output — displaying text as it streams, adding map layers when analysis completes, showing code blocks as they're generated — without waiting for the full response.
Optional: Object Detection with Scale-to-Zero GPUs
For tasks like counting aircraft at an airport or ships in a harbor, we integrated Falcon-Perception , a 0.6B vision-language model that supports open-vocabulary object detection — you describe what you want to find in natural language, and the model finds it.
The model runs on a SageMaker real-time endpoint (
ml.g6e.xlarge, L40S GPU, 48 GB VRAM). The interesting engineering challenge: GPU instances are expensive (~$2.61/hour), but the model doesn't need to run all the time. We implemented full scale-to-zero using two CloudWatch alarms and Application Auto Scaling with SageMaker Inference Components.Scale-in: After 60 consecutive minutes with zero invocations, a CloudWatch alarm fires and a step scaling policy sets
DesiredCopyCount = 0. The GPU instance is released. Billing stops.Scale-out: When an invocation arrives but there are no instances, SageMaker emits a
NoCapacityInvocationFailures metric. A CloudWatch alarm fires immediately, triggering a step scaling policy that sets DesiredCopyCount = 1. A new instance provisions (~5–8 minutes cold start).The cold start is handled gracefully in the agent: if it catches a
ModelNotReadyException, it surfaces a friendly message to the user: "The object detection model is starting up. Please retry in 5–8 minutes." The endpoint is entirely self-managing — no cron jobs, no manual start/stop scripts.1
2
3
4
def detect_objects(image: np.ndarray, query: str) -> list[dict]:
"""Detect objects in a high-resolution image using open-vocabulary detection.
Uses Falcon-Perception via a SageMaker endpoint...
"""A typical workflow:
get_high_resolution_image() fetches NAIP imagery at 0.6m resolution for the area; detect_objects(result['rgb'], "airplane") finds instances; the agent draws bounding boxes, counts detections, and reports back to the user.Evaluation: Measuring What Matters
A geospatial agent is only as good as the accuracy of its analysis. We built an evaluation framework that benchmarks the agent against predefined test cases.
Each test case specifies a question, an expected answer, and the polygon coordinates of the area of interest:
1
2
3
4
5
6
7
{
"id": "1",
"use-case": "NDVI",
"question": "What is the NDVI in this area?",
"answer": "0.45",
"aoi": [[lon1, lat1], [lon2, lat2], ...]
}Scoring uses LLM-as-Judge: we pass the question, expected answer, and generated answer to Claude Haiku 4.5 (temperature 0.2), which returns a score between 0 and 1 with a rationale. For numerical answers, the score degrades proportionally to the percentage error — a 25% error gives a 0.75 score. This handles the fact that satellite scenes change over time, so the "right answer" for NDVI on a given date may differ slightly from the expected answer recorded in the test suite.
The
AgentClient class in evaluation/agent_client.py mirrors the web UI exactly: it reads CloudFormation stack outputs, authenticates via Cognito with real credentials, and invokes AgentCore with the same SSE streaming interface. If the web UI works, the evaluation client works.All evaluations are cached with
diskcache, keyed by model ID, temperature, system prompt, question, and both answers. Re-running the benchmark after fixing an unrelated bug costs nothing for already-evaluated pairs.1
2
3
cd evaluation
python benchmark_agent.py # Full benchmark
python benchmark_agent.py --use_case "NDVI" # Single use caseCost tracking is built in at the agent level. The
result event from the Strands SDK carries accumulated input and output token counts. We multiply these by the per-token costs defined in bedrock_models.py to compute the exact on-demand USD cost of each invocation — displayed in the UI and logged during evaluation runs.Key Takeaways
Coding agents outperform tool-calling agents when data is large and multi-step. If your domain involves data that can't be serialized without information loss, or analyses that require holding state across multiple operations, a coding agent is the right architecture. The Python interpreter is the tool.
The library, not the prompt, is the intelligence surface. We invested engineering effort in a well-designed geospatial library — clean data structures, consistent conventions, thorough docstrings. The system prompt is short. The LLM's job is to write Python; our library's job is to make that Python correct and efficient. This makes the system more reliable, more testable, and more extensible than trying to put domain knowledge into the prompt.
Auto-documentation removes a whole class of maintenance burden. By using
inspect to extract docstrings into the agent's context automatically, we never have a schema that drifts from the implementation. The contract between the LLM and the library is always in sync.AgentCore + CDK makes managed agent hosting genuinely simple. The combination of a managed container runtime (no server to configure) and infrastructure-as-code (reproducible, reviewable, version-controlled deployment) significantly reduces operational overhead. The automatic container rebuild on code changes — triggered by a content hash in a CloudFormation Custom Resource — is a small pattern with large payoff.
Scale-to-zero GPUs are practical. The two-alarm Application Auto Scaling pattern for SageMaker Inference Components is straightforward to implement and eliminates GPU idle billing entirely. For optional capabilities that aren't used constantly, this is the right default.
Getting Started
The project is open-source under the MIT-0 license. To deploy your own instance:
1
2
3
git clone https://github.com/aws-samples/sample-geospatial-agent
cd sample-geospatial-agent
./scripts/deploy.sh --cdkYou'll need AWS credentials, CDK bootstrapped in your account, and Node.js for the React build. The deploy script handles everything else. After deployment, create a Cognito user:
1
2
3
4
aws cognito-idp admin-create-user \
--user-pool-id <user-pool-id> \
--username <username> \
--temporary-password <password>Open the CloudFront URL, log in, draw a polygon, and ask a question.
To run the agent locally against your own AWS account (useful for development):
1
2
python -m geospatial_agent.agent_service # starts on localhost:8080
./scripts/start-ui-local.sh --local # UI connects to local agentTo extend the agent with new analysis capabilities, add a function with a docstring and type hints to
agent/geospatial/, export it in __init__.py, register it in the toolkit in agent.py, and redeploy. CDK detects the source change and rebuilds the container automatically.We'd love to see what you build with it. Issues, PRs, and questions are welcome on GitHub.
Series: Strands Agents CodeAct (3 articles)
- 3From Polygon to Insight: Building an Open-Source Geospatial AI Agent on AWS This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article