agis-spatial 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agis_spatial-0.1.0/.github/workflows/ci.yml +50 -0
- agis_spatial-0.1.0/.github/workflows/publish.yml +18 -0
- agis_spatial-0.1.0/.gitignore +11 -0
- agis_spatial-0.1.0/LICENSE +21 -0
- agis_spatial-0.1.0/PKG-INFO +145 -0
- agis_spatial-0.1.0/README.md +114 -0
- agis_spatial-0.1.0/pyproject.toml +50 -0
- agis_spatial-0.1.0/scripts/smoke_test.py +41 -0
- agis_spatial-0.1.0/src/agis/__init__.py +3 -0
- agis_spatial-0.1.0/src/agis/cli.py +349 -0
- agis_spatial-0.1.0/src/agis/config.py +44 -0
- agis_spatial-0.1.0/src/agis/context.py +307 -0
- agis_spatial-0.1.0/src/agis/crs.py +92 -0
- agis_spatial-0.1.0/src/agis/errors.py +26 -0
- agis_spatial-0.1.0/src/agis/evals.py +224 -0
- agis_spatial-0.1.0/src/agis/executor.py +236 -0
- agis_spatial-0.1.0/src/agis/llm.py +58 -0
- agis_spatial-0.1.0/src/agis/ops/__init__.py +6 -0
- agis_spatial-0.1.0/src/agis/ops/attributes.py +175 -0
- agis_spatial-0.1.0/src/agis/ops/base.py +75 -0
- agis_spatial-0.1.0/src/agis/ops/geoprocessing.py +333 -0
- agis_spatial-0.1.0/src/agis/ops/io.py +105 -0
- agis_spatial-0.1.0/src/agis/ops/join.py +158 -0
- agis_spatial-0.1.0/src/agis/ops/merge.py +83 -0
- agis_spatial-0.1.0/src/agis/ops/tiling.py +184 -0
- agis_spatial-0.1.0/src/agis/plan.py +69 -0
- agis_spatial-0.1.0/src/agis/planner.py +210 -0
- agis_spatial-0.1.0/src/agis/sample_data.py +85 -0
- agis_spatial-0.1.0/src/agis/session.py +167 -0
- agis_spatial-0.1.0/src/agis/setup.py +241 -0
- agis_spatial-0.1.0/tests/conftest.py +9 -0
- agis_spatial-0.1.0/tests/evals/cases.yaml +166 -0
- agis_spatial-0.1.0/tests/test_attributes.py +89 -0
- agis_spatial-0.1.0/tests/test_builtins.py +71 -0
- agis_spatial-0.1.0/tests/test_cli.py +33 -0
- agis_spatial-0.1.0/tests/test_context.py +96 -0
- agis_spatial-0.1.0/tests/test_evals.py +56 -0
- agis_spatial-0.1.0/tests/test_executor.py +167 -0
- agis_spatial-0.1.0/tests/test_join.py +89 -0
- agis_spatial-0.1.0/tests/test_ops.py +234 -0
- agis_spatial-0.1.0/tests/test_setup.py +29 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
# Weekly, to catch a dependency release that drops a macOS wheel.
|
|
9
|
+
- cron: "0 9 * * 1"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
os: [macos-14, macos-15, macos-15-intel]
|
|
17
|
+
python: ["3.11", "3.13"]
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v6
|
|
22
|
+
- name: Install
|
|
23
|
+
run: |
|
|
24
|
+
uv venv --python ${{ matrix.python }}
|
|
25
|
+
uv pip install -e ".[dev]"
|
|
26
|
+
- run: uv run --no-sync ruff check .
|
|
27
|
+
- run: uv run --no-sync pytest -q
|
|
28
|
+
|
|
29
|
+
clean-install:
|
|
30
|
+
# Mirrors what a user does: install the built wheel as a uv tool on a bare runner,
|
|
31
|
+
# refusing to compile anything, then run the CLI and the smoke test.
|
|
32
|
+
strategy:
|
|
33
|
+
fail-fast: false
|
|
34
|
+
matrix:
|
|
35
|
+
os: [macos-14, macos-15, macos-15-intel]
|
|
36
|
+
runs-on: ${{ matrix.os }}
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- uses: astral-sh/setup-uv@v6
|
|
40
|
+
- run: uv build --wheel
|
|
41
|
+
- name: Install the wheel as a tool, with no source builds allowed
|
|
42
|
+
run: uv tool install --no-build --python 3.12 dist/*.whl
|
|
43
|
+
- run: agis --version
|
|
44
|
+
- run: agis --help
|
|
45
|
+
- name: Smoke test with the tool's own Python
|
|
46
|
+
run: |
|
|
47
|
+
TOOL_PY="$(uv tool dir)/agis-spatial/bin/python"
|
|
48
|
+
"$TOOL_PY" scripts/smoke_test.py
|
|
49
|
+
- name: doctor runs (Ollama is absent on CI, so a failing exit is expected)
|
|
50
|
+
run: agis doctor || true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
# PyPI trusted publishing; configure the publisher at pypi.org/manage/project/agis-spatial.
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
- run: uv build
|
|
18
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Menzies
|
|
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.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agis-spatial
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, local-first natural-language CLI for simple GIS tasks.
|
|
5
|
+
Author: Peter Menzies
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agent,cli,geospatial,gis,llm,ollama
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: MacOS
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: geopandas>=1.0
|
|
15
|
+
Requires-Dist: morecantile>=6.0
|
|
16
|
+
Requires-Dist: numpy>=1.26
|
|
17
|
+
Requires-Dist: ollama>=0.4
|
|
18
|
+
Requires-Dist: prompt-toolkit>=3
|
|
19
|
+
Requires-Dist: pydantic>=2.7
|
|
20
|
+
Requires-Dist: pyogrio>=0.9
|
|
21
|
+
Requires-Dist: pyproj>=3.6
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: rasterio>=1.4
|
|
24
|
+
Requires-Dist: rich>=13
|
|
25
|
+
Requires-Dist: shapely>=2.0
|
|
26
|
+
Requires-Dist: typer>=0.12
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# agis
|
|
33
|
+
|
|
34
|
+
Run simple GIS tasks by describing them in plain English. Everything runs on your machine with a local model; no API keys, and your data never leaves your computer.
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
$ agis "clip roads.gpkg to county.shp and reproject to web mercator"
|
|
38
|
+
Clip roads to the county and reproject to Web Mercator (EPSG:3857).
|
|
39
|
+
# operation details output
|
|
40
|
+
1 clip input=roads.gpkg mask=county.shp (intermediate)
|
|
41
|
+
2 reproject input=$step1 crs=EPSG:3857 roads_clipped_3857.gpkg
|
|
42
|
+
Run this plan? [y/n] (y):
|
|
43
|
+
Wrote roads_clipped_3857.gpkg reprojected to EPSG:3857 (WGS 84 / Pseudo-Mercator)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Install (macOS)
|
|
47
|
+
|
|
48
|
+
You need a Mac with Apple Silicon and at least 8 GB of memory (16 GB recommended). Intel Macs work, but models run on the CPU and are noticeably slower.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
brew install uv ollama # or: curl -LsSf https://astral.sh/uv/install.sh | sh, and the Ollama app from ollama.com
|
|
52
|
+
uv tool install agis-spatial # or: pipx install agis-spatial
|
|
53
|
+
agis setup
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`agis setup` checks that Ollama is running (and offers to start it), downloads `qwen2.5:3b`, and saves that choice to `~/.config/agis/config.toml`. Pass `--model` to use a different one.
|
|
57
|
+
|
|
58
|
+
You don't need Conda, a system GDAL, or any particular Python: `uv` installs agis in its own environment with its own Python, and the geospatial libraries ship with GDAL, PROJ and GEOS built in.
|
|
59
|
+
|
|
60
|
+
If something isn't working, run `agis doctor` and include its output in bug reports.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agis # interactive prompt; later requests can say "now buffer that"
|
|
66
|
+
agis "reproject parcels.shp to EPSG:2227"
|
|
67
|
+
agis "buffer the streams by 30 m and remove that from parcels, save as buildable.gpkg"
|
|
68
|
+
agis "make an MBTiles file from ortho.tif up to zoom 16"
|
|
69
|
+
agis "what projection is dem.tif in?"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`agis` on its own opens the prompt. Arrow keys move through the line you are typing, and up and down walk requests from this session and from earlier ones (`~/.config/agis/history`). Tab completes dataset names in the working folder. `ls`, `cd`, and `pwd` run locally and are not sent to the model. `exit` or Ctrl-D quits. A quoted request still runs once and exits.
|
|
73
|
+
|
|
74
|
+
agis reads the files in the current folder (CRS, geometry type, fields, raster bands) so the model knows what it's working with. It always shows the plan before running it.
|
|
75
|
+
|
|
76
|
+
| Option | Effect |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `-n`, `--dry-run` | Show the plan without running it |
|
|
79
|
+
| `-y`, `--yes` | Run without asking |
|
|
80
|
+
| `-f`, `--force` | Allow overwriting existing outputs |
|
|
81
|
+
| `-m`, `--model` | Use a different Ollama model |
|
|
82
|
+
| `-C`, `--dir` | Work in another folder |
|
|
83
|
+
| `-v`, `--verbose` | Show the file context and raw plans |
|
|
84
|
+
|
|
85
|
+
Outputs are named after their inputs and steps (`roads_clipped_3857.gpkg`) unless you name them. agis never overwrites an input. If the output name is already taken, it appends a number (`roads_clipped_3857_2.gpkg`); `--force` overwrites that file instead. Intermediate results of multi-step plans go to a temporary folder that is deleted afterwards.
|
|
86
|
+
|
|
87
|
+
### Operations
|
|
88
|
+
|
|
89
|
+
| Operation | Works on |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| reproject (EPSG codes, common names like "web mercator", "utm", or "match county.shp") | vector, raster |
|
|
92
|
+
| clip (to a mask layer or a bounding box) | vector, raster |
|
|
93
|
+
| difference, union | vector |
|
|
94
|
+
| merge (append features from other vector files; columns line up by name) | vector |
|
|
95
|
+
| buffer (meters, kilometers, feet, miles; correct in any CRS) | vector |
|
|
96
|
+
| dissolve (all, or by a field) | vector |
|
|
97
|
+
| filter (keep rows by attribute: equals, comparisons, contains, in, null) | vector |
|
|
98
|
+
| select (keep named columns; geometry is always kept) | vector |
|
|
99
|
+
| join (attach a CSV, TSV, or another layer's columns on a shared field) | vector + table |
|
|
100
|
+
| info | vector, raster, table |
|
|
101
|
+
| convert (gpkg, shp, geojson, fgb, kml, tif, Cloud Optimized GeoTIFF) | vector, raster |
|
|
102
|
+
| tile_raster (MBTiles of 256px PNG tiles) | raster |
|
|
103
|
+
|
|
104
|
+
Requests that need anything else (slope, contours, and so on) get a short explanation instead of a guess.
|
|
105
|
+
|
|
106
|
+
## How it works
|
|
107
|
+
|
|
108
|
+
agis uses a plan-then-execute design built for small local models:
|
|
109
|
+
|
|
110
|
+
1. **Context:** summarize the files in the folder and any the request mentions.
|
|
111
|
+
2. **Plan:** one call to the model through Ollama, with output constrained to a JSON schema generated from the operation definitions, so the model can only produce well-formed plans.
|
|
112
|
+
3. **Validate:** check every file, `$stepN` reference, CRS and output path. If the plan can't run, the problems go back to the model for one corrected attempt.
|
|
113
|
+
4. **Preview and confirm**, then **execute** with GeoPandas, pyogrio, shapely, pyproj and rasterio. If a step fails, files from that run are removed and the error goes back to the model for one re-plan.
|
|
114
|
+
|
|
115
|
+
The model never writes or runs code; it only fills in arguments for a fixed set of operations.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
src/agis/
|
|
119
|
+
cli.py command line (run, repl, setup, doctor, eval)
|
|
120
|
+
session.py plan -> validate -> preview -> confirm -> run, with re-planning
|
|
121
|
+
context.py file summaries given to the model
|
|
122
|
+
planner.py system prompt, few-shot examples, model call
|
|
123
|
+
plan.py Plan schema built from the operation registry
|
|
124
|
+
executor.py validation, output naming, running steps
|
|
125
|
+
llm.py model backend (Ollama)
|
|
126
|
+
ops/ one Pydantic model per operation, with its implementation
|
|
127
|
+
setup.py agis setup / agis doctor
|
|
128
|
+
evals.py agis eval
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To add an operation, subclass `OpArgs` in `agis/ops/` with an `op: Literal["name"]` field, describe its arguments with `Field(description=...)`, and implement `run`. It's registered automatically and shows up in the schema and prompt.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
uv venv && uv pip install -e ".[dev]"
|
|
137
|
+
.venv/bin/pytest # unit tests, no model needed
|
|
138
|
+
.venv/bin/agis eval -m qwen2.5:3b # plan accuracy against tests/evals/cases.yaml
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`agis eval` runs each case in `tests/evals/cases.yaml` against generated sample data and compares the plan to the expected one (CRSs, units and file names are normalized). Use it to compare models or check prompt changes; `--only` filters cases and `--show` prints every plan.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# agis
|
|
2
|
+
|
|
3
|
+
Run simple GIS tasks by describing them in plain English. Everything runs on your machine with a local model; no API keys, and your data never leaves your computer.
|
|
4
|
+
|
|
5
|
+
```console
|
|
6
|
+
$ agis "clip roads.gpkg to county.shp and reproject to web mercator"
|
|
7
|
+
Clip roads to the county and reproject to Web Mercator (EPSG:3857).
|
|
8
|
+
# operation details output
|
|
9
|
+
1 clip input=roads.gpkg mask=county.shp (intermediate)
|
|
10
|
+
2 reproject input=$step1 crs=EPSG:3857 roads_clipped_3857.gpkg
|
|
11
|
+
Run this plan? [y/n] (y):
|
|
12
|
+
Wrote roads_clipped_3857.gpkg reprojected to EPSG:3857 (WGS 84 / Pseudo-Mercator)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install (macOS)
|
|
16
|
+
|
|
17
|
+
You need a Mac with Apple Silicon and at least 8 GB of memory (16 GB recommended). Intel Macs work, but models run on the CPU and are noticeably slower.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
brew install uv ollama # or: curl -LsSf https://astral.sh/uv/install.sh | sh, and the Ollama app from ollama.com
|
|
21
|
+
uv tool install agis-spatial # or: pipx install agis-spatial
|
|
22
|
+
agis setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`agis setup` checks that Ollama is running (and offers to start it), downloads `qwen2.5:3b`, and saves that choice to `~/.config/agis/config.toml`. Pass `--model` to use a different one.
|
|
26
|
+
|
|
27
|
+
You don't need Conda, a system GDAL, or any particular Python: `uv` installs agis in its own environment with its own Python, and the geospatial libraries ship with GDAL, PROJ and GEOS built in.
|
|
28
|
+
|
|
29
|
+
If something isn't working, run `agis doctor` and include its output in bug reports.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
agis # interactive prompt; later requests can say "now buffer that"
|
|
35
|
+
agis "reproject parcels.shp to EPSG:2227"
|
|
36
|
+
agis "buffer the streams by 30 m and remove that from parcels, save as buildable.gpkg"
|
|
37
|
+
agis "make an MBTiles file from ortho.tif up to zoom 16"
|
|
38
|
+
agis "what projection is dem.tif in?"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`agis` on its own opens the prompt. Arrow keys move through the line you are typing, and up and down walk requests from this session and from earlier ones (`~/.config/agis/history`). Tab completes dataset names in the working folder. `ls`, `cd`, and `pwd` run locally and are not sent to the model. `exit` or Ctrl-D quits. A quoted request still runs once and exits.
|
|
42
|
+
|
|
43
|
+
agis reads the files in the current folder (CRS, geometry type, fields, raster bands) so the model knows what it's working with. It always shows the plan before running it.
|
|
44
|
+
|
|
45
|
+
| Option | Effect |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `-n`, `--dry-run` | Show the plan without running it |
|
|
48
|
+
| `-y`, `--yes` | Run without asking |
|
|
49
|
+
| `-f`, `--force` | Allow overwriting existing outputs |
|
|
50
|
+
| `-m`, `--model` | Use a different Ollama model |
|
|
51
|
+
| `-C`, `--dir` | Work in another folder |
|
|
52
|
+
| `-v`, `--verbose` | Show the file context and raw plans |
|
|
53
|
+
|
|
54
|
+
Outputs are named after their inputs and steps (`roads_clipped_3857.gpkg`) unless you name them. agis never overwrites an input. If the output name is already taken, it appends a number (`roads_clipped_3857_2.gpkg`); `--force` overwrites that file instead. Intermediate results of multi-step plans go to a temporary folder that is deleted afterwards.
|
|
55
|
+
|
|
56
|
+
### Operations
|
|
57
|
+
|
|
58
|
+
| Operation | Works on |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| reproject (EPSG codes, common names like "web mercator", "utm", or "match county.shp") | vector, raster |
|
|
61
|
+
| clip (to a mask layer or a bounding box) | vector, raster |
|
|
62
|
+
| difference, union | vector |
|
|
63
|
+
| merge (append features from other vector files; columns line up by name) | vector |
|
|
64
|
+
| buffer (meters, kilometers, feet, miles; correct in any CRS) | vector |
|
|
65
|
+
| dissolve (all, or by a field) | vector |
|
|
66
|
+
| filter (keep rows by attribute: equals, comparisons, contains, in, null) | vector |
|
|
67
|
+
| select (keep named columns; geometry is always kept) | vector |
|
|
68
|
+
| join (attach a CSV, TSV, or another layer's columns on a shared field) | vector + table |
|
|
69
|
+
| info | vector, raster, table |
|
|
70
|
+
| convert (gpkg, shp, geojson, fgb, kml, tif, Cloud Optimized GeoTIFF) | vector, raster |
|
|
71
|
+
| tile_raster (MBTiles of 256px PNG tiles) | raster |
|
|
72
|
+
|
|
73
|
+
Requests that need anything else (slope, contours, and so on) get a short explanation instead of a guess.
|
|
74
|
+
|
|
75
|
+
## How it works
|
|
76
|
+
|
|
77
|
+
agis uses a plan-then-execute design built for small local models:
|
|
78
|
+
|
|
79
|
+
1. **Context:** summarize the files in the folder and any the request mentions.
|
|
80
|
+
2. **Plan:** one call to the model through Ollama, with output constrained to a JSON schema generated from the operation definitions, so the model can only produce well-formed plans.
|
|
81
|
+
3. **Validate:** check every file, `$stepN` reference, CRS and output path. If the plan can't run, the problems go back to the model for one corrected attempt.
|
|
82
|
+
4. **Preview and confirm**, then **execute** with GeoPandas, pyogrio, shapely, pyproj and rasterio. If a step fails, files from that run are removed and the error goes back to the model for one re-plan.
|
|
83
|
+
|
|
84
|
+
The model never writes or runs code; it only fills in arguments for a fixed set of operations.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
src/agis/
|
|
88
|
+
cli.py command line (run, repl, setup, doctor, eval)
|
|
89
|
+
session.py plan -> validate -> preview -> confirm -> run, with re-planning
|
|
90
|
+
context.py file summaries given to the model
|
|
91
|
+
planner.py system prompt, few-shot examples, model call
|
|
92
|
+
plan.py Plan schema built from the operation registry
|
|
93
|
+
executor.py validation, output naming, running steps
|
|
94
|
+
llm.py model backend (Ollama)
|
|
95
|
+
ops/ one Pydantic model per operation, with its implementation
|
|
96
|
+
setup.py agis setup / agis doctor
|
|
97
|
+
evals.py agis eval
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
To add an operation, subclass `OpArgs` in `agis/ops/` with an `op: Literal["name"]` field, describe its arguments with `Field(description=...)`, and implement `run`. It's registered automatically and shows up in the schema and prompt.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
uv venv && uv pip install -e ".[dev]"
|
|
106
|
+
.venv/bin/pytest # unit tests, no model needed
|
|
107
|
+
.venv/bin/agis eval -m qwen2.5:3b # plan accuracy against tests/evals/cases.yaml
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`agis eval` runs each case in `tests/evals/cases.yaml` against generated sample data and compares the plan to the expected one (CRSs, units and file names are normalized). Use it to compare models or check prompt changes; `--only` filters cases and `--show` prints every plan.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agis-spatial"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A lightweight, local-first natural-language CLI for simple GIS tasks."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Peter Menzies" }]
|
|
9
|
+
keywords = ["gis", "geospatial", "llm", "agent", "ollama", "cli"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Operating System :: MacOS",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"typer>=0.12",
|
|
18
|
+
"rich>=13",
|
|
19
|
+
"pydantic>=2.7",
|
|
20
|
+
"ollama>=0.4",
|
|
21
|
+
"geopandas>=1.0",
|
|
22
|
+
"pyogrio>=0.9",
|
|
23
|
+
"shapely>=2.0",
|
|
24
|
+
"pyproj>=3.6",
|
|
25
|
+
"rasterio>=1.4",
|
|
26
|
+
"morecantile>=6.0",
|
|
27
|
+
"numpy>=1.26",
|
|
28
|
+
"pyyaml>=6.0",
|
|
29
|
+
"prompt_toolkit>=3",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = ["pytest>=8", "ruff>=0.5"]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
agis = "agis.cli:main"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/agis"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py311"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Post-install check that the geospatial stack works without any system GDAL.
|
|
2
|
+
|
|
3
|
+
Run with the Python from a fresh install, e.g. the one `uv tool install agis-spatial` created.
|
|
4
|
+
It doesn't need Ollama: it exercises the operations directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from agis.executor import resolve, run
|
|
12
|
+
from agis.ops.geoprocessing import Buffer, Clip, Convert, Reproject
|
|
13
|
+
from agis.ops.tiling import TileRaster
|
|
14
|
+
from agis.plan import Plan, plan_schema
|
|
15
|
+
from agis.sample_data import write_sample_data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> int:
|
|
19
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
20
|
+
root = Path(tmp)
|
|
21
|
+
write_sample_data(root)
|
|
22
|
+
plan = Plan(steps=[
|
|
23
|
+
Clip(input="roads.gpkg", mask="county.shp"),
|
|
24
|
+
Buffer(input="$step1", distance=50),
|
|
25
|
+
Reproject(input="$step2", crs="EPSG:3857"),
|
|
26
|
+
Convert(input="dem.tif", format="cog"),
|
|
27
|
+
TileRaster(input="ortho.tif", min_zoom=11, max_zoom=12),
|
|
28
|
+
])
|
|
29
|
+
steps = resolve(plan, root, root / "work")
|
|
30
|
+
results = run(steps)
|
|
31
|
+
for step, result in zip(steps, results):
|
|
32
|
+
print(f"{step.args.op}: {result.message}")
|
|
33
|
+
assert (root / "roads_clipped_buffer50m_3857.gpkg").exists()
|
|
34
|
+
assert (root / "ortho_tiles.mbtiles").read_bytes().startswith(b"SQLite format 3")
|
|
35
|
+
assert plan_schema()["$defs"]
|
|
36
|
+
print("smoke test passed")
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
sys.exit(main())
|