smart tdee
This commit is contained in:
@@ -29,12 +29,19 @@ WEATHER_CODES: dict[int, str] = {
|
||||
99: "гроза с градом",
|
||||
}
|
||||
|
||||
WEATHER_QUERY_KEYWORDS = (
|
||||
"погод", "дожд", "снег", "ветер", "температур", "градус", "мороз", "жар",
|
||||
"на улице", "одеть", "зонт", "прогноз", "завтра", "послезавтра", "выходн",
|
||||
"weather", "rain", "forecast", "umbrella", "outside",
|
||||
)
|
||||
|
||||
_cache: dict[str, Any] = {
|
||||
"data": None,
|
||||
"fetched_at": 0.0,
|
||||
"expires_at": 0.0,
|
||||
"source": "local",
|
||||
"local_coverage": {"current": [], "hourly": []},
|
||||
"local_coverage": {"current": [], "hourly": [], "daily": []},
|
||||
"merged_fields": [],
|
||||
}
|
||||
|
||||
CURRENT_FIELDS = (
|
||||
@@ -51,6 +58,14 @@ HOURLY_FIELDS = (
|
||||
"precipitation",
|
||||
"weather_code",
|
||||
)
|
||||
DAILY_FIELDS = (
|
||||
"weather_code",
|
||||
"temperature_2m_max",
|
||||
"temperature_2m_min",
|
||||
"precipitation_sum",
|
||||
"precipitation_probability_max",
|
||||
"wind_speed_10m_max",
|
||||
)
|
||||
|
||||
RECOMMENDED_SYNC_DOMAINS = "dwd_icon,ncep_gfs013,ncep_gefs025"
|
||||
RECOMMENDED_SYNC_VARIABLES = (
|
||||
@@ -58,11 +73,20 @@ RECOMMENDED_SYNC_VARIABLES = (
|
||||
"precipitation,rain,cloud_cover,weather_code,wind_u_component_10m,wind_v_component_10m"
|
||||
)
|
||||
SYNC_HINT = (
|
||||
"Контейнер open-meteo-sync, скорее всего, качает только temperature_2m. "
|
||||
f"Задай SYNC_DOMAINS={RECOMMENDED_SYNC_DOMAINS} и "
|
||||
"Локальный open-meteo-sync отдаёт неполные данные. "
|
||||
f"SYNC_DOMAINS={RECOMMENDED_SYNC_DOMAINS} "
|
||||
f"SYNC_VARIABLES={RECOMMENDED_SYNC_VARIABLES} (~12 GB). "
|
||||
"Документация: github.com/open-meteo/open-data/tree/main/tutorial_weather_api"
|
||||
)
|
||||
PRECIP_PROB_HINT = (
|
||||
"Для вероятности дождя добавь ncep_gefs025 в SYNC_DOMAINS "
|
||||
"и precipitation_probability в SYNC_VARIABLES."
|
||||
)
|
||||
|
||||
|
||||
def weather_query_relevant(query: str) -> bool:
|
||||
q = (query or "").lower()
|
||||
return any(kw in q for kw in WEATHER_QUERY_KEYWORDS)
|
||||
|
||||
|
||||
def _hourly_series(hourly: dict[str, Any], key: str) -> list[Any]:
|
||||
@@ -70,6 +94,11 @@ def _hourly_series(hourly: dict[str, Any], key: str) -> list[Any]:
|
||||
return values if isinstance(values, list) else []
|
||||
|
||||
|
||||
def _daily_series(daily: dict[str, Any], key: str) -> list[Any]:
|
||||
values = daily.get(key)
|
||||
return values if isinstance(values, list) else []
|
||||
|
||||
|
||||
def _hourly_start_index(times: list[str], anchor_time: str | None) -> int:
|
||||
if not times:
|
||||
return 0
|
||||
@@ -85,18 +114,21 @@ def _hourly_start_index(times: list[str], anchor_time: str | None) -> int:
|
||||
|
||||
|
||||
def _field_coverage(raw: dict[str, Any]) -> dict[str, list[str]]:
|
||||
"""Какие поля реально пришли от OpenMeteo (не null)."""
|
||||
current = raw.get("current") or {}
|
||||
hourly = raw.get("hourly") or {}
|
||||
current_present = [
|
||||
key for key in CURRENT_FIELDS if current.get(key) is not None
|
||||
]
|
||||
daily = raw.get("daily") or {}
|
||||
current_present = [key for key in CURRENT_FIELDS if current.get(key) is not None]
|
||||
hourly_present = []
|
||||
for key in HOURLY_FIELDS:
|
||||
series = _hourly_series(hourly, key)
|
||||
if any(v is not None for v in series):
|
||||
hourly_present.append(key)
|
||||
return {"current": current_present, "hourly": hourly_present}
|
||||
daily_present = []
|
||||
for key in DAILY_FIELDS:
|
||||
series = _daily_series(daily, key)
|
||||
if any(v is not None for v in series):
|
||||
daily_present.append(key)
|
||||
return {"current": current_present, "hourly": hourly_present, "daily": daily_present}
|
||||
|
||||
|
||||
def _coverage_sufficient(coverage: dict[str, list[str]]) -> bool:
|
||||
@@ -106,11 +138,27 @@ def _coverage_sufficient(coverage: dict[str, list[str]]) -> bool:
|
||||
return False
|
||||
if len(current) < 3:
|
||||
return False
|
||||
if "precipitation_probability" not in hourly and "weather_code" not in hourly:
|
||||
if "weather_code" not in hourly and "temperature_2m" not in hourly:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _local_needs_sync_hint(local_coverage: dict[str, list[str]]) -> bool:
|
||||
current = set(local_coverage.get("current") or [])
|
||||
hourly = set(local_coverage.get("hourly") or [])
|
||||
if "temperature_2m" not in current:
|
||||
return True
|
||||
if "weather_code" not in current:
|
||||
return True
|
||||
if "temperature_2m" not in hourly:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _missing_precip_probability(coverage: dict[str, list[str]]) -> bool:
|
||||
return "precipitation_probability" not in set(coverage.get("hourly") or [])
|
||||
|
||||
|
||||
def _fmt_num(value: Any, *, suffix: str = "") -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
@@ -121,6 +169,42 @@ def _fmt_num(value: Any, *, suffix: str = "") -> str:
|
||||
return f"{text}{suffix}" if suffix else text
|
||||
|
||||
|
||||
def _conditions(code: Any) -> str:
|
||||
if code is None:
|
||||
return "неизвестно"
|
||||
return WEATHER_CODES.get(int(code), "неизвестно")
|
||||
|
||||
|
||||
def _format_day_label(date_str: str, index: int) -> str:
|
||||
if index == 0:
|
||||
return "Сегодня"
|
||||
if index == 1:
|
||||
return "Завтра"
|
||||
if not date_str:
|
||||
return f"День {index + 1}"
|
||||
parts = date_str.split("-")
|
||||
if len(parts) == 3:
|
||||
return f"{parts[2]}.{parts[1]}"
|
||||
return date_str
|
||||
|
||||
|
||||
def _merge_hourly_field(target: dict[str, Any], source: dict[str, Any], field: str) -> bool:
|
||||
hourly_t = target.setdefault("hourly", {})
|
||||
hourly_s = source.get("hourly") or {}
|
||||
src = hourly_s.get(field)
|
||||
if not isinstance(src, list) or not any(v is not None for v in src):
|
||||
return False
|
||||
dst = hourly_t.get(field)
|
||||
if isinstance(dst, list) and len(dst) == len(src):
|
||||
hourly_t[field] = [
|
||||
dst[i] if dst[i] is not None else src[i]
|
||||
for i in range(len(src))
|
||||
]
|
||||
else:
|
||||
hourly_t[field] = src
|
||||
return True
|
||||
|
||||
|
||||
class OpenMeteoClient:
|
||||
def __init__(self) -> None:
|
||||
settings = get_settings()
|
||||
@@ -131,6 +215,7 @@ class OpenMeteoClient:
|
||||
self.lon = settings.weather_lon
|
||||
self.location_name = settings.weather_location_name
|
||||
self.cache_ttl = settings.weather_cache_sec
|
||||
self.forecast_days = max(2, int(settings.weather_forecast_days or 7))
|
||||
|
||||
def _request_params(self) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -138,8 +223,9 @@ class OpenMeteoClient:
|
||||
"longitude": self.lon,
|
||||
"current": ",".join(CURRENT_FIELDS),
|
||||
"hourly": ",".join(HOURLY_FIELDS),
|
||||
"daily": ",".join(DAILY_FIELDS),
|
||||
"timezone": "auto",
|
||||
"forecast_days": 2,
|
||||
"forecast_days": self.forecast_days,
|
||||
}
|
||||
|
||||
def _fetch_from_url(self, base_url: str) -> dict[str, Any]:
|
||||
@@ -157,18 +243,26 @@ class OpenMeteoClient:
|
||||
local_coverage = _field_coverage(local_raw)
|
||||
source = "local"
|
||||
raw = local_raw
|
||||
merged_fields: list[str] = []
|
||||
|
||||
if (
|
||||
need_fallback = (
|
||||
self.fallback_on_partial
|
||||
and self.fallback_url
|
||||
and self.fallback_url.rstrip("/") != self.base_url
|
||||
and not _coverage_sufficient(local_coverage)
|
||||
):
|
||||
)
|
||||
|
||||
if need_fallback:
|
||||
try:
|
||||
fallback_raw = self._fetch_from_url(self.fallback_url)
|
||||
if _coverage_sufficient(_field_coverage(fallback_raw)):
|
||||
fallback_coverage = _field_coverage(fallback_raw)
|
||||
|
||||
if not _coverage_sufficient(local_coverage) and _coverage_sufficient(fallback_coverage):
|
||||
raw = fallback_raw
|
||||
source = "fallback"
|
||||
elif _missing_precip_probability(local_coverage) and not _missing_precip_probability(fallback_coverage):
|
||||
if _merge_hourly_field(raw, fallback_raw, "precipitation_probability"):
|
||||
merged_fields.append("precipitation_probability")
|
||||
source = "merged"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -177,6 +271,7 @@ class OpenMeteoClient:
|
||||
_cache["expires_at"] = now + self.cache_ttl
|
||||
_cache["source"] = source
|
||||
_cache["local_coverage"] = local_coverage
|
||||
_cache["merged_fields"] = merged_fields
|
||||
return raw
|
||||
|
||||
def cache_status(self) -> dict[str, Any]:
|
||||
@@ -194,43 +289,78 @@ class OpenMeteoClient:
|
||||
"ttl_sec": self.cache_ttl,
|
||||
"expires_in_sec": expires_in_sec,
|
||||
"source": _cache.get("source") or "local",
|
||||
"merged_fields": list(_cache.get("merged_fields") or []),
|
||||
}
|
||||
|
||||
def fetch_current_and_hourly(self, hours_ahead: int = 12) -> dict[str, Any]:
|
||||
def _build_hourly_slice(self, raw: dict[str, Any], hours_ahead: int) -> list[dict[str, Any]]:
|
||||
current = raw.get("current") or {}
|
||||
hourly = raw.get("hourly") or {}
|
||||
times = hourly.get("time") or []
|
||||
start = _hourly_start_index(times, current.get("time"))
|
||||
end = min(start + hours_ahead, len(times))
|
||||
rows: list[dict[str, Any]] = []
|
||||
for i in range(start, end):
|
||||
code = _hourly_series(hourly, "weather_code")[i] if i < len(_hourly_series(hourly, "weather_code")) else None
|
||||
temp_series = _hourly_series(hourly, "temperature_2m")
|
||||
precip_series = _hourly_series(hourly, "precipitation")
|
||||
prob_series = _hourly_series(hourly, "precipitation_probability")
|
||||
rows.append({
|
||||
"time": times[i],
|
||||
"temperature_c": temp_series[i] if i < len(temp_series) else None,
|
||||
"precipitation_mm": precip_series[i] if i < len(precip_series) else None,
|
||||
"precipitation_probability": prob_series[i] if i < len(prob_series) else None,
|
||||
"weather_code": code,
|
||||
"conditions": _conditions(code),
|
||||
})
|
||||
return rows
|
||||
|
||||
def _build_daily_slice(self, raw: dict[str, Any], days_ahead: int) -> list[dict[str, Any]]:
|
||||
daily = raw.get("daily") or {}
|
||||
times = daily.get("time") or []
|
||||
limit = min(days_ahead, len(times))
|
||||
rows: list[dict[str, Any]] = []
|
||||
for i in range(limit):
|
||||
code = _daily_series(daily, "weather_code")[i] if i < len(_daily_series(daily, "weather_code")) else None
|
||||
rows.append({
|
||||
"date": times[i],
|
||||
"label": _format_day_label(times[i], i),
|
||||
"temperature_max_c": _daily_series(daily, "temperature_2m_max")[i] if i < len(_daily_series(daily, "temperature_2m_max")) else None,
|
||||
"temperature_min_c": _daily_series(daily, "temperature_2m_min")[i] if i < len(_daily_series(daily, "temperature_2m_min")) else None,
|
||||
"precipitation_sum_mm": _daily_series(daily, "precipitation_sum")[i] if i < len(_daily_series(daily, "precipitation_sum")) else None,
|
||||
"precipitation_probability_max": _daily_series(daily, "precipitation_probability_max")[i] if i < len(_daily_series(daily, "precipitation_probability_max")) else None,
|
||||
"wind_speed_max_kmh": _daily_series(daily, "wind_speed_10m_max")[i] if i < len(_daily_series(daily, "wind_speed_10m_max")) else None,
|
||||
"weather_code": code,
|
||||
"conditions": _conditions(code),
|
||||
})
|
||||
return rows
|
||||
|
||||
def fetch_forecast(self, hours_ahead: int = 12, days_ahead: int = 7) -> dict[str, Any]:
|
||||
hours_ahead = max(1, min(int(hours_ahead), 168))
|
||||
days_ahead = max(1, min(int(days_ahead), self.forecast_days))
|
||||
try:
|
||||
raw = self._fetch_raw()
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": str(exc), "location": self.location_name}
|
||||
|
||||
current = raw.get("current") or {}
|
||||
hourly = raw.get("hourly") or {}
|
||||
times = hourly.get("time") or []
|
||||
start = _hourly_start_index(times, current.get("time"))
|
||||
end = min(start + hours_ahead, len(times))
|
||||
hourly_slice = []
|
||||
for i in range(start, end):
|
||||
code = _hourly_series(hourly, "weather_code")[i] if i < len(_hourly_series(hourly, "weather_code")) else None
|
||||
temp_series = _hourly_series(hourly, "temperature_2m")
|
||||
precip_series = _hourly_series(hourly, "precipitation")
|
||||
prob_series = _hourly_series(hourly, "precipitation_probability")
|
||||
hourly_slice.append({
|
||||
"time": times[i],
|
||||
"temperature_c": temp_series[i] if i < len(temp_series) else None,
|
||||
"precipitation_mm": precip_series[i] if i < len(precip_series) else None,
|
||||
"precipitation_probability": prob_series[i] if i < len(prob_series) else None,
|
||||
"weather_code": code,
|
||||
"conditions": WEATHER_CODES.get(code, "неизвестно") if code is not None else "неизвестно",
|
||||
})
|
||||
|
||||
code = current.get("weather_code")
|
||||
coverage = _field_coverage(raw)
|
||||
local_coverage = _cache.get("local_coverage") or coverage
|
||||
|
||||
sync_hint = ""
|
||||
if _local_needs_sync_hint(local_coverage):
|
||||
sync_hint = SYNC_HINT
|
||||
elif _missing_precip_probability(local_coverage):
|
||||
sync_hint = PRECIP_PROB_HINT
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"location": self.location_name,
|
||||
"data_source": _cache.get("source") or "local",
|
||||
"local_field_coverage": _cache.get("local_coverage") or coverage,
|
||||
"merged_fields": list(_cache.get("merged_fields") or []),
|
||||
"local_field_coverage": local_coverage,
|
||||
"field_coverage": coverage,
|
||||
"sync_hint": SYNC_HINT if not _coverage_sufficient(_cache.get("local_coverage") or coverage) else "",
|
||||
"sync_hint": sync_hint,
|
||||
"current": {
|
||||
"time": current.get("time"),
|
||||
"temperature_c": current.get("temperature_2m"),
|
||||
@@ -239,13 +369,17 @@ class OpenMeteoClient:
|
||||
"precipitation_mm": current.get("precipitation"),
|
||||
"wind_speed_kmh": current.get("wind_speed_10m"),
|
||||
"weather_code": code,
|
||||
"conditions": WEATHER_CODES.get(code, "неизвестно") if code is not None else "неизвестно",
|
||||
"conditions": _conditions(code),
|
||||
},
|
||||
"hourly": hourly_slice,
|
||||
"hourly": self._build_hourly_slice(raw, hours_ahead),
|
||||
"daily": self._build_daily_slice(raw, days_ahead),
|
||||
}
|
||||
|
||||
def rain_summary(self, hours_ahead: int = 12) -> str:
|
||||
data = self.fetch_current_and_hourly(hours_ahead=hours_ahead)
|
||||
def fetch_current_and_hourly(self, hours_ahead: int = 12) -> dict[str, Any]:
|
||||
return self.fetch_forecast(hours_ahead=hours_ahead, days_ahead=min(7, self.forecast_days))
|
||||
|
||||
def rain_summary(self, hours_ahead: int = 12, daily: list[dict[str, Any]] | None = None) -> str:
|
||||
data = self.fetch_forecast(hours_ahead=hours_ahead, days_ahead=2)
|
||||
if not data.get("ok"):
|
||||
return f"Погода недоступна: {data.get('error', 'ошибка')}"
|
||||
|
||||
@@ -255,16 +389,49 @@ class OpenMeteoClient:
|
||||
precip = hour.get("precipitation_mm") or 0
|
||||
if (prob is not None and prob >= 40) or precip > 0:
|
||||
time_str = (hour.get("time") or "")[11:16]
|
||||
rainy_hours.append(f"{time_str} ({prob}% вероятность, {precip} мм)")
|
||||
prob_text = f"{prob}%" if prob is not None else "—"
|
||||
rainy_hours.append(f"{time_str} ({prob_text}, {precip} мм)")
|
||||
|
||||
lines: list[str] = []
|
||||
if rainy_hours:
|
||||
return "Ожидаются осадки: " + ", ".join(rainy_hours[:6])
|
||||
return "Существенных осадков в ближайшие часы не ожидается."
|
||||
lines.append("Ожидаются осадки: " + ", ".join(rainy_hours[:6]))
|
||||
else:
|
||||
lines.append("Существенных осадков в ближайшие часы не ожидается.")
|
||||
|
||||
days = daily if daily is not None else data.get("daily") or []
|
||||
if len(days) > 1:
|
||||
tomorrow = days[1]
|
||||
tmax = tomorrow.get("temperature_max_c")
|
||||
tmin = tomorrow.get("temperature_min_c")
|
||||
prob = tomorrow.get("precipitation_probability_max")
|
||||
precip = tomorrow.get("precipitation_sum_mm") or 0
|
||||
cond = tomorrow.get("conditions") or "неизвестно"
|
||||
prob_part = f", дождь до {prob}%" if prob is not None and prob >= 30 else ""
|
||||
precip_part = f", {precip} мм" if precip > 0 else ""
|
||||
lines.append(
|
||||
f"Завтра: {_fmt_num(tmin)}–{_fmt_num(tmax, suffix='°C')}, {cond}{prob_part}{precip_part}."
|
||||
)
|
||||
return " ".join(lines)
|
||||
|
||||
def daily_summary(self, days_ahead: int = 7) -> str:
|
||||
data = self.fetch_forecast(hours_ahead=1, days_ahead=days_ahead)
|
||||
if not data.get("ok"):
|
||||
return ""
|
||||
parts = []
|
||||
for day in data.get("daily") or []:
|
||||
label = day.get("label") or day.get("date")
|
||||
tmax = day.get("temperature_max_c")
|
||||
tmin = day.get("temperature_min_c")
|
||||
cond = day.get("conditions") or "неизвестно"
|
||||
prob = day.get("precipitation_probability_max")
|
||||
prob_part = f", дождь до {prob}%" if prob is not None and prob >= 30 else ""
|
||||
parts.append(f"{label}: {_fmt_num(tmin)}–{_fmt_num(tmax, suffix='°C')}, {cond}{prob_part}")
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def format_weather_snapshot(data: dict[str, Any] | None = None) -> str:
|
||||
def format_weather_snapshot(data: dict[str, Any] | None = None, *, include_daily: bool = True) -> str:
|
||||
client = OpenMeteoClient()
|
||||
snapshot = data if data is not None else client.fetch_current_and_hourly(hours_ahead=6)
|
||||
snapshot = data if data is not None else client.fetch_forecast(hours_ahead=6, days_ahead=3)
|
||||
|
||||
lines = ["[Погода]"]
|
||||
if not snapshot.get("ok"):
|
||||
@@ -281,30 +448,50 @@ def format_weather_snapshot(data: dict[str, Any] | None = None) -> str:
|
||||
f"{snapshot.get('location')}: {_fmt_num(cur.get('temperature_c'), suffix='°C')}"
|
||||
f"{apparent_part}, {cur.get('conditions') or 'неизвестно'}{wind_part}."
|
||||
)
|
||||
hourly = snapshot.get("hourly") or []
|
||||
|
||||
rainy_hours = []
|
||||
for hour in hourly:
|
||||
for hour in snapshot.get("hourly") or []:
|
||||
prob = hour.get("precipitation_probability")
|
||||
precip = hour.get("precipitation_mm") or 0
|
||||
if (prob is not None and prob >= 40) or precip > 0:
|
||||
time_str = (hour.get("time") or "")[11:16]
|
||||
rainy_hours.append(f"{time_str} ({prob}% вероятность, {precip} мм)")
|
||||
prob_text = f"{prob}%" if prob is not None else "—"
|
||||
rainy_hours.append(f"{time_str} ({prob_text}, {precip} мм)")
|
||||
if rainy_hours:
|
||||
lines.append("Ожидаются осадки: " + ", ".join(rainy_hours[:6]))
|
||||
else:
|
||||
lines.append("Существенных осадков в ближайшие часы не ожидается.")
|
||||
lines.append("Вопросы «что на улице» / «будет ли дождь» — get_weather.")
|
||||
|
||||
if include_daily:
|
||||
days = snapshot.get("daily") or []
|
||||
if len(days) > 1:
|
||||
tomorrow = days[1]
|
||||
lines.append(
|
||||
f"Завтра: {_fmt_num(tomorrow.get('temperature_min_c'))}–"
|
||||
f"{_fmt_num(tomorrow.get('temperature_max_c'), suffix='°C')}, "
|
||||
f"{tomorrow.get('conditions') or 'неизвестно'}."
|
||||
)
|
||||
if len(days) > 2:
|
||||
week_bits = []
|
||||
for day in days[2:7]:
|
||||
week_bits.append(
|
||||
f"{day.get('label')}: {_fmt_num(day.get('temperature_min_c'))}–"
|
||||
f"{_fmt_num(day.get('temperature_max_c'), suffix='°C')}"
|
||||
)
|
||||
if week_bits:
|
||||
lines.append("Далее: " + "; ".join(week_bits) + ".")
|
||||
|
||||
lines.append("Подробнее — get_weather (hours_ahead, days_ahead).")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_weather_dashboard(hours_ahead: int = 12) -> dict[str, Any]:
|
||||
"""Полный снимок для UI: данные OpenMeteo + контекст ассистента."""
|
||||
def build_weather_dashboard(hours_ahead: int = 12, days_ahead: int = 7) -> dict[str, Any]:
|
||||
client = OpenMeteoClient()
|
||||
weather = client.fetch_current_and_hourly(hours_ahead=hours_ahead)
|
||||
settings = get_settings()
|
||||
weather = client.fetch_forecast(hours_ahead=hours_ahead, days_ahead=days_ahead)
|
||||
return {
|
||||
"weather": weather,
|
||||
"rain_summary": client.rain_summary(hours_ahead=hours_ahead) if weather.get("ok") else "",
|
||||
"rain_summary": client.rain_summary(hours_ahead=hours_ahead, daily=weather.get("daily")) if weather.get("ok") else "",
|
||||
"daily_summary": client.daily_summary(days_ahead=days_ahead) if weather.get("ok") else "",
|
||||
"assistant_context": format_weather_snapshot(weather),
|
||||
"cache": client.cache_status(),
|
||||
"config": {
|
||||
@@ -313,24 +500,26 @@ def build_weather_dashboard(hours_ahead: int = 12) -> dict[str, Any]:
|
||||
"longitude": client.lon,
|
||||
"openmeteo_base_url": client.base_url,
|
||||
"cache_ttl_sec": client.cache_ttl,
|
||||
"forecast_days": 2,
|
||||
"forecast_days": client.forecast_days,
|
||||
"timezone": "auto",
|
||||
},
|
||||
"available_fields": {
|
||||
"current": list(CURRENT_FIELDS),
|
||||
"hourly": list(HOURLY_FIELDS),
|
||||
"daily": list(DAILY_FIELDS),
|
||||
},
|
||||
"field_coverage": weather.get("field_coverage") if weather.get("ok") else {"current": [], "hourly": []},
|
||||
"local_field_coverage": weather.get("local_field_coverage") if weather.get("ok") else {"current": [], "hourly": []},
|
||||
"field_coverage": weather.get("field_coverage") if weather.get("ok") else {"current": [], "hourly": [], "daily": []},
|
||||
"local_field_coverage": weather.get("local_field_coverage") if weather.get("ok") else {"current": [], "hourly": [], "daily": []},
|
||||
"data_source": weather.get("data_source", "local") if weather.get("ok") else "local",
|
||||
"merged_fields": weather.get("merged_fields", []) if weather.get("ok") else [],
|
||||
"sync_hint": weather.get("sync_hint", "") if weather.get("ok") else SYNC_HINT,
|
||||
"recommended_sync": {
|
||||
"domains": RECOMMENDED_SYNC_DOMAINS,
|
||||
"variables": RECOMMENDED_SYNC_VARIABLES,
|
||||
},
|
||||
"assistant_tools": {
|
||||
"get_weather": "Текущая погода и почасовой прогнос (hours_ahead до 48)",
|
||||
"get_weather": "Сейчас + почасово (hours_ahead до 168) + по дням (days_ahead до 16)",
|
||||
"get_morning_briefing": "Погода + заголовки RSS-новостей",
|
||||
},
|
||||
"system_prompt": "Краткий блок [Погода] в system prompt каждого сообщения (6 ч почасово).",
|
||||
"system_prompt": "Блок [Погода] в system prompt — только если запрос про погоду/одежду/прогноз.",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user