refactor
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
NOT_HANDLED: Any = object()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolContext:
|
||||
db: Session
|
||||
user_id: int
|
||||
session_id: int | None
|
||||
@@ -0,0 +1,37 @@
|
||||
from typing import Any
|
||||
|
||||
from app.rag.retriever import retrieve_document_chunks
|
||||
from app.tools._dispatch import NOT_HANDLED, ToolContext
|
||||
|
||||
TOOL_NAMES = frozenset({"search_documents"})
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_documents",
|
||||
"description": "Семантический поиск по загруженным документам (RAG).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Поисковый запрос"},
|
||||
"limit": {"type": "integer", "description": "Макс. фрагментов"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any:
|
||||
if name not in TOOL_NAMES:
|
||||
return NOT_HANDLED
|
||||
|
||||
if name == "search_documents":
|
||||
return await retrieve_document_chunks(
|
||||
arguments.get("query", ""),
|
||||
user_id=ctx.user_id,
|
||||
top_k=int(arguments.get("limit") or 6),
|
||||
)
|
||||
return NOT_HANDLED
|
||||
@@ -0,0 +1,403 @@
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from app.fitness.service import FitnessService
|
||||
from app.fitness.structuring import structure_meal, structure_workout
|
||||
from app.integrations.openfoodfacts import OpenFoodFactsClient
|
||||
from app.integrations.wger import WgerClient
|
||||
from app.tools._dispatch import NOT_HANDLED, ToolContext
|
||||
|
||||
TOOL_NAMES = frozenset({
|
||||
"get_fitness_summary",
|
||||
"get_fitness_history",
|
||||
"set_fitness_profile",
|
||||
"calc_fitness_targets",
|
||||
"calc_body_composition",
|
||||
"log_meal",
|
||||
"log_water",
|
||||
"log_weight",
|
||||
"log_steps",
|
||||
"log_workout",
|
||||
"lookup_food",
|
||||
"lookup_exercise",
|
||||
"set_fitness_reminder",
|
||||
})
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_fitness_summary",
|
||||
"description": (
|
||||
"Сводка фитнеса за день: ккал, БЖУ, вода, еда, тренировки. "
|
||||
"Без даты — сегодня; date=YYYY-MM-DD или days_ago=1 (вчера)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {"type": "string", "description": "Дата YYYY-MM-DD"},
|
||||
"days_ago": {
|
||||
"type": "integer",
|
||||
"description": "0 сегодня, 1 вчера, 2 позавчера…",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_fitness_history",
|
||||
"description": (
|
||||
"Краткая история за несколько дней (ккал, вода, тренировки по дням). "
|
||||
"«На прошлой неделе», «за 7 дней»."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {"type": "integer", "description": "Сколько дней, по умолчанию 7"},
|
||||
"end_date": {"type": "string", "description": "Конец периода YYYY-MM-DD, по умолчанию сегодня"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "set_fitness_profile",
|
||||
"description": "Настроить фитнес-профиль и пересчитать цели ккал/БЖУ/воды (TDEE = BMR + NEAT).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sex": {"type": "string", "description": "male/female"},
|
||||
"age": {"type": "integer"},
|
||||
"height_cm": {"type": "number"},
|
||||
"weight_kg": {"type": "number"},
|
||||
"goal": {"type": "string", "description": "lose/maintain/gain"},
|
||||
"target_weight_kg": {"type": "number"},
|
||||
"neat_base_kcal": {
|
||||
"type": "number",
|
||||
"description": "NEAT-база 200–300 ккал, по умолчанию 200",
|
||||
},
|
||||
"activity_level": {
|
||||
"type": "string",
|
||||
"description": "sedentary/moderate/active/very_active — fallback для TDEE план",
|
||||
},
|
||||
"weekly_workouts": {
|
||||
"type": "integer",
|
||||
"description": "Тренировок в неделю для fallback TDEE план",
|
||||
},
|
||||
"baseline_steps": {
|
||||
"type": "integer",
|
||||
"description": "Ожидаемые шаги/день (fallback TDEE план)",
|
||||
},
|
||||
"baseline_workout_kcal": {
|
||||
"type": "number",
|
||||
"description": "Ожидаемые ккал тренировок в неделю (fallback TDEE план)",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calc_fitness_targets",
|
||||
"description": "Калькулятор BMR/TDEE/макросов без сохранения (rest-day: BMR + NEAT).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sex": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
"height_cm": {"type": "number"},
|
||||
"weight_kg": {"type": "number"},
|
||||
"goal": {"type": "string"},
|
||||
"neat_base_kcal": {"type": "number"},
|
||||
"steps": {"type": "integer", "description": "Шаги за день для расчёта TDEE"},
|
||||
},
|
||||
"required": ["weight_kg", "height_cm", "age"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calc_body_composition",
|
||||
"description": (
|
||||
"Navy-калькулятор % жира, WHR, LBM, FFMI без сохранения. "
|
||||
"Пол/рост/вес из профиля, если не указаны."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sex": {"type": "string"},
|
||||
"height_cm": {"type": "number"},
|
||||
"weight_kg": {"type": "number"},
|
||||
"neck_cm": {"type": "number"},
|
||||
"waist_cm": {"type": "number"},
|
||||
"hip_cm": {"type": "number"},
|
||||
"body_fat_pct": {"type": "number"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_meal",
|
||||
"description": "Записать приём пищи. LLM оценит ккал и БЖУ из текста.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "description": "Что съел"},
|
||||
"meal_type": {"type": "string"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_water",
|
||||
"description": "Записать воду в мл.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount_ml": {"type": "integer"},
|
||||
},
|
||||
"required": ["amount_ml"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_weight",
|
||||
"description": (
|
||||
"Записать антропометрию: вес и обхваты (см). "
|
||||
"При neck+waist(+hip для женщин) автоматически считается Navy % жира."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"weight_kg": {"type": "number"},
|
||||
"body_fat_pct": {"type": "number"},
|
||||
"neck_cm": {"type": "number"},
|
||||
"waist_cm": {"type": "number"},
|
||||
"hip_cm": {"type": "number"},
|
||||
"chest_cm": {"type": "number"},
|
||||
"notes": {"type": "string"},
|
||||
"date": {"type": "string"},
|
||||
"days_ago": {"type": "integer"},
|
||||
},
|
||||
"required": ["weight_kg"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_steps",
|
||||
"description": "Записать шаги (можно задним числом: date или days_ago).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"steps": {"type": "integer"},
|
||||
"active_calories": {"type": "number"},
|
||||
"notes": {"type": "string"},
|
||||
"date": {"type": "string"},
|
||||
"days_ago": {"type": "integer"},
|
||||
},
|
||||
"required": ["steps"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_workout",
|
||||
"description": "Записать тренировку из текста (date/days_ago для прошлых дней).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string"},
|
||||
"date": {"type": "string"},
|
||||
"days_ago": {"type": "integer"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_food",
|
||||
"description": "Поиск продукта в Open Food Facts (ккал на 100г).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_exercise",
|
||||
"description": "Поиск упражнения в базе wger.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "set_fitness_reminder",
|
||||
"description": "Вкл/выкл или настроить напоминание: water, meal, workout, weigh_in.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {"type": "string"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"hour": {"type": "integer"},
|
||||
"minute": {"type": "integer"},
|
||||
"interval_hours": {"type": "integer"},
|
||||
},
|
||||
"required": ["kind"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any:
|
||||
if name not in TOOL_NAMES:
|
||||
return NOT_HANDLED
|
||||
|
||||
fitness = FitnessService(ctx.db, ctx.user_id)
|
||||
|
||||
if name == "get_fitness_summary":
|
||||
day: date | None = None
|
||||
if arguments.get("date"):
|
||||
day = date.fromisoformat(str(arguments["date"]))
|
||||
elif arguments.get("days_ago") is not None:
|
||||
day = datetime.now(timezone.utc).date() - timedelta(days=int(arguments["days_ago"]))
|
||||
return fitness.get_daily_summary(day)
|
||||
if name == "get_fitness_history":
|
||||
end_day = None
|
||||
if arguments.get("end_date"):
|
||||
end_day = date.fromisoformat(str(arguments["end_date"]))
|
||||
return fitness.get_history(
|
||||
days=int(arguments.get("days") or 7),
|
||||
end_day=end_day,
|
||||
)
|
||||
if name == "set_fitness_profile":
|
||||
updates = {
|
||||
k: arguments[k]
|
||||
for k in (
|
||||
"sex", "age", "height_cm", "weight_kg",
|
||||
"goal", "target_weight_kg", "neat_base_kcal",
|
||||
"activity_level", "weekly_workouts",
|
||||
"baseline_steps", "baseline_workout_kcal",
|
||||
)
|
||||
if k in arguments and arguments[k] is not None
|
||||
}
|
||||
return fitness.set_profile(updates)
|
||||
if name == "calc_fitness_targets":
|
||||
from app.fitness.calculators import compute_daily_targets
|
||||
|
||||
steps = int(arguments.get("steps") or 0)
|
||||
return compute_daily_targets(arguments, steps_total=steps, workouts=[])
|
||||
if name == "calc_body_composition":
|
||||
return fitness.calc_body_composition(arguments)
|
||||
if name == "log_meal":
|
||||
structured = await structure_meal(arguments.get("text", ""))
|
||||
return fitness.log_meal(
|
||||
description=structured.get("description") or arguments.get("text", ""),
|
||||
meal_type=arguments.get("meal_type") or structured.get("meal_type") or "snack",
|
||||
calories=float(structured.get("calories") or 0),
|
||||
protein_g=float(structured.get("protein_g") or 0),
|
||||
fat_g=float(structured.get("fat_g") or 0),
|
||||
carbs_g=float(structured.get("carbs_g") or 0),
|
||||
source="llm",
|
||||
estimated=True,
|
||||
)
|
||||
if name == "log_water":
|
||||
return fitness.log_water(int(arguments.get("amount_ml", 250)))
|
||||
if name == "log_weight":
|
||||
day = None
|
||||
if arguments.get("date"):
|
||||
day = date.fromisoformat(str(arguments["date"]))
|
||||
return fitness.log_weight(
|
||||
float(arguments["weight_kg"]),
|
||||
body_fat_pct=arguments.get("body_fat_pct"),
|
||||
chest_cm=arguments.get("chest_cm"),
|
||||
waist_cm=arguments.get("waist_cm"),
|
||||
neck_cm=arguments.get("neck_cm"),
|
||||
hip_cm=arguments.get("hip_cm"),
|
||||
notes=arguments.get("notes", ""),
|
||||
day=day,
|
||||
days_ago=arguments.get("days_ago"),
|
||||
)
|
||||
if name == "log_steps":
|
||||
day = None
|
||||
if arguments.get("date"):
|
||||
day = date.fromisoformat(str(arguments["date"]))
|
||||
return fitness.log_steps(
|
||||
int(arguments.get("steps") or 0),
|
||||
active_calories=arguments.get("active_calories"),
|
||||
notes=arguments.get("notes", ""),
|
||||
day=day,
|
||||
days_ago=arguments.get("days_ago"),
|
||||
)
|
||||
if name == "log_workout":
|
||||
structured = await structure_workout(arguments.get("text", ""))
|
||||
day = None
|
||||
if arguments.get("date"):
|
||||
day = date.fromisoformat(str(arguments["date"]))
|
||||
return fitness.log_workout(
|
||||
title=structured.get("title") or "Тренировка",
|
||||
notes=structured.get("notes") or arguments.get("text", ""),
|
||||
duration_min=structured.get("duration_min"),
|
||||
exercises=structured.get("exercises"),
|
||||
active_calories=structured.get("active_calories"),
|
||||
total_calories=structured.get("total_calories"),
|
||||
steps=structured.get("steps"),
|
||||
activity_type=structured.get("activity_type"),
|
||||
met=structured.get("met"),
|
||||
day=day,
|
||||
days_ago=arguments.get("days_ago"),
|
||||
)
|
||||
if name == "lookup_food":
|
||||
return OpenFoodFactsClient().search(
|
||||
arguments.get("query", ""),
|
||||
limit=arguments.get("limit", 5),
|
||||
)
|
||||
if name == "lookup_exercise":
|
||||
return WgerClient().search_exercises(
|
||||
arguments.get("query", ""),
|
||||
limit=arguments.get("limit", 8),
|
||||
)
|
||||
if name == "set_fitness_reminder":
|
||||
return fitness.set_reminder(
|
||||
arguments.get("kind", "water"),
|
||||
enabled=arguments.get("enabled"),
|
||||
hour=arguments.get("hour"),
|
||||
minute=arguments.get("minute"),
|
||||
interval_hours=arguments.get("interval_hours"),
|
||||
)
|
||||
return NOT_HANDLED
|
||||
@@ -0,0 +1,116 @@
|
||||
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
|
||||
@@ -0,0 +1,146 @@
|
||||
from typing import Any
|
||||
|
||||
from app.memory.service import MemoryService
|
||||
from app.tools._dispatch import NOT_HANDLED, ToolContext
|
||||
|
||||
TOOL_NAMES = frozenset({
|
||||
"remember_fact",
|
||||
"recall_memories",
|
||||
"forget_memory",
|
||||
"update_profile",
|
||||
"update_session_summary",
|
||||
})
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "remember_fact",
|
||||
"description": (
|
||||
"Сохранить факт в долгосрочную память. "
|
||||
"Когда пользователь просит «запомни», или сообщает устойчивое предпочтение/факт."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string", "description": "Что запомнить"},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "preference, person, habit, project, fact",
|
||||
},
|
||||
"importance": {"type": "integer", "description": "1-5, по умолчанию 3"},
|
||||
},
|
||||
"required": ["content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "recall_memories",
|
||||
"description": (
|
||||
"Поиск в долгосрочной памяти. "
|
||||
"Когда спрашивают «что ты помнишь», «что я говорил про X»."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Подстрока для поиска"},
|
||||
"category": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "forget_memory",
|
||||
"description": "Удалить (деактивировать) факт по id из recall_memories или снимка памяти.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {"type": "integer"},
|
||||
},
|
||||
"required": ["memory_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_profile",
|
||||
"description": (
|
||||
"Обновить профиль пользователя: name, timezone, language, notes. "
|
||||
"Передавай только изменившиеся поля."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "string", "description": "Возраст пользователя"},
|
||||
"timezone": {"type": "string"},
|
||||
"language": {"type": "string"},
|
||||
"notes": {"type": "string"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_session_summary",
|
||||
"description": (
|
||||
"Сохранить краткую сводку темы текущего чата "
|
||||
"(когда диалог длинный или пользователь просит «сожми контекст»)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {"type": "string", "description": "2-5 предложений о теме чата"},
|
||||
"session_id": {"type": "integer"},
|
||||
},
|
||||
"required": ["summary", "session_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any:
|
||||
if name not in TOOL_NAMES:
|
||||
return NOT_HANDLED
|
||||
|
||||
memory = MemoryService(ctx.db, ctx.user_id)
|
||||
|
||||
if name == "remember_fact":
|
||||
return memory.remember_fact(
|
||||
arguments.get("content", ""),
|
||||
category=arguments.get("category", "fact"),
|
||||
importance=arguments.get("importance", 3),
|
||||
session_id=ctx.session_id,
|
||||
source="tool",
|
||||
)
|
||||
if name == "recall_memories":
|
||||
return memory.recall_memories(
|
||||
query=arguments.get("query"),
|
||||
category=arguments.get("category"),
|
||||
limit=arguments.get("limit", 20),
|
||||
)
|
||||
if name == "forget_memory":
|
||||
return memory.forget_memory(int(arguments["memory_id"]))
|
||||
if name == "update_profile":
|
||||
updates = {
|
||||
k: arguments[k]
|
||||
for k in ("name", "age", "timezone", "language", "notes")
|
||||
if k in arguments and arguments[k] is not None
|
||||
}
|
||||
return memory.update_profile(updates)
|
||||
if name == "update_session_summary":
|
||||
return memory.update_session_summary(
|
||||
int(arguments["session_id"]),
|
||||
arguments.get("summary", ""),
|
||||
)
|
||||
return NOT_HANDLED
|
||||
@@ -0,0 +1,157 @@
|
||||
from typing import Any
|
||||
|
||||
from app.pomodoro.service import PomodoroService
|
||||
from app.tools._dispatch import NOT_HANDLED, ToolContext
|
||||
|
||||
TOOL_NAMES = frozenset({
|
||||
"get_pomodoro_status",
|
||||
"start_pomodoro",
|
||||
"start_short_break",
|
||||
"start_long_break",
|
||||
"stop_pomodoro",
|
||||
"skip_pomodoro_phase",
|
||||
"reset_pomodoro_cycle",
|
||||
"get_pomodoro_history",
|
||||
})
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_pomodoro_status",
|
||||
"description": "ОБЯЗАТЕЛЬНО вызывай перед любым ответом о таймере. Статус, фаза и прогресс цикла.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "start_pomodoro",
|
||||
"description": "Запустить фазу работы в цикле помидоро (25 мин по умолчанию).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_min": {"type": "integer", "description": "Минуты работы"},
|
||||
"task_note": {"type": "string", "description": "Над чем работаем"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "start_short_break",
|
||||
"description": "Запустить короткий перерыв между работами.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_min": {"type": "integer", "description": "Минуты перерыва"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "start_long_break",
|
||||
"description": "Запустить длинный перерыв после завершения цикла работ.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_min": {"type": "integer", "description": "Минуты перерыва"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stop_pomodoro",
|
||||
"description": "Остановить текущую фазу таймера.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {"type": "string", "description": "Отчёт о сделанном"},
|
||||
"completed": {
|
||||
"type": "boolean",
|
||||
"description": "True если фаза полностью завершена",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "skip_pomodoro_phase",
|
||||
"description": "Досрочно завершить текущую фазу и перейти к следующей в цикле.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "reset_pomodoro_cycle",
|
||||
"description": "Сбросить цикл помидоро: обнулить счётчик работ и остановить таймер.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clear_task": {
|
||||
"type": "boolean",
|
||||
"description": "Также очистить текущую задачу",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_pomodoro_history",
|
||||
"description": "История помидоро-сессий (таймер), не Taiga-задачи.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "description": "Сколько сессий вернуть"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any:
|
||||
if name not in TOOL_NAMES:
|
||||
return NOT_HANDLED
|
||||
|
||||
pomodoro = PomodoroService(ctx.db, ctx.user_id)
|
||||
|
||||
if name == "get_pomodoro_status":
|
||||
return pomodoro.get_status()
|
||||
if name == "start_pomodoro":
|
||||
return pomodoro.start_work(
|
||||
duration_min=arguments.get("duration_min"),
|
||||
task_note=arguments.get("task_note", ""),
|
||||
)
|
||||
if name == "start_short_break":
|
||||
return pomodoro.start_short_break(duration_min=arguments.get("duration_min"))
|
||||
if name == "start_long_break":
|
||||
return pomodoro.start_long_break(duration_min=arguments.get("duration_min"))
|
||||
if name == "stop_pomodoro":
|
||||
return pomodoro.stop(
|
||||
result=arguments.get("result", ""),
|
||||
completed=arguments.get("completed", False),
|
||||
)
|
||||
if name == "skip_pomodoro_phase":
|
||||
return pomodoro.skip_phase()
|
||||
if name == "reset_pomodoro_cycle":
|
||||
return pomodoro.reset_cycle(clear_task=arguments.get("clear_task", False))
|
||||
if name == "get_pomodoro_history":
|
||||
return pomodoro.history(limit=arguments.get("limit", 10))
|
||||
return NOT_HANDLED
|
||||
@@ -0,0 +1,123 @@
|
||||
from typing import Any
|
||||
|
||||
from app.projects.service import ProjectService
|
||||
from app.tools._dispatch import NOT_HANDLED, ToolContext
|
||||
|
||||
TOOL_NAMES = frozenset({
|
||||
"sync_taiga_projects",
|
||||
"list_taiga_projects",
|
||||
"list_taiga_tasks",
|
||||
"create_work_item",
|
||||
"list_work_items",
|
||||
})
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "sync_taiga_projects",
|
||||
"description": "Синхронизировать список проектов из Taiga API. Вызывай если проекты неизвестны.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_taiga_projects",
|
||||
"description": "Список проектов Taiga с привязкой Gitea.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_taiga_tasks",
|
||||
"description": (
|
||||
"ОБЯЗАТЕЛЬНО при вопросах «какие задачи», «покажи задачи проекта», «что открыто в Taiga». "
|
||||
"Живые user stories и tasks из Taiga API. НЕ путать с list_work_items."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_slug": {
|
||||
"type": "string",
|
||||
"description": "Slug проекта, например home_assistant. Пусто = все проекты.",
|
||||
},
|
||||
"limit": {"type": "integer", "description": "Макс. на проект, по умолчанию 20"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_work_item",
|
||||
"description": (
|
||||
"Создать фичу/баг из вольного текста: структурировать через LLM, "
|
||||
"создать Taiga story + Gitea issue. Вызывай при «заведи баг», «оформи фичу», «добавь в таигу»."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "description": "Полное описание от пользователя"},
|
||||
"project_slug": {
|
||||
"type": "string",
|
||||
"description": "Slug проекта Taiga, если известен",
|
||||
},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_work_items",
|
||||
"description": (
|
||||
"Только задачи, созданные ЭТИМ ассистентом через create_work_item (локальная БД). "
|
||||
"НЕ использовать для общего вопроса «какие задачи в Taiga» — для того list_taiga_tasks."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"type": "string", "description": "open или closed"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any:
|
||||
if name not in TOOL_NAMES:
|
||||
return NOT_HANDLED
|
||||
|
||||
projects = ProjectService(ctx.db, ctx.user_id)
|
||||
|
||||
if name == "sync_taiga_projects":
|
||||
from app.projects.context import invalidate_projects_snapshot_cache
|
||||
|
||||
result = projects.sync_taiga_projects()
|
||||
invalidate_projects_snapshot_cache(ctx.user_id)
|
||||
return result
|
||||
if name == "list_taiga_projects":
|
||||
return projects.list_projects()
|
||||
if name == "list_taiga_tasks":
|
||||
return projects.list_taiga_open_tasks(
|
||||
project_slug=arguments.get("project_slug"),
|
||||
limit=arguments.get("limit", 20),
|
||||
)
|
||||
if name == "create_work_item":
|
||||
return await projects.create_work_item(
|
||||
arguments.get("text", ""),
|
||||
project_slug=arguments.get("project_slug"),
|
||||
)
|
||||
if name == "list_work_items":
|
||||
return projects.list_work_items(
|
||||
limit=arguments.get("limit", 20),
|
||||
status=arguments.get("status"),
|
||||
)
|
||||
return NOT_HANDLED
|
||||
+22
-1084
File diff suppressed because it is too large
Load Diff
@@ -1,961 +0,0 @@
|
||||
import json
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.fitness.service import FitnessService
|
||||
from app.fitness.structuring import structure_meal, structure_workout
|
||||
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.integrations.openfoodfacts import OpenFoodFactsClient
|
||||
from app.integrations.wger import WgerClient
|
||||
from app.memory.service import MemoryService
|
||||
from app.pomodoro.service import PomodoroService
|
||||
from app.projects.service import ProjectService
|
||||
from app.reminders.service import RemindersService
|
||||
from app.shopping.service import ShoppingService
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_pomodoro_status",
|
||||
"description": "ОБЯЗАТЕЛЬНО вызывай перед любым ответом о таймере. Статус, фаза и прогресс цикла.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "start_pomodoro",
|
||||
"description": "Запустить фазу работы в цикле помидоро (25 мин по умолчанию).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_min": {"type": "integer", "description": "Минуты работы"},
|
||||
"task_note": {"type": "string", "description": "Над чем работаем"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "start_short_break",
|
||||
"description": "Запустить короткий перерыв между работами.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_min": {"type": "integer", "description": "Минуты перерыва"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "start_long_break",
|
||||
"description": "Запустить длинный перерыв после завершения цикла работ.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_min": {"type": "integer", "description": "Минуты перерыва"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stop_pomodoro",
|
||||
"description": "Остановить текущую фазу таймера.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {"type": "string", "description": "Отчёт о сделанном"},
|
||||
"completed": {
|
||||
"type": "boolean",
|
||||
"description": "True если фаза полностью завершена",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "skip_pomodoro_phase",
|
||||
"description": "Досрочно завершить текущую фазу и перейти к следующей в цикле.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "reset_pomodoro_cycle",
|
||||
"description": "Сбросить цикл помидоро: обнулить счётчик работ и остановить таймер.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clear_task": {
|
||||
"type": "boolean",
|
||||
"description": "Также очистить текущую задачу",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_pomodoro_history",
|
||||
"description": "История помидоро-сессий (таймер), не Taiga-задачи.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "description": "Сколько сессий вернуть"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "sync_taiga_projects",
|
||||
"description": "Синхронизировать список проектов из Taiga API. Вызывай если проекты неизвестны.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_taiga_projects",
|
||||
"description": "Список проектов Taiga с привязкой Gitea.",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_taiga_tasks",
|
||||
"description": (
|
||||
"ОБЯЗАТЕЛЬНО при вопросах «какие задачи», «покажи задачи проекта», «что открыто в Taiga». "
|
||||
"Живые user stories и tasks из Taiga API. НЕ путать с list_work_items."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_slug": {
|
||||
"type": "string",
|
||||
"description": "Slug проекта, например home_assistant. Пусто = все проекты.",
|
||||
},
|
||||
"limit": {"type": "integer", "description": "Макс. на проект, по умолчанию 20"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_work_item",
|
||||
"description": (
|
||||
"Создать фичу/баг из вольного текста: структурировать через LLM, "
|
||||
"создать Taiga story + Gitea issue. Вызывай при «заведи баг», «оформи фичу», «добавь в таигу»."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "description": "Полное описание от пользователя"},
|
||||
"project_slug": {
|
||||
"type": "string",
|
||||
"description": "Slug проекта Taiga, если известен",
|
||||
},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "remember_fact",
|
||||
"description": (
|
||||
"Сохранить факт в долгосрочную память. "
|
||||
"Когда пользователь просит «запомни», или сообщает устойчивое предпочтение/факт."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string", "description": "Что запомнить"},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "preference, person, habit, project, fact",
|
||||
},
|
||||
"importance": {"type": "integer", "description": "1-5, по умолчанию 3"},
|
||||
},
|
||||
"required": ["content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "recall_memories",
|
||||
"description": (
|
||||
"Поиск в долгосрочной памяти. "
|
||||
"Когда спрашивают «что ты помнишь», «что я говорил про X»."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Подстрока для поиска"},
|
||||
"category": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "forget_memory",
|
||||
"description": "Удалить (деактивировать) факт по id из recall_memories или снимка памяти.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {"type": "integer"},
|
||||
},
|
||||
"required": ["memory_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_profile",
|
||||
"description": (
|
||||
"Обновить профиль пользователя: name, timezone, language, notes. "
|
||||
"Передавай только изменившиеся поля."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "string", "description": "Возраст пользователя"},
|
||||
"timezone": {"type": "string"},
|
||||
"language": {"type": "string"},
|
||||
"notes": {"type": "string"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_session_summary",
|
||||
"description": (
|
||||
"Сохранить краткую сводку темы текущего чата "
|
||||
"(когда диалог длинный или пользователь просит «сожми контекст»)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {"type": "string", "description": "2-5 предложений о теме чата"},
|
||||
"session_id": {"type": "integer"},
|
||||
},
|
||||
"required": ["summary", "session_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_fitness_summary",
|
||||
"description": (
|
||||
"Сводка фитнеса за день: ккал, БЖУ, вода, еда, тренировки. "
|
||||
"Без даты — сегодня; date=YYYY-MM-DD или days_ago=1 (вчера)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {"type": "string", "description": "Дата YYYY-MM-DD"},
|
||||
"days_ago": {
|
||||
"type": "integer",
|
||||
"description": "0 сегодня, 1 вчера, 2 позавчера…",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_fitness_history",
|
||||
"description": (
|
||||
"Краткая история за несколько дней (ккал, вода, тренировки по дням). "
|
||||
"«На прошлой неделе», «за 7 дней»."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {"type": "integer", "description": "Сколько дней, по умолчанию 7"},
|
||||
"end_date": {"type": "string", "description": "Конец периода YYYY-MM-DD, по умолчанию сегодня"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "set_fitness_profile",
|
||||
"description": "Настроить фитнес-профиль и пересчитать цели ккал/БЖУ/воды.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sex": {"type": "string", "description": "male/female"},
|
||||
"age": {"type": "integer"},
|
||||
"height_cm": {"type": "number"},
|
||||
"weight_kg": {"type": "number"},
|
||||
"activity_level": {
|
||||
"type": "string",
|
||||
"description": "sedentary/light/moderate/active/very_active",
|
||||
},
|
||||
"goal": {"type": "string", "description": "lose/maintain/gain"},
|
||||
"target_weight_kg": {"type": "number"},
|
||||
"weekly_workouts": {"type": "integer"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calc_fitness_targets",
|
||||
"description": "Калькулятор BMR/TDEE/макросов без сохранения.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sex": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
"height_cm": {"type": "number"},
|
||||
"weight_kg": {"type": "number"},
|
||||
"activity_level": {"type": "string"},
|
||||
"goal": {"type": "string"},
|
||||
},
|
||||
"required": ["weight_kg", "height_cm", "age"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_meal",
|
||||
"description": "Записать приём пищи. LLM оценит ккал и БЖУ из текста.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string", "description": "Что съел"},
|
||||
"meal_type": {"type": "string"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_water",
|
||||
"description": "Записать воду в мл.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount_ml": {"type": "integer"},
|
||||
},
|
||||
"required": ["amount_ml"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_weight",
|
||||
"description": "Записать вес в кг.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"weight_kg": {"type": "number"},
|
||||
"body_fat_pct": {"type": "number"},
|
||||
"notes": {"type": "string"},
|
||||
},
|
||||
"required": ["weight_kg"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "log_workout",
|
||||
"description": "Записать тренировку из текста.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_food",
|
||||
"description": "Поиск продукта в Open Food Facts (ккал на 100г).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_exercise",
|
||||
"description": "Поиск упражнения в базе wger.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "set_fitness_reminder",
|
||||
"description": "Вкл/выкл или настроить напоминание: water, meal, workout, weigh_in.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {"type": "string"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"hour": {"type": "integer"},
|
||||
"minute": {"type": "integer"},
|
||||
"interval_hours": {"type": "integer"},
|
||||
},
|
||||
"required": ["kind"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": (
|
||||
"ОБЯЗАТЕЛЬНО для вопросов о погоде, «что на улице», «будет ли дождь». "
|
||||
"Текущая погода и прогноз по часам."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hours_ahead": {
|
||||
"type": "integer",
|
||||
"description": "Сколько часов прогноза (по умолчанию 12)",
|
||||
},
|
||||
},
|
||||
"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 через RP-чат). "
|
||||
"«Нарисуй себя» / портрет персонажа → draw_self=true. "
|
||||
"Другая сцена → scene_description на английском (booru-теги). "
|
||||
"Внешность берётся из карточки персонажа. Только по запросу или когда уместно."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"draw_self": {
|
||||
"type": "boolean",
|
||||
"description": "Нарисовать персонажа из карточки в контексте текущего чата",
|
||||
},
|
||||
"scene_description": {
|
||||
"type": "string",
|
||||
"description": "Описание сцены на английском (booru-теги), если не draw_self",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_shopping_lists",
|
||||
"description": "Все списки покупок с позициями. «Что купить», «покажи списки».",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_shopping_list",
|
||||
"description": "Создать новый список покупок.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Название списка, например «Продукты»"},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "add_shopping_items",
|
||||
"description": "Добавить товары в список. Список создаётся, если не существует.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list_name": {"type": "string", "description": "Название списка"},
|
||||
"list_id": {"type": "integer"},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string"},
|
||||
"quantity": {"type": "number"},
|
||||
"unit": {"type": "string"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["items"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "check_shopping_item",
|
||||
"description": "Отметить позицию как купленную (checked=true) или снять отметку (false).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item_id": {"type": "integer"},
|
||||
"checked": {"type": "boolean"},
|
||||
},
|
||||
"required": ["item_id", "checked"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "remove_shopping_item",
|
||||
"description": "Удалить позицию из списка по item_id.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"item_id": {"type": "integer"}},
|
||||
"required": ["item_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_shopping_list",
|
||||
"description": "Удалить весь список покупок.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"list_id": {"type": "integer"}},
|
||||
"required": ["list_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_reminders",
|
||||
"description": "Список активных напоминаний. «Что напомнил», «мои напоминания».",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "description": "Макс. записей, по умолчанию 20"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_reminder",
|
||||
"description": (
|
||||
"Создать напоминание. due_at — ISO datetime в часовом поясе пользователя "
|
||||
"(см. [Текущее время]). Примеры: через 15 мин, завтра 09:00, 2027-05-12T12:16:00."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "О чём напомнить"},
|
||||
"due_at": {"type": "string", "description": "ISO datetime"},
|
||||
"notes": {"type": "string"},
|
||||
"all_day": {"type": "boolean"},
|
||||
"recurrence": {
|
||||
"type": "string",
|
||||
"enum": ["none", "daily", "weekly", "monthly", "yearly"],
|
||||
"description": "Повтор (yearly — день рождения, Новый год)",
|
||||
},
|
||||
},
|
||||
"required": ["title", "due_at"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_reminder",
|
||||
"description": "Изменить напоминание по id.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reminder_id": {"type": "integer"},
|
||||
"title": {"type": "string"},
|
||||
"due_at": {"type": "string"},
|
||||
"notes": {"type": "string"},
|
||||
"all_day": {"type": "boolean"},
|
||||
"recurrence": {
|
||||
"type": "string",
|
||||
"enum": ["none", "daily", "weekly", "monthly", "yearly"],
|
||||
},
|
||||
"enabled": {"type": "boolean"},
|
||||
},
|
||||
"required": ["reminder_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_reminder",
|
||||
"description": "Удалить напоминание по id.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"reminder_id": {"type": "integer"}},
|
||||
"required": ["reminder_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "complete_reminder",
|
||||
"description": "Отметить напоминание выполненным (снять с календаря).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"reminder_id": {"type": "integer"}},
|
||||
"required": ["reminder_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_work_items",
|
||||
"description": (
|
||||
"Только задачи, созданные ЭТИМ ассистентом через create_work_item (локальная БД). "
|
||||
"НЕ использовать для общего вопроса «какие задачи в Taiga» — для того list_taiga_tasks."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"type": "string", "description": "open или closed"},
|
||||
"limit": {"type": "integer"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute_tool(
|
||||
db: Session,
|
||||
name: str,
|
||||
arguments: dict[str, Any],
|
||||
*,
|
||||
session_id: int | None = None,
|
||||
) -> str:
|
||||
pomodoro = PomodoroService(db)
|
||||
projects = ProjectService(db)
|
||||
memory = MemoryService(db)
|
||||
fitness = FitnessService(db)
|
||||
shopping = ShoppingService(db)
|
||||
reminders = RemindersService(db)
|
||||
|
||||
try:
|
||||
if name == "get_pomodoro_status":
|
||||
result = pomodoro.get_status()
|
||||
elif name == "start_pomodoro":
|
||||
result = pomodoro.start_work(
|
||||
duration_min=arguments.get("duration_min"),
|
||||
task_note=arguments.get("task_note", ""),
|
||||
)
|
||||
elif name == "start_short_break":
|
||||
result = pomodoro.start_short_break(duration_min=arguments.get("duration_min"))
|
||||
elif name == "start_long_break":
|
||||
result = pomodoro.start_long_break(duration_min=arguments.get("duration_min"))
|
||||
elif name == "stop_pomodoro":
|
||||
result = pomodoro.stop(
|
||||
result=arguments.get("result", ""),
|
||||
completed=arguments.get("completed", False),
|
||||
)
|
||||
elif name == "skip_pomodoro_phase":
|
||||
result = pomodoro.skip_phase()
|
||||
elif name == "reset_pomodoro_cycle":
|
||||
result = pomodoro.reset_cycle(clear_task=arguments.get("clear_task", False))
|
||||
elif name == "get_pomodoro_history":
|
||||
result = pomodoro.history(limit=arguments.get("limit", 10))
|
||||
elif name == "sync_taiga_projects":
|
||||
from app.projects.context import invalidate_projects_snapshot_cache
|
||||
|
||||
result = projects.sync_taiga_projects()
|
||||
invalidate_projects_snapshot_cache()
|
||||
elif name == "list_taiga_projects":
|
||||
result = projects.list_projects()
|
||||
elif name == "list_taiga_tasks":
|
||||
result = projects.list_taiga_open_tasks(
|
||||
project_slug=arguments.get("project_slug"),
|
||||
limit=arguments.get("limit", 20),
|
||||
)
|
||||
elif name == "create_work_item":
|
||||
result = await projects.create_work_item(
|
||||
arguments.get("text", ""),
|
||||
project_slug=arguments.get("project_slug"),
|
||||
)
|
||||
elif name == "list_work_items":
|
||||
result = projects.list_work_items(
|
||||
limit=arguments.get("limit", 20),
|
||||
status=arguments.get("status"),
|
||||
)
|
||||
elif name == "remember_fact":
|
||||
result = memory.remember_fact(
|
||||
arguments.get("content", ""),
|
||||
category=arguments.get("category", "fact"),
|
||||
importance=arguments.get("importance", 3),
|
||||
session_id=session_id,
|
||||
source="tool",
|
||||
)
|
||||
elif name == "recall_memories":
|
||||
result = memory.recall_memories(
|
||||
query=arguments.get("query"),
|
||||
category=arguments.get("category"),
|
||||
limit=arguments.get("limit", 20),
|
||||
)
|
||||
elif name == "forget_memory":
|
||||
result = memory.forget_memory(int(arguments["memory_id"]))
|
||||
elif name == "update_profile":
|
||||
updates = {
|
||||
k: arguments[k]
|
||||
for k in ("name", "age", "timezone", "language", "notes")
|
||||
if k in arguments and arguments[k] is not None
|
||||
}
|
||||
result = memory.update_profile(updates)
|
||||
elif name == "update_session_summary":
|
||||
result = memory.update_session_summary(
|
||||
int(arguments["session_id"]),
|
||||
arguments.get("summary", ""),
|
||||
)
|
||||
elif name == "get_fitness_summary":
|
||||
day: date | None = None
|
||||
if arguments.get("date"):
|
||||
day = date.fromisoformat(str(arguments["date"]))
|
||||
elif arguments.get("days_ago") is not None:
|
||||
day = datetime.now(timezone.utc).date() - timedelta(
|
||||
days=int(arguments["days_ago"])
|
||||
)
|
||||
result = fitness.get_daily_summary(day)
|
||||
elif name == "get_fitness_history":
|
||||
end_day = None
|
||||
if arguments.get("end_date"):
|
||||
end_day = date.fromisoformat(str(arguments["end_date"]))
|
||||
result = fitness.get_history(
|
||||
days=int(arguments.get("days") or 7),
|
||||
end_day=end_day,
|
||||
)
|
||||
elif name == "set_fitness_profile":
|
||||
updates = {
|
||||
k: arguments[k]
|
||||
for k in (
|
||||
"sex", "age", "height_cm", "weight_kg", "activity_level",
|
||||
"goal", "target_weight_kg", "weekly_workouts",
|
||||
)
|
||||
if k in arguments and arguments[k] is not None
|
||||
}
|
||||
result = fitness.set_profile(updates)
|
||||
elif name == "calc_fitness_targets":
|
||||
result = fitness.calc_targets(arguments)
|
||||
elif name == "log_meal":
|
||||
structured = await structure_meal(arguments.get("text", ""))
|
||||
result = fitness.log_meal(
|
||||
description=structured.get("description") or arguments.get("text", ""),
|
||||
meal_type=arguments.get("meal_type") or structured.get("meal_type") or "snack",
|
||||
calories=float(structured.get("calories") or 0),
|
||||
protein_g=float(structured.get("protein_g") or 0),
|
||||
fat_g=float(structured.get("fat_g") or 0),
|
||||
carbs_g=float(structured.get("carbs_g") or 0),
|
||||
source="llm",
|
||||
estimated=True,
|
||||
)
|
||||
elif name == "log_water":
|
||||
result = fitness.log_water(int(arguments.get("amount_ml", 250)))
|
||||
elif name == "log_weight":
|
||||
result = fitness.log_weight(
|
||||
float(arguments["weight_kg"]),
|
||||
body_fat_pct=arguments.get("body_fat_pct"),
|
||||
notes=arguments.get("notes", ""),
|
||||
)
|
||||
elif name == "log_workout":
|
||||
structured = await structure_workout(arguments.get("text", ""))
|
||||
result = fitness.log_workout(
|
||||
title=structured.get("title") or "Тренировка",
|
||||
notes=structured.get("notes") or arguments.get("text", ""),
|
||||
duration_min=structured.get("duration_min"),
|
||||
exercises=structured.get("exercises"),
|
||||
)
|
||||
elif name == "lookup_food":
|
||||
result = OpenFoodFactsClient().search(
|
||||
arguments.get("query", ""),
|
||||
limit=arguments.get("limit", 5),
|
||||
)
|
||||
elif name == "lookup_exercise":
|
||||
result = WgerClient().search_exercises(
|
||||
arguments.get("query", ""),
|
||||
limit=arguments.get("limit", 8),
|
||||
)
|
||||
elif name == "set_fitness_reminder":
|
||||
result = fitness.set_reminder(
|
||||
arguments.get("kind", "water"),
|
||||
enabled=arguments.get("enabled"),
|
||||
hour=arguments.get("hour"),
|
||||
minute=arguments.get("minute"),
|
||||
interval_hours=arguments.get("interval_hours"),
|
||||
)
|
||||
elif name == "get_weather":
|
||||
hours = int(arguments.get("hours_ahead") or 12)
|
||||
client = OpenMeteoClient()
|
||||
weather = client.fetch_current_and_hourly(hours_ahead=hours)
|
||||
result = {
|
||||
"weather": weather,
|
||||
"rain_summary": client.rain_summary(hours_ahead=hours) if weather.get("ok") else "",
|
||||
}
|
||||
elif name == "get_morning_briefing":
|
||||
include_news = arguments.get("include_news", True)
|
||||
result = build_weather_briefing(
|
||||
hours_ahead=12,
|
||||
include_news=bool(include_news),
|
||||
)
|
||||
elif name == "generate_image":
|
||||
result = await run_generate_image(
|
||||
db,
|
||||
session_id=session_id,
|
||||
draw_self=bool(arguments.get("draw_self")),
|
||||
scene_description=arguments.get("scene_description", ""),
|
||||
)
|
||||
elif name == "list_shopping_lists":
|
||||
result = shopping.list_lists(include_items=True)
|
||||
elif name == "create_shopping_list":
|
||||
result = shopping.create_list(arguments.get("name", ""))
|
||||
elif name == "add_shopping_items":
|
||||
result = shopping.add_items(
|
||||
arguments.get("items") or [],
|
||||
list_id=arguments.get("list_id"),
|
||||
list_name=arguments.get("list_name"),
|
||||
)
|
||||
elif name == "check_shopping_item":
|
||||
result = shopping.set_item_checked(
|
||||
int(arguments["item_id"]),
|
||||
bool(arguments.get("checked", True)),
|
||||
)
|
||||
elif name == "remove_shopping_item":
|
||||
result = shopping.remove_item(int(arguments["item_id"]))
|
||||
elif name == "delete_shopping_list":
|
||||
result = shopping.delete_list(int(arguments["list_id"]))
|
||||
elif name == "list_reminders":
|
||||
result = reminders.list_upcoming(limit=int(arguments.get("limit") or 20))
|
||||
elif name == "create_reminder":
|
||||
result = reminders.create(
|
||||
title=arguments.get("title", ""),
|
||||
due_at=arguments.get("due_at", ""),
|
||||
notes=arguments.get("notes", ""),
|
||||
all_day=bool(arguments.get("all_day", False)),
|
||||
recurrence=arguments.get("recurrence", "none"),
|
||||
)
|
||||
elif name == "update_reminder":
|
||||
result = reminders.update(
|
||||
int(arguments["reminder_id"]),
|
||||
title=arguments.get("title"),
|
||||
due_at=arguments.get("due_at"),
|
||||
notes=arguments.get("notes"),
|
||||
all_day=arguments.get("all_day"),
|
||||
recurrence=arguments.get("recurrence"),
|
||||
enabled=arguments.get("enabled"),
|
||||
)
|
||||
elif name == "delete_reminder":
|
||||
result = reminders.delete(int(arguments["reminder_id"]))
|
||||
elif name == "complete_reminder":
|
||||
result = reminders.complete(int(arguments["reminder_id"]))
|
||||
else:
|
||||
return json.dumps({"error": f"Unknown tool: {name}"}, ensure_ascii=False)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
except ValueError as exc:
|
||||
return json.dumps({"error": str(exc)}, ensure_ascii=False)
|
||||
except Exception as exc:
|
||||
return json.dumps({"error": str(exc)}, ensure_ascii=False)
|
||||
@@ -0,0 +1,134 @@
|
||||
from typing import Any
|
||||
|
||||
from app.reminders_scoped.service import RemindersService
|
||||
from app.tools._dispatch import NOT_HANDLED, ToolContext
|
||||
|
||||
TOOL_NAMES = frozenset({
|
||||
"list_reminders",
|
||||
"create_reminder",
|
||||
"update_reminder",
|
||||
"delete_reminder",
|
||||
"complete_reminder",
|
||||
})
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_reminders",
|
||||
"description": "Список активных напоминаний. «Что напомнил», «мои напоминания».",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "description": "Макс. записей, по умолчанию 20"},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_reminder",
|
||||
"description": (
|
||||
"Создать напоминание. due_at — ISO datetime в часовом поясе пользователя "
|
||||
"(см. [Текущее время]). Примеры: через 15 мин, завтра 09:00, 2027-05-12T12:16:00."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "О чём напомнить"},
|
||||
"due_at": {"type": "string", "description": "ISO datetime"},
|
||||
"notes": {"type": "string"},
|
||||
"all_day": {"type": "boolean"},
|
||||
"recurrence": {
|
||||
"type": "string",
|
||||
"enum": ["none", "daily", "weekly", "monthly", "yearly"],
|
||||
"description": "Повтор (yearly — день рождения, Новый год)",
|
||||
},
|
||||
},
|
||||
"required": ["title", "due_at"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_reminder",
|
||||
"description": "Изменить напоминание по id.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reminder_id": {"type": "integer"},
|
||||
"title": {"type": "string"},
|
||||
"due_at": {"type": "string"},
|
||||
"notes": {"type": "string"},
|
||||
"all_day": {"type": "boolean"},
|
||||
"recurrence": {
|
||||
"type": "string",
|
||||
"enum": ["none", "daily", "weekly", "monthly", "yearly"],
|
||||
},
|
||||
"enabled": {"type": "boolean"},
|
||||
},
|
||||
"required": ["reminder_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_reminder",
|
||||
"description": "Удалить напоминание по id.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"reminder_id": {"type": "integer"}},
|
||||
"required": ["reminder_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "complete_reminder",
|
||||
"description": "Отметить напоминание выполненным (снять с календаря).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"reminder_id": {"type": "integer"}},
|
||||
"required": ["reminder_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any:
|
||||
if name not in TOOL_NAMES:
|
||||
return NOT_HANDLED
|
||||
|
||||
reminders = RemindersService(ctx.db, ctx.user_id)
|
||||
|
||||
if name == "list_reminders":
|
||||
return reminders.list_upcoming(limit=int(arguments.get("limit") or 20))
|
||||
if name == "create_reminder":
|
||||
return reminders.create(
|
||||
title=arguments.get("title", ""),
|
||||
due_at=arguments.get("due_at", ""),
|
||||
notes=arguments.get("notes", ""),
|
||||
all_day=bool(arguments.get("all_day", False)),
|
||||
recurrence=arguments.get("recurrence", "none"),
|
||||
)
|
||||
if name == "update_reminder":
|
||||
return reminders.update(
|
||||
int(arguments["reminder_id"]),
|
||||
title=arguments.get("title"),
|
||||
due_at=arguments.get("due_at"),
|
||||
notes=arguments.get("notes"),
|
||||
all_day=arguments.get("all_day"),
|
||||
recurrence=arguments.get("recurrence"),
|
||||
enabled=arguments.get("enabled"),
|
||||
)
|
||||
if name == "delete_reminder":
|
||||
return reminders.delete(int(arguments["reminder_id"]))
|
||||
if name == "complete_reminder":
|
||||
return reminders.complete(int(arguments["reminder_id"]))
|
||||
return NOT_HANDLED
|
||||
@@ -0,0 +1,132 @@
|
||||
from typing import Any
|
||||
|
||||
from app.shopping.service import ShoppingService
|
||||
from app.tools._dispatch import NOT_HANDLED, ToolContext
|
||||
|
||||
TOOL_NAMES = frozenset({
|
||||
"list_shopping_lists",
|
||||
"create_shopping_list",
|
||||
"add_shopping_items",
|
||||
"check_shopping_item",
|
||||
"remove_shopping_item",
|
||||
"delete_shopping_list",
|
||||
})
|
||||
|
||||
TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_shopping_lists",
|
||||
"description": "Все списки покупок с позициями. «Что купить», «покажи списки».",
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_shopping_list",
|
||||
"description": "Создать новый список покупок.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Название списка, например «Продукты»"},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "add_shopping_items",
|
||||
"description": "Добавить товары в список. Список создаётся, если не существует.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list_name": {"type": "string", "description": "Название списка"},
|
||||
"list_id": {"type": "integer"},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {"type": "string"},
|
||||
"quantity": {"type": "number"},
|
||||
"unit": {"type": "string"},
|
||||
},
|
||||
"required": ["text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["items"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "check_shopping_item",
|
||||
"description": "Отметить позицию как купленную (checked=true) или снять отметку (false).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item_id": {"type": "integer"},
|
||||
"checked": {"type": "boolean"},
|
||||
},
|
||||
"required": ["item_id", "checked"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "remove_shopping_item",
|
||||
"description": "Удалить позицию из списка по item_id.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"item_id": {"type": "integer"}},
|
||||
"required": ["item_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_shopping_list",
|
||||
"description": "Удалить весь список покупок.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"list_id": {"type": "integer"}},
|
||||
"required": ["list_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def execute(name: str, arguments: dict[str, Any], ctx: ToolContext) -> Any:
|
||||
if name not in TOOL_NAMES:
|
||||
return NOT_HANDLED
|
||||
|
||||
shopping = ShoppingService(ctx.db, ctx.user_id)
|
||||
|
||||
if name == "list_shopping_lists":
|
||||
return shopping.list_lists(include_items=True)
|
||||
if name == "create_shopping_list":
|
||||
return shopping.create_list(arguments.get("name", ""))
|
||||
if name == "add_shopping_items":
|
||||
return shopping.add_items(
|
||||
arguments.get("items") or [],
|
||||
list_id=arguments.get("list_id"),
|
||||
list_name=arguments.get("list_name"),
|
||||
)
|
||||
if name == "check_shopping_item":
|
||||
return shopping.set_item_checked(
|
||||
int(arguments["item_id"]),
|
||||
bool(arguments.get("checked", True)),
|
||||
)
|
||||
if name == "remove_shopping_item":
|
||||
return shopping.remove_item(int(arguments["item_id"]))
|
||||
if name == "delete_shopping_list":
|
||||
return shopping.delete_list(int(arguments["list_id"]))
|
||||
return NOT_HANDLED
|
||||
Reference in New Issue
Block a user