Improve search results in OSM tool.
This commit is contained in:
parent
c784b30967
commit
7a8ae0c9c6
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,3 +1,16 @@
|
||||||
|
# OpenStreetMap Tool
|
||||||
|
|
||||||
|
**0.2.0:**
|
||||||
|
- Include Ways as secondary information for improved search results.
|
||||||
|
|
||||||
|
**0.1.0:**
|
||||||
|
- Initial release.
|
||||||
|
|
||||||
|
# Checkpoint Summarization Filter
|
||||||
|
|
||||||
|
**0.1.0:**
|
||||||
|
- Initial release.
|
||||||
|
|
||||||
# Narrative Memory Filter
|
# Narrative Memory Filter
|
||||||
|
|
||||||
**0.0.2:**
|
**0.0.2:**
|
||||||
|
|
|
@ -223,6 +223,8 @@ the LLM's reply.
|
||||||
|
|
||||||
## OpenStreetMap Tool
|
## OpenStreetMap Tool
|
||||||
|
|
||||||
|
_Recommended models: Llama 3.1, Mistral Nemo Instruct._
|
||||||
|
|
||||||
A tool that can find certain points of interest (POIs) nearby a
|
A tool that can find certain points of interest (POIs) nearby a
|
||||||
requested address or place.
|
requested address or place.
|
||||||
|
|
||||||
|
@ -243,6 +245,11 @@ This is because the public instance of the Nominatim API will block
|
||||||
you if you do not set these. Use of the public Nominatim instance is
|
you if you do not set these. Use of the public Nominatim instance is
|
||||||
governed by their [terms of use][nom-tou].
|
governed by their [terms of use][nom-tou].
|
||||||
|
|
||||||
|
The default API services are suitable for applications with a low
|
||||||
|
volume of traffic (absolute max 1 API call per second). If you are
|
||||||
|
running a production service, you should set up your own Nominatim and
|
||||||
|
Overpass services with caching.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
<img src="./agplv3.png" alt="AGPLv3" />
|
<img src="./agplv3.png" alt="AGPLv3" />
|
||||||
|
|
115
osm.py
115
osm.py
|
@ -2,7 +2,7 @@
|
||||||
title: OpenStreetMap Tool
|
title: OpenStreetMap Tool
|
||||||
author: projectmoon
|
author: projectmoon
|
||||||
author_url: https://git.agnos.is/projectmoon/open-webui-filters
|
author_url: https://git.agnos.is/projectmoon/open-webui-filters
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
license: AGPL-3.0+
|
license: AGPL-3.0+
|
||||||
required_open_webui_version: 0.3.9
|
required_open_webui_version: 0.3.9
|
||||||
"""
|
"""
|
||||||
|
@ -22,6 +22,20 @@ NO_RESULTS = ("No results found. Tell the user you found no results. "
|
||||||
"Do not make up answers or hallucinate. Only say you "
|
"Do not make up answers or hallucinate. Only say you "
|
||||||
"found no results.")
|
"found no results.")
|
||||||
|
|
||||||
|
def way_has_info(way):
|
||||||
|
"""
|
||||||
|
Determine if an OSM way entry is useful to us. This means it
|
||||||
|
has something more than just its main classification tag, and
|
||||||
|
has at least a name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return len(way['tags']) > 1 and any('name' in tag for tag in way['tags'])
|
||||||
|
|
||||||
|
def strip_nodes_from_way(way):
|
||||||
|
if 'nodes' in way:
|
||||||
|
del way['nodes']
|
||||||
|
return way
|
||||||
|
|
||||||
def get_bounding_box_center(bbox):
|
def get_bounding_box_center(bbox):
|
||||||
def convert(bbox, key):
|
def convert(bbox, key):
|
||||||
return bbox[key] if isinstance(bbox[key], float) else float(bbox[key])
|
return bbox[key] if isinstance(bbox[key], float) else float(bbox[key])
|
||||||
|
@ -102,7 +116,7 @@ class OsmSearcher:
|
||||||
else [])
|
else [])
|
||||||
|
|
||||||
|
|
||||||
def nominatim_search(self, query, format="json") -> Optional[dict]:
|
def nominatim_search(self, query, format="json", limit: int=1) -> Optional[dict]:
|
||||||
url = self.valves.nominatim_url
|
url = self.valves.nominatim_url
|
||||||
params = {
|
params = {
|
||||||
'q': query,
|
'q': query,
|
||||||
|
@ -122,13 +136,15 @@ class OsmSearcher:
|
||||||
if not data:
|
if not data:
|
||||||
raise ValueError(f"No results found for query '{query}'")
|
raise ValueError(f"No results found for query '{query}'")
|
||||||
|
|
||||||
return data[0]
|
return data[:limit]
|
||||||
else:
|
else:
|
||||||
print(response.text)
|
print(response.text)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def overpass_search(self, place, tags, bbox, limit=5, radius=4000):
|
def overpass_search(
|
||||||
|
self, place, tags, bbox, limit=5, radius=4000
|
||||||
|
) -> (List[dict], List[dict]):
|
||||||
headers = self.create_headers()
|
headers = self.create_headers()
|
||||||
if not headers:
|
if not headers:
|
||||||
raise ValueError("Headers not set")
|
raise ValueError("Headers not set")
|
||||||
|
@ -141,11 +157,10 @@ class OsmSearcher:
|
||||||
search_groups = [f'"{tag_type}"~"{"|".join(values)}"'
|
search_groups = [f'"{tag_type}"~"{"|".join(values)}"'
|
||||||
for tag_type, values in tag_groups.items()]
|
for tag_type, values in tag_groups.items()]
|
||||||
|
|
||||||
# only search nodes, which are guaranteed to have a lat and lon.
|
|
||||||
searches = []
|
searches = []
|
||||||
for search_group in search_groups:
|
for search_group in search_groups:
|
||||||
searches.append(
|
searches.append(
|
||||||
f'node[{search_group}]{around}'
|
f'nwr[{search_group}]{around}'
|
||||||
)
|
)
|
||||||
|
|
||||||
search = ";\n".join(searches)
|
search = ";\n".join(searches)
|
||||||
|
@ -163,20 +178,39 @@ class OsmSearcher:
|
||||||
data = { "data": query }
|
data = { "data": query }
|
||||||
response = requests.get(url, params=data, headers=headers)
|
response = requests.get(url, params=data, headers=headers)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
# nodes are prioritized because they have exact GPS
|
||||||
|
# coordinates. we also include useful way entries (without
|
||||||
|
# node list) as secondary results, because there are often
|
||||||
|
# useful results that don't have a node (e.g. building or
|
||||||
|
# whole area marked for the tag type).
|
||||||
|
results = response.json()
|
||||||
|
results = results['elements'] if 'elements' in results else []
|
||||||
|
nodes = []
|
||||||
|
ways = []
|
||||||
|
|
||||||
|
for res in results:
|
||||||
|
if 'type' not in res:
|
||||||
|
continue
|
||||||
|
if res['type'] == 'node':
|
||||||
|
nodes.append(res)
|
||||||
|
elif res['type'] == 'way' and way_has_info(res):
|
||||||
|
ways.append(strip_nodes_from_way(res))
|
||||||
|
|
||||||
|
return nodes, ways
|
||||||
else:
|
else:
|
||||||
print(response.text)
|
print(response.text)
|
||||||
raise Exception(f"Error calling Overpass API: {response.text}")
|
raise Exception(f"Error calling Overpass API: {response.text}")
|
||||||
|
|
||||||
|
|
||||||
def search_nearby(self, place: str, tags: List[str], limit: int=5) -> str:
|
def search_nearby(self, place: str, tags: List[str], limit: int=5, radius: int=4000) -> str:
|
||||||
headers = self.create_headers()
|
headers = self.create_headers()
|
||||||
if not headers:
|
if not headers:
|
||||||
return VALVES_NOT_SET
|
return VALVES_NOT_SET
|
||||||
|
|
||||||
try:
|
try:
|
||||||
nominatim_result = self.nominatim_search(place)
|
nominatim_result = self.nominatim_search(place, limit=1)
|
||||||
if nominatim_result:
|
if nominatim_result:
|
||||||
|
nominatim_result = nominatim_result[0]
|
||||||
bbox = {
|
bbox = {
|
||||||
'min_lat': nominatim_result['boundingbox'][0],
|
'min_lat': nominatim_result['boundingbox'][0],
|
||||||
'max_lat': nominatim_result['boundingbox'][1],
|
'max_lat': nominatim_result['boundingbox'][1],
|
||||||
|
@ -184,21 +218,24 @@ class OsmSearcher:
|
||||||
'max_lon': nominatim_result['boundingbox'][3]
|
'max_lon': nominatim_result['boundingbox'][3]
|
||||||
}
|
}
|
||||||
|
|
||||||
overpass_result = self.overpass_search(place, tags, bbox, limit)
|
nodes, ways = self.overpass_search(place, tags, bbox, limit, radius)
|
||||||
|
print(nodes)
|
||||||
|
print(ways)
|
||||||
|
|
||||||
# use results from overpass, but if they do not exist,
|
# use results from overpass, but if they do not exist,
|
||||||
# fall back to the nominatim result. we can get away
|
# fall back to the nominatim result. we can get away
|
||||||
# with this because we're not digging through the
|
# with this because we're not digging through the
|
||||||
# objects themselves (as long as they have lat/lon, we
|
# objects themselves (as long as they have lat/lon, we
|
||||||
# are good).
|
# are good).
|
||||||
things_nearby = (overpass_result ['elements']
|
things_nearby = (nodes
|
||||||
if 'elements' in overpass_result
|
if len(nodes) > 0
|
||||||
and len(overpass_result ['elements']) > 0
|
|
||||||
else OsmSearcher.fallback(nominatim_result))
|
else OsmSearcher.fallback(nominatim_result))
|
||||||
|
|
||||||
origin = get_bounding_box_center(bbox)
|
origin = get_bounding_box_center(bbox)
|
||||||
things_nearby = sort_by_closeness(origin, things_nearby)
|
things_nearby = sort_by_closeness(origin, things_nearby)
|
||||||
things_nearby = things_nearby[:limit]
|
things_nearby = things_nearby[:limit]
|
||||||
|
other_results = ways[:(limit+5)]
|
||||||
|
print(other_results)
|
||||||
|
|
||||||
if not things_nearby or len(things_nearby) == 0:
|
if not things_nearby or len(things_nearby) == 0:
|
||||||
return NO_RESULTS
|
return NO_RESULTS
|
||||||
|
@ -208,6 +245,7 @@ class OsmSearcher:
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"These are some of the {tag_type_str} points of interest nearby. "
|
f"These are some of the {tag_type_str} points of interest nearby. "
|
||||||
|
"These are the results known to be closest to the requested location. "
|
||||||
"When telling the user about them, make sure to report "
|
"When telling the user about them, make sure to report "
|
||||||
"all the information (address, contact info, website, etc).\n\n"
|
"all the information (address, contact info, website, etc).\n\n"
|
||||||
"Tell the user about ALL the results, and give the CLOSEST result "
|
"Tell the user about ALL the results, and give the CLOSEST result "
|
||||||
|
@ -223,9 +261,13 @@ class OsmSearcher:
|
||||||
"\n\nAnd so on.\n\n"
|
"\n\nAnd so on.\n\n"
|
||||||
"Only use relevant results. If there are no relevant results, "
|
"Only use relevant results. If there are no relevant results, "
|
||||||
"say so. Do not make up answers or hallucinate."
|
"say so. Do not make up answers or hallucinate."
|
||||||
f"The results are below.\n\n"
|
f"The primary results are below.\n\n"
|
||||||
"----------"
|
"----------"
|
||||||
f"\n\n{str(things_nearby)}"
|
f"\n\n{str(things_nearby)}\n\n"
|
||||||
|
"----------\n\n"
|
||||||
|
f"Additionally, here are some other results that might be useful. "
|
||||||
|
"The exact distance to these from the requested location is not known."
|
||||||
|
f"\n\n{str(other_results)}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return NO_RESULTS
|
return NO_RESULTS
|
||||||
|
@ -272,7 +314,7 @@ class Tools:
|
||||||
"""
|
"""
|
||||||
searcher = OsmSearcher(self.valves)
|
searcher = OsmSearcher(self.valves)
|
||||||
try:
|
try:
|
||||||
result = searcher.nominatim_search(address_or_place)
|
result = searcher.nominatim_search(address_or_place, limit=5)
|
||||||
if result:
|
if result:
|
||||||
return str(result)
|
return str(result)
|
||||||
else:
|
else:
|
||||||
|
@ -333,6 +375,47 @@ class Tools:
|
||||||
return searcher.search_nearby(place, tags, limit=5)
|
return searcher.search_nearby(place, tags, limit=5)
|
||||||
|
|
||||||
|
|
||||||
|
def find_swimming_near_place(self, place: str) -> str:
|
||||||
|
"""
|
||||||
|
Finds swimming pools, water parks, swimming areas, and other aquatic
|
||||||
|
activities on OpenStreetMap near a given place or address. The location
|
||||||
|
of the address or place is reverse geo-coded, then nearby results are fetched
|
||||||
|
from OpenStreetMap.
|
||||||
|
:param place: The name of a place or an address, which will be sent to Nominatim.
|
||||||
|
:return: A list of swimming poools or places, if found.
|
||||||
|
"""
|
||||||
|
tags = [
|
||||||
|
"leisure=swimming_pool",
|
||||||
|
"leisure=swimming_area",
|
||||||
|
"leisure=water_park",
|
||||||
|
"tourism=theme_park"
|
||||||
|
]
|
||||||
|
searcher = OsmSearcher(self.valves)
|
||||||
|
return searcher.search_nearby(place, tags, limit=5, radius=10000)
|
||||||
|
|
||||||
|
|
||||||
|
def find_recreation_near_place(self, place: str) -> str:
|
||||||
|
"""
|
||||||
|
Finds playgrounds, theme parks, frisbee golf, ice skating, and other recreational
|
||||||
|
activities on OpenStreetMap near a given place or address. The location
|
||||||
|
of the address or place is reverse geo-coded, then nearby results are fetched
|
||||||
|
from OpenStreetMap.
|
||||||
|
:param place: The name of a place or an address, which will be sent to Nominatim.
|
||||||
|
:return: A list of recreational places, if found.
|
||||||
|
"""
|
||||||
|
tags = [
|
||||||
|
"leisure=horse_riding",
|
||||||
|
"leisure=ice_rink",
|
||||||
|
"leisure=park",
|
||||||
|
"leisure=playground",
|
||||||
|
"leisure=disc_golf_course",
|
||||||
|
"leisure=amusement_arcade",
|
||||||
|
"tourism=theme_park"
|
||||||
|
]
|
||||||
|
searcher = OsmSearcher(self.valves)
|
||||||
|
return searcher.search_nearby(place, tags, limit=10, radius=10000)
|
||||||
|
|
||||||
|
|
||||||
def find_place_of_worship_near_place(self, place: str) -> str:
|
def find_place_of_worship_near_place(self, place: str) -> str:
|
||||||
"""
|
"""
|
||||||
Finds places of worship (churches, mosques, temples, etc) on
|
Finds places of worship (churches, mosques, temples, etc) on
|
||||||
|
|
Loading…
Reference in New Issue