etter 0.1.1__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.
- etter-0.1.1/LICENSE +29 -0
- etter-0.1.1/PKG-INFO +293 -0
- etter-0.1.1/README.md +258 -0
- etter-0.1.1/etter/__init__.py +72 -0
- etter-0.1.1/etter/datasources/__init__.py +20 -0
- etter-0.1.1/etter/datasources/composite.py +93 -0
- etter-0.1.1/etter/datasources/ign_bdcarto.py +521 -0
- etter-0.1.1/etter/datasources/location_types.py +261 -0
- etter-0.1.1/etter/datasources/postgis.py +533 -0
- etter-0.1.1/etter/datasources/protocol.py +88 -0
- etter-0.1.1/etter/datasources/swissnames3d.py +463 -0
- etter-0.1.1/etter/examples.py +297 -0
- etter-0.1.1/etter/exceptions.py +90 -0
- etter-0.1.1/etter/models.py +155 -0
- etter-0.1.1/etter/parser.py +362 -0
- etter-0.1.1/etter/prompts.py +245 -0
- etter-0.1.1/etter/spatial.py +254 -0
- etter-0.1.1/etter/spatial_config.py +288 -0
- etter-0.1.1/etter/validators.py +199 -0
- etter-0.1.1/etter.egg-info/PKG-INFO +293 -0
- etter-0.1.1/etter.egg-info/SOURCES.txt +34 -0
- etter-0.1.1/etter.egg-info/dependency_links.txt +1 -0
- etter-0.1.1/etter.egg-info/requires.txt +26 -0
- etter-0.1.1/etter.egg-info/top_level.txt +1 -0
- etter-0.1.1/pyproject.toml +63 -0
- etter-0.1.1/setup.cfg +4 -0
- etter-0.1.1/tests/test_context_aware_distances.py +241 -0
- etter-0.1.1/tests/test_contextual_distances.py +207 -0
- etter-0.1.1/tests/test_ign_bdcarto.py +212 -0
- etter-0.1.1/tests/test_models.py +206 -0
- etter-0.1.1/tests/test_parser_streaming.py +171 -0
- etter-0.1.1/tests/test_postgis_datasource.py +405 -0
- etter-0.1.1/tests/test_spatial.py +144 -0
- etter-0.1.1/tests/test_spatial_config.py +133 -0
- etter-0.1.1/tests/test_swissnames3d.py +273 -0
- etter-0.1.1/tests/test_validators.py +240 -0
etter-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present, camptocamp SA
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
etter-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: etter
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Natural language geographic query parsing using LLMs
|
|
5
|
+
Author: etter contributors
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: langchain>=1.2
|
|
11
|
+
Requires-Dist: pydantic>=2.12
|
|
12
|
+
Requires-Dist: shapely>=2.1
|
|
13
|
+
Requires-Dist: pyproj>=3.7
|
|
14
|
+
Requires-Dist: geopandas>=1.1
|
|
15
|
+
Requires-Dist: rapidfuzz>=3.14
|
|
16
|
+
Provides-Extra: postgis
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "postgis"
|
|
18
|
+
Requires-Dist: geoalchemy2>=0.15; extra == "postgis"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: langchain-openai>=1.1.14; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff>=0.15; extra == "dev"
|
|
24
|
+
Requires-Dist: ty>=0.0.31; extra == "dev"
|
|
25
|
+
Requires-Dist: pdoc>=14.0; extra == "dev"
|
|
26
|
+
Requires-Dist: fastapi>=0.135; extra == "dev"
|
|
27
|
+
Requires-Dist: python-dotenv>=1.2; extra == "dev"
|
|
28
|
+
Requires-Dist: uvicorn>=0.44.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mcp[cli]>=1.27.0; extra == "dev"
|
|
30
|
+
Requires-Dist: rich>=15.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: testcontainers[postgres]>=4.14.2; extra == "dev"
|
|
32
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == "dev"
|
|
33
|
+
Requires-Dist: etter[postgis]; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
> **[etter](https://en.wiktionary.org/wiki/Etter#German)** /ˈɛtɐ/ *n.* (Swiss German) — the boundary or enclosure marking the edge of a village or commune; a natural demarcation between settled and unsettled land.
|
|
39
|
+
|
|
40
|
+
Natural language geographic query parsing using LLMs.
|
|
41
|
+
|
|
42
|
+
## Overview
|
|
43
|
+
|
|
44
|
+
etter transforms natural language location queries into structured geographic filters that can be used by search engines and spatial databases.
|
|
45
|
+
It uses Large Language Models (LLMs) to understand multilingual queries and extract spatial relationships.
|
|
46
|
+
|
|
47
|
+
**Key Principle:** etter's sole purpose is to extract the **geographic filter** from user queries. It does NOT handle feature/activity identification or search execution.
|
|
48
|
+
|
|
49
|
+
> [!TIP]
|
|
50
|
+
> Documentation available at [https://geoblocks.github.io/etter/](https://geoblocks.github.io/etter/)
|
|
51
|
+
|
|
52
|
+
## Sponsorship
|
|
53
|
+
|
|
54
|
+
[](https://www.camptocamp.com)
|
|
55
|
+
|
|
56
|
+
The development of this library is sponsored by [Camptocamp](https://www.camptocamp.com).
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- **Geographic Filters Only**: Extracts spatial relationships from queries, ignoring non-geographic content
|
|
61
|
+
- **Multilingual Support**: Parse queries in English, German, French, Italian, and more
|
|
62
|
+
- **Rich Spatial Relations**: Support for containment, buffer, and directional queries
|
|
63
|
+
- **Structured Output**: Pydantic models with full type safety
|
|
64
|
+
- **Streaming Support**: Real-time feedback with reasoning transparency for responsive UIs
|
|
65
|
+
- **Flexible Configuration**: Customizable spatial relations and confidence thresholds
|
|
66
|
+
- **LLM Provider Agnostic**: Works with OpenAI, Anthropic, or local models
|
|
67
|
+
|
|
68
|
+
## What etter Does (and Doesn't Do)
|
|
69
|
+
|
|
70
|
+
**✅ etter extracts:**
|
|
71
|
+
|
|
72
|
+
- Spatial relations: "north of", "in", "near", etc.
|
|
73
|
+
- Reference locations: "Lausanne", "Lake Geneva", etc.
|
|
74
|
+
- Distance parameters: "within 5km", "around 2 miles", etc.
|
|
75
|
+
|
|
76
|
+
**❌ etter does NOT handle:**
|
|
77
|
+
|
|
78
|
+
- Feature/activity identification: "hiking", "restaurants", "hotels"
|
|
79
|
+
- Attribute filtering: "with children", "vegetarian", "4-star"
|
|
80
|
+
- Search execution or database queries
|
|
81
|
+
|
|
82
|
+
**Integration Pattern:**
|
|
83
|
+
Parent application handles feature/activity filtering and combines it with etter's geographic filter for complete search functionality.
|
|
84
|
+
|
|
85
|
+
## Installation
|
|
86
|
+
|
|
87
|
+
This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Install dependencies
|
|
91
|
+
uv sync
|
|
92
|
+
|
|
93
|
+
# Or with development dependencies
|
|
94
|
+
uv sync --extra dev
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## REPL
|
|
98
|
+
|
|
99
|
+
An interactive REPL is available for testing queries interactively:
|
|
100
|
+
|
|
101
|
+
Set your OpenAI API key before running:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
export OPENAI_API_KEY='sk-...'
|
|
105
|
+
uv run python repl.py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Demo API Server
|
|
109
|
+
|
|
110
|
+
A FastAPI demo server is available that combines query parsing with geographic resolution using SwissNames3D data.
|
|
111
|
+
|
|
112
|
+
**Setup:**
|
|
113
|
+
|
|
114
|
+
Set `OPENAI_API_KEY` in your `.env` file:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
echo "OPENAI_API_KEY=sk-..." > .env
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Running the server:**
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv run uvicorn demo.main:app --port 8000 --reload
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The API will be available at `http://localhost:8000`.
|
|
127
|
+
|
|
128
|
+
**Making a query:**
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Standard endpoint (returns complete result)
|
|
132
|
+
curl -X POST http://localhost:8000/api/query \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d '{"query": "north of Lausanne"}'
|
|
135
|
+
|
|
136
|
+
# Streaming endpoint (returns Server-Sent Events)
|
|
137
|
+
curl -X POST http://localhost:8000/api/query/stream \
|
|
138
|
+
-H "Content-Type: application/json" \
|
|
139
|
+
-d '{"query": "north of Lausanne"}' \
|
|
140
|
+
--no-buffer
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Response: A GeoJSON FeatureCollection containing the parsed geographic query, spatial relation, and computed search areas.
|
|
144
|
+
|
|
145
|
+
The web UI at `http://localhost:8000` includes a toggle to enable streaming mode with real-time reasoning display.
|
|
146
|
+
|
|
147
|
+
## Quick Start
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from langchain_openai import ChatOpenAI
|
|
151
|
+
from etter import GeoFilterParser
|
|
152
|
+
import os
|
|
153
|
+
|
|
154
|
+
# Initialize LLM
|
|
155
|
+
llm = ChatOpenAI(
|
|
156
|
+
model="gpt-4o",
|
|
157
|
+
temperature=0,
|
|
158
|
+
api_key=os.getenv("OPENAI_API_KEY")
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Initialize parser
|
|
162
|
+
parser = GeoFilterParser(
|
|
163
|
+
llm=llm,
|
|
164
|
+
confidence_threshold=0.6,
|
|
165
|
+
strict_mode=False
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Strict mode - raises error on low confidence
|
|
169
|
+
parser = GeoFilterParser(
|
|
170
|
+
llm=llm,
|
|
171
|
+
confidence_threshold=0.8,
|
|
172
|
+
strict_mode=True
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Custom Spatial Relations
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from etter import SpatialRelationConfig, RelationConfig
|
|
180
|
+
|
|
181
|
+
config = SpatialRelationConfig()
|
|
182
|
+
config.register_relation(RelationConfig(
|
|
183
|
+
name="close_to",
|
|
184
|
+
category="buffer",
|
|
185
|
+
description="Very close proximity",
|
|
186
|
+
default_distance_m=1000,
|
|
187
|
+
buffer_from="center"
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
parser = GeoFilterParser(spatial_config=config)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## API Reference
|
|
194
|
+
|
|
195
|
+
### GeoFilterParser
|
|
196
|
+
|
|
197
|
+
Main class for parsing queries.
|
|
198
|
+
|
|
199
|
+
**Methods:**
|
|
200
|
+
|
|
201
|
+
- `parse(query: str) -> GeoQuery`: Parse a single query
|
|
202
|
+
- `parse_stream(query: str) -> AsyncGenerator[dict]`: Parse with streaming events
|
|
203
|
+
- `parse_batch(queries: List[str]) -> List[GeoQuery]`: Parse multiple queries
|
|
204
|
+
- `get_available_relations(category: Optional[str]) -> List[str]`: List available relations
|
|
205
|
+
- `describe_relation(name: str) -> str`: Get relation description
|
|
206
|
+
|
|
207
|
+
### GeoQuery
|
|
208
|
+
|
|
209
|
+
Structured output model representing the parsed geographic filter.
|
|
210
|
+
|
|
211
|
+
**Attributes:**
|
|
212
|
+
|
|
213
|
+
- `query_type`: Type of query (simple, compound, split, boolean)
|
|
214
|
+
- `spatial_relation`: Spatial relationship (e.g., "north_of", "in", "near")
|
|
215
|
+
- `reference_location`: Reference location (e.g., "Lausanne")
|
|
216
|
+
- `buffer_config`: Buffer parameters (optional)
|
|
217
|
+
- `confidence_breakdown`: Confidence scores
|
|
218
|
+
- `original_query`: Original input text
|
|
219
|
+
|
|
220
|
+
**Note:** etter is fully implemented with three integrated layers: parsing, geographic resolution via datasources, and spatial operations. The demo API shows a complete end-to-end workflow that resolves locations and computes search areas.
|
|
221
|
+
|
|
222
|
+
## Available Spatial Relations
|
|
223
|
+
|
|
224
|
+
### Containment
|
|
225
|
+
|
|
226
|
+
- `in`: Exact boundary matching
|
|
227
|
+
|
|
228
|
+
### Buffer/Proximity
|
|
229
|
+
|
|
230
|
+
- `near`: Proximity with context-aware distance (default 5km, LLM infers based on activity, feature scale, and intent)
|
|
231
|
+
- `on_shores_of`: 1km ring buffer (excludes water body)
|
|
232
|
+
- `along`: 500m buffer for linear features
|
|
233
|
+
- `left_bank`, `right_bank`: Buffer on one side of a linear feature (river, road) relative to its flow direction
|
|
234
|
+
- `in_the_heart_of`: Erosion for central areas (default -500m, LLM infers based on area size)
|
|
235
|
+
|
|
236
|
+
### Directional
|
|
237
|
+
|
|
238
|
+
- **Cardinal**: `north_of`, `south_of`, `east_of`, `west_of`: 10km sector (90° each)
|
|
239
|
+
- **Diagonal**: `northeast_of`, `southeast_of`, `southwest_of`, `northwest_of`: 10km sector (90° each)
|
|
240
|
+
|
|
241
|
+
## Error Handling
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from etter import ParsingError, UnknownRelationError, LowConfidenceError
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
result = parser.parse("some query")
|
|
248
|
+
except ParsingError as e:
|
|
249
|
+
print(f"Failed to parse: {e}")
|
|
250
|
+
print(f"Raw LLM response: {e.raw_response}")
|
|
251
|
+
except UnknownRelationError as e:
|
|
252
|
+
print(f"Unknown relation: {e.relation_name}")
|
|
253
|
+
except LowConfidenceError as e:
|
|
254
|
+
print(f"Low confidence: {e.confidence}")
|
|
255
|
+
print(f"Reasoning: {e.reasoning}")
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Demo Examples
|
|
259
|
+
|
|
260
|
+
Here are some good example queries to try with the demo application:
|
|
261
|
+
|
|
262
|
+
- `walk in the Gros-de-Vaud`
|
|
263
|
+
- `on the shores of the lac Morat`
|
|
264
|
+
- `near Lausanne`
|
|
265
|
+
- `south west of Lausanne`
|
|
266
|
+
- `5km north of Lausanne`
|
|
267
|
+
- `walking distance from Zurich main railway station`
|
|
268
|
+
- `15 min biking from Zurich main railway station`
|
|
269
|
+
- `along l'Orbe`
|
|
270
|
+
- `2km right bank of the Rhône`
|
|
271
|
+
|
|
272
|
+
## Architecture
|
|
273
|
+
|
|
274
|
+
See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed system design.
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Install dev dependencies
|
|
280
|
+
uv sync --extra dev
|
|
281
|
+
|
|
282
|
+
# Run tests
|
|
283
|
+
uv run pytest
|
|
284
|
+
|
|
285
|
+
# Format code
|
|
286
|
+
uv run ruff format
|
|
287
|
+
|
|
288
|
+
# Linting
|
|
289
|
+
uv run ruff check
|
|
290
|
+
|
|
291
|
+
# Type checking
|
|
292
|
+
uv run ty check
|
|
293
|
+
```
|
etter-0.1.1/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
> **[etter](https://en.wiktionary.org/wiki/Etter#German)** /ˈɛtɐ/ *n.* (Swiss German) — the boundary or enclosure marking the edge of a village or commune; a natural demarcation between settled and unsettled land.
|
|
4
|
+
|
|
5
|
+
Natural language geographic query parsing using LLMs.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
etter transforms natural language location queries into structured geographic filters that can be used by search engines and spatial databases.
|
|
10
|
+
It uses Large Language Models (LLMs) to understand multilingual queries and extract spatial relationships.
|
|
11
|
+
|
|
12
|
+
**Key Principle:** etter's sole purpose is to extract the **geographic filter** from user queries. It does NOT handle feature/activity identification or search execution.
|
|
13
|
+
|
|
14
|
+
> [!TIP]
|
|
15
|
+
> Documentation available at [https://geoblocks.github.io/etter/](https://geoblocks.github.io/etter/)
|
|
16
|
+
|
|
17
|
+
## Sponsorship
|
|
18
|
+
|
|
19
|
+
[](https://www.camptocamp.com)
|
|
20
|
+
|
|
21
|
+
The development of this library is sponsored by [Camptocamp](https://www.camptocamp.com).
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Geographic Filters Only**: Extracts spatial relationships from queries, ignoring non-geographic content
|
|
26
|
+
- **Multilingual Support**: Parse queries in English, German, French, Italian, and more
|
|
27
|
+
- **Rich Spatial Relations**: Support for containment, buffer, and directional queries
|
|
28
|
+
- **Structured Output**: Pydantic models with full type safety
|
|
29
|
+
- **Streaming Support**: Real-time feedback with reasoning transparency for responsive UIs
|
|
30
|
+
- **Flexible Configuration**: Customizable spatial relations and confidence thresholds
|
|
31
|
+
- **LLM Provider Agnostic**: Works with OpenAI, Anthropic, or local models
|
|
32
|
+
|
|
33
|
+
## What etter Does (and Doesn't Do)
|
|
34
|
+
|
|
35
|
+
**✅ etter extracts:**
|
|
36
|
+
|
|
37
|
+
- Spatial relations: "north of", "in", "near", etc.
|
|
38
|
+
- Reference locations: "Lausanne", "Lake Geneva", etc.
|
|
39
|
+
- Distance parameters: "within 5km", "around 2 miles", etc.
|
|
40
|
+
|
|
41
|
+
**❌ etter does NOT handle:**
|
|
42
|
+
|
|
43
|
+
- Feature/activity identification: "hiking", "restaurants", "hotels"
|
|
44
|
+
- Attribute filtering: "with children", "vegetarian", "4-star"
|
|
45
|
+
- Search execution or database queries
|
|
46
|
+
|
|
47
|
+
**Integration Pattern:**
|
|
48
|
+
Parent application handles feature/activity filtering and combines it with etter's geographic filter for complete search functionality.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Install dependencies
|
|
56
|
+
uv sync
|
|
57
|
+
|
|
58
|
+
# Or with development dependencies
|
|
59
|
+
uv sync --extra dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## REPL
|
|
63
|
+
|
|
64
|
+
An interactive REPL is available for testing queries interactively:
|
|
65
|
+
|
|
66
|
+
Set your OpenAI API key before running:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
export OPENAI_API_KEY='sk-...'
|
|
70
|
+
uv run python repl.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Demo API Server
|
|
74
|
+
|
|
75
|
+
A FastAPI demo server is available that combines query parsing with geographic resolution using SwissNames3D data.
|
|
76
|
+
|
|
77
|
+
**Setup:**
|
|
78
|
+
|
|
79
|
+
Set `OPENAI_API_KEY` in your `.env` file:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
echo "OPENAI_API_KEY=sk-..." > .env
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Running the server:**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv run uvicorn demo.main:app --port 8000 --reload
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The API will be available at `http://localhost:8000`.
|
|
92
|
+
|
|
93
|
+
**Making a query:**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Standard endpoint (returns complete result)
|
|
97
|
+
curl -X POST http://localhost:8000/api/query \
|
|
98
|
+
-H "Content-Type: application/json" \
|
|
99
|
+
-d '{"query": "north of Lausanne"}'
|
|
100
|
+
|
|
101
|
+
# Streaming endpoint (returns Server-Sent Events)
|
|
102
|
+
curl -X POST http://localhost:8000/api/query/stream \
|
|
103
|
+
-H "Content-Type: application/json" \
|
|
104
|
+
-d '{"query": "north of Lausanne"}' \
|
|
105
|
+
--no-buffer
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Response: A GeoJSON FeatureCollection containing the parsed geographic query, spatial relation, and computed search areas.
|
|
109
|
+
|
|
110
|
+
The web UI at `http://localhost:8000` includes a toggle to enable streaming mode with real-time reasoning display.
|
|
111
|
+
|
|
112
|
+
## Quick Start
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from langchain_openai import ChatOpenAI
|
|
116
|
+
from etter import GeoFilterParser
|
|
117
|
+
import os
|
|
118
|
+
|
|
119
|
+
# Initialize LLM
|
|
120
|
+
llm = ChatOpenAI(
|
|
121
|
+
model="gpt-4o",
|
|
122
|
+
temperature=0,
|
|
123
|
+
api_key=os.getenv("OPENAI_API_KEY")
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Initialize parser
|
|
127
|
+
parser = GeoFilterParser(
|
|
128
|
+
llm=llm,
|
|
129
|
+
confidence_threshold=0.6,
|
|
130
|
+
strict_mode=False
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Strict mode - raises error on low confidence
|
|
134
|
+
parser = GeoFilterParser(
|
|
135
|
+
llm=llm,
|
|
136
|
+
confidence_threshold=0.8,
|
|
137
|
+
strict_mode=True
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Custom Spatial Relations
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from etter import SpatialRelationConfig, RelationConfig
|
|
145
|
+
|
|
146
|
+
config = SpatialRelationConfig()
|
|
147
|
+
config.register_relation(RelationConfig(
|
|
148
|
+
name="close_to",
|
|
149
|
+
category="buffer",
|
|
150
|
+
description="Very close proximity",
|
|
151
|
+
default_distance_m=1000,
|
|
152
|
+
buffer_from="center"
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
parser = GeoFilterParser(spatial_config=config)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## API Reference
|
|
159
|
+
|
|
160
|
+
### GeoFilterParser
|
|
161
|
+
|
|
162
|
+
Main class for parsing queries.
|
|
163
|
+
|
|
164
|
+
**Methods:**
|
|
165
|
+
|
|
166
|
+
- `parse(query: str) -> GeoQuery`: Parse a single query
|
|
167
|
+
- `parse_stream(query: str) -> AsyncGenerator[dict]`: Parse with streaming events
|
|
168
|
+
- `parse_batch(queries: List[str]) -> List[GeoQuery]`: Parse multiple queries
|
|
169
|
+
- `get_available_relations(category: Optional[str]) -> List[str]`: List available relations
|
|
170
|
+
- `describe_relation(name: str) -> str`: Get relation description
|
|
171
|
+
|
|
172
|
+
### GeoQuery
|
|
173
|
+
|
|
174
|
+
Structured output model representing the parsed geographic filter.
|
|
175
|
+
|
|
176
|
+
**Attributes:**
|
|
177
|
+
|
|
178
|
+
- `query_type`: Type of query (simple, compound, split, boolean)
|
|
179
|
+
- `spatial_relation`: Spatial relationship (e.g., "north_of", "in", "near")
|
|
180
|
+
- `reference_location`: Reference location (e.g., "Lausanne")
|
|
181
|
+
- `buffer_config`: Buffer parameters (optional)
|
|
182
|
+
- `confidence_breakdown`: Confidence scores
|
|
183
|
+
- `original_query`: Original input text
|
|
184
|
+
|
|
185
|
+
**Note:** etter is fully implemented with three integrated layers: parsing, geographic resolution via datasources, and spatial operations. The demo API shows a complete end-to-end workflow that resolves locations and computes search areas.
|
|
186
|
+
|
|
187
|
+
## Available Spatial Relations
|
|
188
|
+
|
|
189
|
+
### Containment
|
|
190
|
+
|
|
191
|
+
- `in`: Exact boundary matching
|
|
192
|
+
|
|
193
|
+
### Buffer/Proximity
|
|
194
|
+
|
|
195
|
+
- `near`: Proximity with context-aware distance (default 5km, LLM infers based on activity, feature scale, and intent)
|
|
196
|
+
- `on_shores_of`: 1km ring buffer (excludes water body)
|
|
197
|
+
- `along`: 500m buffer for linear features
|
|
198
|
+
- `left_bank`, `right_bank`: Buffer on one side of a linear feature (river, road) relative to its flow direction
|
|
199
|
+
- `in_the_heart_of`: Erosion for central areas (default -500m, LLM infers based on area size)
|
|
200
|
+
|
|
201
|
+
### Directional
|
|
202
|
+
|
|
203
|
+
- **Cardinal**: `north_of`, `south_of`, `east_of`, `west_of`: 10km sector (90° each)
|
|
204
|
+
- **Diagonal**: `northeast_of`, `southeast_of`, `southwest_of`, `northwest_of`: 10km sector (90° each)
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from etter import ParsingError, UnknownRelationError, LowConfidenceError
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
result = parser.parse("some query")
|
|
213
|
+
except ParsingError as e:
|
|
214
|
+
print(f"Failed to parse: {e}")
|
|
215
|
+
print(f"Raw LLM response: {e.raw_response}")
|
|
216
|
+
except UnknownRelationError as e:
|
|
217
|
+
print(f"Unknown relation: {e.relation_name}")
|
|
218
|
+
except LowConfidenceError as e:
|
|
219
|
+
print(f"Low confidence: {e.confidence}")
|
|
220
|
+
print(f"Reasoning: {e.reasoning}")
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Demo Examples
|
|
224
|
+
|
|
225
|
+
Here are some good example queries to try with the demo application:
|
|
226
|
+
|
|
227
|
+
- `walk in the Gros-de-Vaud`
|
|
228
|
+
- `on the shores of the lac Morat`
|
|
229
|
+
- `near Lausanne`
|
|
230
|
+
- `south west of Lausanne`
|
|
231
|
+
- `5km north of Lausanne`
|
|
232
|
+
- `walking distance from Zurich main railway station`
|
|
233
|
+
- `15 min biking from Zurich main railway station`
|
|
234
|
+
- `along l'Orbe`
|
|
235
|
+
- `2km right bank of the Rhône`
|
|
236
|
+
|
|
237
|
+
## Architecture
|
|
238
|
+
|
|
239
|
+
See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed system design.
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# Install dev dependencies
|
|
245
|
+
uv sync --extra dev
|
|
246
|
+
|
|
247
|
+
# Run tests
|
|
248
|
+
uv run pytest
|
|
249
|
+
|
|
250
|
+
# Format code
|
|
251
|
+
uv run ruff format
|
|
252
|
+
|
|
253
|
+
# Linting
|
|
254
|
+
uv run ruff check
|
|
255
|
+
|
|
256
|
+
# Type checking
|
|
257
|
+
uv run ty check
|
|
258
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
etter - Natural language geographic query parsing using LLMs.
|
|
3
|
+
|
|
4
|
+
Parse location queries into structured geographic queries using LLM.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version("etter")
|
|
11
|
+
except PackageNotFoundError: # running from source without install
|
|
12
|
+
__version__ = "unknown"
|
|
13
|
+
|
|
14
|
+
# Main API
|
|
15
|
+
# Exceptions
|
|
16
|
+
# Datasources
|
|
17
|
+
from .datasources import CompositeDataSource, GeoDataSource, IGNBDCartoSource, PostGISDataSource, SwissNames3DSource
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
GeoFilterError,
|
|
20
|
+
LowConfidenceError,
|
|
21
|
+
LowConfidenceWarning,
|
|
22
|
+
ParsingError,
|
|
23
|
+
UnknownRelationError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Models (for type hints and result access)
|
|
28
|
+
from .models import (
|
|
29
|
+
BufferConfig,
|
|
30
|
+
ConfidenceLevel,
|
|
31
|
+
ConfidenceScore,
|
|
32
|
+
GeoQuery,
|
|
33
|
+
ReferenceLocation,
|
|
34
|
+
SpatialRelation,
|
|
35
|
+
)
|
|
36
|
+
from .parser import GeoFilterParser
|
|
37
|
+
|
|
38
|
+
# Spatial operations
|
|
39
|
+
from .spatial import apply_spatial_relation
|
|
40
|
+
|
|
41
|
+
# Configuration
|
|
42
|
+
from .spatial_config import RelationConfig, SpatialRelationConfig
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Main API
|
|
46
|
+
"GeoFilterParser",
|
|
47
|
+
# Models
|
|
48
|
+
"GeoQuery",
|
|
49
|
+
"SpatialRelation",
|
|
50
|
+
"ReferenceLocation",
|
|
51
|
+
"BufferConfig",
|
|
52
|
+
"ConfidenceScore",
|
|
53
|
+
"ConfidenceLevel",
|
|
54
|
+
# Configuration
|
|
55
|
+
"SpatialRelationConfig",
|
|
56
|
+
"RelationConfig",
|
|
57
|
+
# Exceptions
|
|
58
|
+
"GeoFilterError",
|
|
59
|
+
"ParsingError",
|
|
60
|
+
"ValidationError",
|
|
61
|
+
"UnknownRelationError",
|
|
62
|
+
"LowConfidenceError",
|
|
63
|
+
"LowConfidenceWarning",
|
|
64
|
+
# Datasources
|
|
65
|
+
"GeoDataSource",
|
|
66
|
+
"SwissNames3DSource",
|
|
67
|
+
"IGNBDCartoSource",
|
|
68
|
+
"CompositeDataSource",
|
|
69
|
+
"PostGISDataSource",
|
|
70
|
+
# Spatial
|
|
71
|
+
"apply_spatial_relation",
|
|
72
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geographic data source layer for resolving location names to geometries.
|
|
3
|
+
|
|
4
|
+
Provides a Protocol-based interface (GeoDataSource) and concrete implementations:
|
|
5
|
+
SwissNames3DSource, IGNBDCartoSource, PostGISDataSource, and CompositeDataSource.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .composite import CompositeDataSource
|
|
9
|
+
from .ign_bdcarto import IGNBDCartoSource
|
|
10
|
+
from .postgis import PostGISDataSource
|
|
11
|
+
from .protocol import GeoDataSource
|
|
12
|
+
from .swissnames3d import SwissNames3DSource
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CompositeDataSource",
|
|
16
|
+
"GeoDataSource",
|
|
17
|
+
"IGNBDCartoSource",
|
|
18
|
+
"PostGISDataSource",
|
|
19
|
+
"SwissNames3DSource",
|
|
20
|
+
]
|