OpenWebUI 天气工具 本地化调优(新手向)

本文虽为新手向,但仍需一定动手和查询能力,还需要一定的好奇心和探索精神才能更好地调优

Tl;DR

"""
title: Keyless Weather
author: spyci
author_url: https://github.com/open-webui
funding_url: https://github.com/open-webui
version: 0.1.1
"""

import os
import requests
import urllib.parse
import datetime


def get_city_info(city: str):
    url = f"https://geocoding-api.open-meteo.com/v1/search?name={urllib.parse.quote(city)}&count=1&language=en&format=json"
    response = requests.get(url)

    if response.status_code == 200:
        try:
            data = response.json()["results"][0]
            return data["latitude"], data["longitude"], data["timezone"]
        except (KeyError, IndexError):
            print(f"City '{city}' not found")
            return None
    else:
        print(f"Failed to retrieve data for city '{city}': {response.status_code}")
        return None


wmo_weather_codes = {
    "0": "Clear sky",
    "1": "Mainly clear, partly cloudy, and overcast",
    "2": "Mainly clear, partly cloudy, and overcast",
    "3": "Mainly clear, partly cloudy, and overcast",
    "45": "Fog and depositing rime fog",
    "48": "Fog and depositing rime fog",
    "51": "Drizzle: Light, moderate, and dense intensity",
    "53": "Drizzle: Light, moderate, and dense intensity",
    "55": "Drizzle: Light, moderate, and dense intensity",
    "56": "Freezing Drizzle: Light and dense intensity",
    "57": "Freezing Drizzle: Light and dense intensity",
    "61": "Rain: Slight, moderate and heavy intensity",
    "63": "Rain: Slight, moderate and heavy intensity",
    "65": "Rain: Slight, moderate and heavy intensity",
    "66": "Freezing Rain: Light and heavy intensity",
    "67": "Freezing Rain: Light and heavy intensity",
    "71": "Snow fall: Slight, moderate, and heavy intensity",
    "73": "Snow fall: Slight, moderate, and heavy intensity",
    "75": "Snow fall: Slight, moderate, and heavy intensity",
    "77": "Snow grains",
    "80": "Rain showers: Slight, moderate, and violent",
    "81": "Rain showers: Slight, moderate, and violent",
    "82": "Rain showers: Slight, moderate, and violent",
    "85": "Snow showers slight and heavy",
    "86": "Snow showers slight and heavy",
    "95": "Thunderstorm: Slight or moderate",
    "96": "Thunderstorm with slight and heavy hail",
    "99": "Thunderstorm with slight and heavy hail",
}


def fetch_weather_data(base_url, params):
    try:
        response = requests.get(base_url, params=params)
        response.raise_for_status()
        data = response.json()
        if "error" in data:
            return f"Error fetching weather data: {data['message']}"
        return data
    except requests.RequestException as e:
        return f"Error fetching weather data: {str(e)}"


def format_date(date_str, date_format="%Y-%m-%dT%H:%M", output_format="%I:%M %p"):
    dt = datetime.datetime.strptime(date_str, date_format)
    return dt.strftime(output_format)


class Tools:
    def __init__(self):
        self.citation = True
        self.default_location = "你所在城市名拼音"
        pass

    def get_future_weather_week(self, city: str = None) -> str:
        """
        Get the weather for the next week for a given city.
        :param city: The name of the city to get the weather for. If None, uses default location.
        :return: The current weather information or an error message.
        """
        if not city:
            city = self.default_location

        city_info = get_city_info(city)
        if not city_info:
            return """Error fetching weather data"""

        lat, lng, tmzone = city_info
        print(f"Latitude: {lat}, Longitude: {lng}, Timezone: {tmzone}")

        base_url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": lat,
            "longitude": lng,
            "models": "ecmwf_ifs025",
            "daily": [
                "weather_code",
                "sunrise",
                "sunset",
                "temperature_2m_max",
                "temperature_2m_min",
                "cloud_cover_mean",
                "relative_humidity_2m_mean",
                "precipitation_probability_max",
                "wind_speed_10m_max",
                "wind_gusts_10m_max",
                "wind_direction_10m_dominant",
            ],
            "current": "temperature_2m",
            "timezone": tmzone,
            "forecast_days": 7,
        }

        data = fetch_weather_data(base_url, params)
        if isinstance(data, str):
            return data

        formatted_timestamp = format_date(data["current"]["time"])
        data["daily"]["time"][0] += " (Today)"

        mapped_data = {
            date: {
                "weather_description": wmo_weather_codes[
                    str(data["daily"]["weather_code"][i])
                ],
                "sunrise_sunset": f'Sunrise: {format_date(data["daily"]["sunrise"][i])} / Sunset: {format_date(data["daily"]["sunset"][i])}',
                "temperature_max_min": f'{data["daily"]["temperature_2m_max"][i]} {data["daily_units"]["temperature_2m_max"]} / {data["daily"]["temperature_2m_min"][i]} {data["daily_units"]["temperature_2m_min"]}',
                "cloud_cover": f'{data["daily"]["cloud_cover_mean"][i]} {data["daily_units"]["cloud_cover_mean"]}',
                "humidity": f'{data["daily"]["relative_humidity_2m_mean"][i]} {data["daily_units"]["relative_humidity_2m_mean"]}',
                "precipitation_probability": f'{data["daily"]["precipitation_probability_max"][i]} {data["daily_units"]["precipitation_probability_max"]}',
                "wind": f'Speed: {data["daily"]["wind_speed_10m_max"][i]} {data["daily_units"]["wind_speed_10m_max"]} / Gusts: {data["daily"]["wind_gusts_10m_max"][i]} {data["daily_units"]["wind_gusts_10m_max"]} / Direction: {data["daily"]["wind_direction_10m_dominant"][i]} {data["daily_units"]["wind_direction_10m_dominant"]}',
            }
            for i, date in enumerate(data["daily"]["time"])
        }

        return f"""
Give a weather description for the next week, include the time of the data ({formatted_timestamp} {data['timezone_abbreviation']} in {city}):
Show a standard table layout of each of these days: {mapped_data}
Include a one sentence summary of the week at the end."""

    def get_current_weather(self, city: str = None) -> str:
        """
        Get the current weather for a given city.
        :param city: The name of the city to get the weather for. If None, uses default location.
        :return: The current weather information or an error message.
        """
        if not city:
            city = self.default_location

        city_info = get_city_info(city)
        if not city_info:
            return """Error fetching weather data"""

        lat, lng, tmzone = city_info
        print(f"Latitude: {lat}, Longitude: {lng}, Timezone: {tmzone}")

        base_url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": lat,
            "longitude": lng,
            "models": "ecmwf_ifs025",
            "current": [
                "temperature_2m",
                "apparent_temperature",
                "relative_humidity_2m",
                "cloud_cover",
                "surface_pressure",
                "wind_speed_10m",
                "weather_code",
            ],
            "timezone": tmzone,
            "forecast_days": 1,
        }

        data = fetch_weather_data(base_url, params)
        if isinstance(data, str):
            return data

        formatted_timestamp = format_date(data["current"]["time"])
        data["current"]["weather_code"] = wmo_weather_codes[
            str(data["current"]["weather_code"])
        ]
        formatted_data = ", ".join(
            [
                f"{x} ({data['current_units'][x]}) = '{data['current'][x]}'"
                for x in data["current"].keys()
            ]
        ).replace("weather_code", "weather_description")

        return f"""
Give a weather description, include the time of the data ({formatted_timestamp} {data['timezone_abbreviation']} in {city}):
Include this data: [{formatted_data}]
Ensure you mention the real temperature and the "feels like"(apparent_temperature) temperature. Convert all numbers to integers.
Keep response as brief as possible."""

  • 直接替换天气工具 整个源代码。
  • 大家自行修改第83行的城市名称,用汉语拼音。
    • 此方法适合主要城市(重名概率低)。3,4县城市的拼音重名概率高,需再研究研究。
  • 此段代码只包含我日常喜欢得到的天气数据,其他更多数据需要佬们自行添加,见下方第二部分。

缘起

为了部署几个 API 方便自己使用,被论坛佬们拉进 OpenWebUI 坑里。不懂代码,但有了 AI 帮助,也能玩的不亦乐乎。

前言

OpenWebUI 的 工具 可以扩展很多功能,给 AI 一定赋能。(比如让AI读取时间,天气,增加记忆力,获取特定网络的讯息等等)

官方的工具页面,我们能看到很多工具。比如这次要说的 天气工具 。安装后,对任何LLM使用这个工具,只要问 地点 + 天气 这两个关键词,都能给出适合的回答。(视大模型能力,大多数中英文都可以。英文出错概率更低。)

** 我调优代码均为 Claude3.7 创造,我再辅助微调或修饰细节

分享的调优方法:

  1. 第一部分,增加默认地址。适合 不经常出远门的佬
  2. 第二部分,改变气象模型,选择数据,以 优化天气预报

第一部分 - 增加默认地址

此工具 默认调用方式地点 + 天气 + 具体时间。数据源是open-meteo.com ,建议到网站自行选取你需要的天气数据。英文看不懂?找沉浸式翻译。

  • 比如我要问“今天上海的天气怎样?”,或“广州未来7天天气怎样”,“现在成都的天气怎么样”。

我觉得常住一个地方,每次都要问一个固定地点,显得我很傻,我也懒得每次都输入,于是做出了修改:

  1. 检查源代码 第80行 - 97行 前后,找到这段代码,并添加默认城市名, 直接整段替换吧:backhand_index_pointing_down:
class Tools:
    def __init__(self):
        self.citation = True
        # 添加默认城市名,自行修改
        self.default_location = "城市名称拼音"
        pass
        
    def get_future_weather_week(self, city: str = None) -> str:
        """
        Get the weather for the next week for a given city.
        :param city: The name of the city to get the weather for. If None, uses default location.
        :return: The current weather information or an error message.
        """
        # 如果未提供城市,则使用默认位置
        if not city:
            city = self.default_location
            
        city_info = get_city_info(city)
        if not city_info:
            return f"""Could not find weather data for '{city}'. Please check the city name and try again."""

然后,接下来几行代码不动,直到下一个 def get_current_weather 字段,现在约在150行前后。找到def,if not,if not这几行,直接替换成这样就行了:backhand_index_pointing_down:

    def get_current_weather(self, city: str = None) -> str:
        """
        Get the current weather for a given city.
        :param city: The name of the city to get the weather for. If None, uses default location.
        :return: The current weather information or an error message.
        """
        if not city:
            city = self.default_location
            
        city_info = get_city_info(city)
        if not city_info:
            return """Error fetching weather data"""

这两段修改,第一个是修改默认地点,和“未来7天”的地理数据处理方式,第二个是修改“现在”的地理数据处理方式

已知问题

  • 我发现很多同拼音城市,需要大家自己验证能不能正确读取,可能需要加具体地点名字才能搜索?或者需要具体的坐标代码
  • 具体可以连代码一起发给Claude问问,似乎它能给出一套带有默认坐标系的代码修改方案来。

第二部分 - 调整天气模型 & 天气数据

1.1 原代码使用反人类的英制单位。必须调成公制才行。

  • 经研究发现,网站默认公制单位,英制单位是作者强行加入的:fearful:。只要删除作者强改的部分就可以了。

还是找到未来和当前天气的数据部分:def get_future_weather_week 部分,和get_current_weather 部分。

  1. 去掉 未来天气 中的英制单位,约在120行左右看到如下:
            "current": "temperature_2m",
            "timezone": tmzone,
            "temperature_unit": "fahrenheit",
            "wind_speed_unit": "mph",
            "precipitation_unit": "inch",
            "forecast_days": 7,

删改成这样:backhand_index_pointing_down:

            "current": "temperature_2m",
            "timezone": tmzone,
            "forecast_days": 7,
  1. 去掉 此刻天气 中的英制单位,约在177行左右,看到如下:
            "timezone": tmzone,
            "temperature_unit": "fahrenheit",
            "wind_speed_unit": "mph",
            "precipitation_unit": "inch",
            "forecast_days": 1,

删改成这样:backhand_index_pointing_down:

            "timezone": tmzone,
            "forecast_days": 1,

现在,询问天气,应该就是公制单位了。

1.2 接下来做天气模型选择和个人气象数据偏好部分修改:

  1. 关于天气模型,在网站的 Data Sources 部分有详细介绍,点这里查看。世界顶级模型有中国气象局的 CMA GRAPES Global ,欧洲气象局的 ECMWF ,和美国气象局的 GFS 。各位佬自己选择哈。
  • 手机 app 用 windy 习惯了,所以我选择 “ECMWF” 数据源。
    1. 找到 “Weather models,选择一个天气模型,并选择。

    2. 然后到了自助餐时刻。你喜欢看哪些天气数据?气温?云量?下不下雨?下多少雨?刮多大风?找到Daily Weather Variables选择每日预报的数据, 找到 Current Weather 选择想要的此刻的天气数据。此时你可能需要翻译辅助。

    3. 最后到下方的 API Response 部分,切换到 “Python” ,你会看到一段代码,找到 params 后面的字段,大概长这样:backhand_index_pointing_down::

params = {
	"latitude": 52.52,
	"longitude": 13.41,
	"daily": ["weather_code", "temperature_2m_max", "temperature_2m_min"],
	"models": "gfs_seamless",
	"current": ["temperature_2m", "relative_humidity_2m", "apparent_temperature"],

daily :获取未来每日预报需要的数据
current :获取此刻天气需要数据
models :所采用的气象预报模型

  1. 把这几段代码分别填到原代码中。
    比如,我采用“ECMWF”未来天气预报包含日出日落信息,气温,湿度,云量,等等,源代码未来天气 params 部分(约103行左右)被我改成这样(注意,latitudelongitudetimezone 不要动):
        params = {
            "latitude": lat,
            "longitude": lng,
            "models": "ecmwf_ifs025",
            "daily": [
                "weather_code",
                "sunrise",
                "sunset",
                "temperature_2m_max",
                "temperature_2m_min",
                "cloud_cover_mean",
                "relative_humidity_2m_mean",
                "precipitation_probability_max",
                "wind_speed_10m_max",
                "wind_gusts_10m_max",
                "wind_direction_10m_dominant",
            ],
            "current": "temperature_2m",
            "timezone": tmzone,
            "forecast_days": 7,
        }

当前天气部分(约170行左右),我改成这样:

        base_url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": lat,
            "longitude": lng,
            "models": "ecmwf_ifs025",
            "current": [
                "temperature_2m",
                "apparent_temperature",
                "relative_humidity_2m",
                "cloud_cover",
                "surface_pressure",
                "wind_speed_10m",
                "weather_code",
            ],
            "timezone": tmzone,
            "forecast_days": 1,
        }
  1. 最后,未来天气预报部分,需要调整输出表格(其实就是输出表格内的字被我们改了,现在重新建立下表格)。这部分是 mapped_data 字典(约132行左右)。
    1. 方法:把之前调整后的整段代码发给 Claude3.7,让他帮我修改mapped_data 部分。他几乎可以一次帮我们修改好。
    2. 如果你直接采用我的数据,那直接复制下面代码替换就好了。
        mapped_data = {
            date: {
                "weather_description": wmo_weather_codes[
                    str(data["daily"]["weather_code"][i])
                ],
                "sunrise_sunset": f'Sunrise: {format_date(data["daily"]["sunrise"][i])} / Sunset: {format_date(data["daily"]["sunset"][i])}',
                "temperature_max_min": f'{data["daily"]["temperature_2m_max"][i]} {data["daily_units"]["temperature_2m_max"]} / {data["daily"]["temperature_2m_min"][i]} {data["daily_units"]["temperature_2m_min"]}',
                "cloud_cover": f'{data["daily"]["cloud_cover_mean"][i]} {data["daily_units"]["cloud_cover_mean"]}',
                "humidity": f'{data["daily"]["relative_humidity_2m_mean"][i]} {data["daily_units"]["relative_humidity_2m_mean"]}',
                "precipitation_probability": f'{data["daily"]["precipitation_probability_max"][i]} {data["daily_units"]["precipitation_probability_max"]}',
                "wind": f'Speed: {data["daily"]["wind_speed_10m_max"][i]} {data["daily_units"]["wind_speed_10m_max"]} / Gusts: {data["daily"]["wind_gusts_10m_max"][i]} {data["daily_units"]["wind_gusts_10m_max"]} / Direction: {data["daily"]["wind_direction_10m_dominant"][i]} {data["daily_units"]["wind_direction_10m_dominant"]}',
            }
            for i, date in enumerate(data["daily"]["time"])
        }

补充说明

OpenWebUI 的工具调用,需要LLM模型支持。就是需要一个LLM去调用这个工具读取数据,把数据返回给正在对话的LLM。由这个LLM对数据进行对话处理。这会导致 API 多一次调用。如果你的API是计次的,要注意用量。一般选一个便宜的,速度快的LLM就好了。不太推荐带推理的模型去获取,太慢了。

具体在“管理员面板设置界面 → 设置任务模型” 设置。本地模型由ollama提供,外部模型由API选择。

另外,任务模型似乎不能用2API(我猜)?

8 个赞

不懂代码,佬友谦虚了,感谢分享

1 个赞

现在应该有这方面的MCP 了吧

我真的只在大学学过单片机汇编语言,那种机器语言之难以理解~直接把我踢出了编程世界。(在此向大学教学体制竖个中指)。后来毕业才尝试用C去编程等等,才发现“代码”也是可以看得懂的 :rofl:

:rofl:哈哈哈哈~那就更用不到我这个了。我就瞎折腾。 :tieba_087:

挺不错哈。继续可以弄个MCP 接口玩玩。哈哈:grinning_face_with_smiling_eyes:

1 个赞

感谢大佬。

1 个赞

太强了,谢谢佬分享

1 个赞

此话题已在最后回复的 30 天后被自动关闭。不再允许新回复。