from typing import Any from app.homelab.digest import build_weather_briefing from app.homelab.image_gen import generate_image as run_generate_image from app.homelab.openmeteo import OpenMeteoClient from app.tools._dispatch import NOT_HANDLED, ToolContext TOOL_NAMES = frozenset({ "get_weather", "get_morning_briefing", "generate_image", }) TOOL_DEFINITIONS: list[dict[str, Any]] = [ { "type": "function", "function": { "name": "get_weather", "description": ( "ОБЯЗАТЕЛЬНО для вопросов о погоде, «что на улице», «будет ли дождь», «завтра», «на неделю». " "Текущая погода, почасовой и дневной прогноз." ), "parameters": { "type": "object", "properties": { "hours_ahead": { "type": "integer", "description": "Сколько часов почасового прогноза (по умолчанию 12, до 168)", }, "days_ahead": { "type": "integer", "description": "Сколько дней дневного прогноза (по умолчанию 7, до 16)", }, }, "required": [], }, }, }, { "type": "function", "function": { "name": "get_morning_briefing", "description": "Утренний брифинг: погода и заголовки новостей.", "parameters": { "type": "object", "properties": { "include_news": { "type": "boolean", "description": "Включить новости (по умолчанию true)", }, }, "required": [], }, }, }, { "type": "function", "function": { "name": "generate_image", "description": ( "Аниме-картинка (Anima). draw_self=true — персонаж из карточки; " "scene_description — поза/кадр/одежда (booru-теги на англ. или короткий запрос: " "full body, sitting, apron). Можно оба параметра: draw_self + scene_description. " "Внешность только из appearance_tags карточки." ), "parameters": { "type": "object", "properties": { "draw_self": { "type": "boolean", "description": "Нарисовать персонажа из карточки", }, "scene_description": { "type": "string", "description": ( "Поза, кадр, одежда, обстановка — booru-теги или запрос " "(full_body, standing, apron, blush). С draw_self=true — уточняет сцену." ), }, }, "required": [], }, }, }, ] async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any: if name not in TOOL_NAMES: return NOT_HANDLED if name == "get_weather": hours = max(1, min(int(arguments.get("hours_ahead") or 12), 168)) days = max(1, min(int(arguments.get("days_ahead") or 7), 16)) client = OpenMeteoClient() weather = client.fetch_forecast(hours_ahead=hours, days_ahead=days) return { "weather": weather, "rain_summary": client.rain_summary(hours_ahead=hours, daily=weather.get("daily")) if weather.get("ok") else "", "daily_summary": client.daily_summary(days_ahead=days) if weather.get("ok") else "", } if name == "get_morning_briefing": include_news = arguments.get("include_news", True) return build_weather_briefing( hours_ahead=12, include_news=bool(include_news), ) if name == "generate_image": return await run_generate_image( ctx.db, user_id=ctx.user_id, session_id=ctx.session_id, draw_self=bool(arguments.get("draw_self")), scene_description=arguments.get("scene_description", ""), ) return NOT_HANDLED