Improve search results in OSM tool.

This commit is contained in:
projectmoon 2024-08-05 09:48:44 +02:00
parent c784b30967
commit 7a8ae0c9c6
3 changed files with 119 additions and 16 deletions

View File

@ -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:**

View File

@ -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
View File

@ -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