forked from DemiPeek268/Ruimtelijke-Assistent-Hackathon
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathspatial.py
More file actions
134 lines (111 loc) · 4.63 KB
/
Copy pathspatial.py
File metadata and controls
134 lines (111 loc) · 4.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import copy
import json
import logging
import re
import urllib.parse
import urllib.request
from app.models.state import ConversationState, IntentAnalysis
from app.services.nodes.base import BaseNode
from langchain_core.runnables import RunnableConfig
_COORD_RE = re.compile(r"lat=([0-9.]+), lon=([0-9.]+)")
logger = logging.getLogger(__name__)
PDOK_FREE_URL = "https://api.pdok.nl/bzk/locatieserver/search/v3_1/free"
# WKT POINT(lon lat) — note: WKT uses (lon lat) order
_WKT_POINT_RE = re.compile(r"POINT\(([0-9.]+)\s+([0-9.]+)\)")
class SpatialNode(BaseNode):
"""Resolve named spatial origins into LATLON filters when needed."""
def __init__(self):
super().__init__("spatial", auto_activate=False)
async def run(self, state: ConversationState, config: RunnableConfig) -> dict:
intent_analysis = state["intent_analysis"]
intent = intent_analysis.intent
if not state.get("needs_spatial_resolution"):
return {
"needs_spatial_resolution": False,
"pdok_used": False,
}
resolved_intent = copy.deepcopy(intent)
resolved_locations: list[dict] = []
pdok_used = False
for origin_filter in resolved_intent.spatial_query.origin_filters:
query = origin_filter.value.removeprefix("PLACE:").strip()
pdok_used = True
result_str = self.pdok_locatie_search(query)
coords = self._parse_best_match(result_str)
if coords is None:
unresolved = IntentAnalysis(
is_clear=False,
follow_up_question=(
f"Ik kon de locatie '{query}' niet eenduidig vinden. "
"Kun je de locatie specifieker beschrijven?"
),
)
await self.dispatch(
"follow_up_text", {"content": unresolved.follow_up_question}, config
)
return {
"intent_analysis": unresolved,
"needs_spatial_resolution": False,
"pdok_used": pdok_used,
}
lat, lon = coords
origin_filter.value = f"LATLON:{lat:.6f},{lon:.6f}"
resolved_locations.append({"query": query, "lat": lat, "lon": lon})
updated_analysis = IntentAnalysis(is_clear=True, intent=resolved_intent)
return {
"intent_analysis": updated_analysis,
"needs_spatial_resolution": False,
"pdok_used": pdok_used,
}
@staticmethod
def _parse_centroide(wkt: str) -> tuple[float, float] | None:
"""Parse WKT POINT(lon lat) → (lat, lon)."""
m = _WKT_POINT_RE.match(wkt or "")
if not m:
return None
lon, lat = float(m.group(1)), float(m.group(2))
return lat, lon
@staticmethod
def _parse_best_match(result_text: str) -> tuple[float, float] | None:
match = _COORD_RE.search(result_text or "")
if not match:
return None
return float(match.group(1)), float(match.group(2))
@staticmethod
def pdok_locatie_search(query: str) -> str:
"""Search PDOK and return a formatted best-match result string for spatial resolution."""
params = urllib.parse.urlencode(
{"q": query, "rows": 3, "fl": "id,weergavenaam,type,centroide_ll"}
)
url = f"{PDOK_FREE_URL}?{params}"
try:
with urllib.request.urlopen(url, timeout=5) as resp:
data = json.loads(resp.read())
except Exception as exc:
logger.warning("PDOK Locatieserver request failed: %s", exc)
return f"Fout bij opzoeken van locatie '{query}': {exc}"
docs = data.get("response", {}).get("docs", [])
if not docs:
return f"Geen resultaten gevonden voor '{query}'."
lines = []
for doc in docs:
coords = SpatialNode._parse_centroide(doc.get("centroide_ll", ""))
if coords is None:
continue
lat, lon = coords
lines.append(
f"- {doc.get('weergavenaam')} (type: {doc.get('type')}) "
f"→ lat={lat:.6f}, lon={lon:.6f}"
)
if not lines:
return f"Geen coördinaten gevonden voor '{query}'."
return (
f"Resultaten voor '{query}':\n"
+ "\n".join(lines)
+ "\n\nGebruik de coördinaten van het beste resultaat als LATLON filter in spatial_query.origin_filters: "
)
def fallback(self) -> dict:
return {
"needs_spatial_resolution": False,
"pdok_used": False,
}