From f9999e9bb666ec02350641cea95b3320b7142229 Mon Sep 17 00:00:00 2001 From: OpenClaw Assistant Date: Sat, 21 Feb 2026 18:36:40 +0000 Subject: [PATCH] Create paradiz skill for VK pricing from Excel --- skills/paradiz/SKILL.md | 45 +++++++ skills/paradiz/references/price_template.csv | 3 + skills/paradiz/scripts/calc_quote.py | 125 +++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 skills/paradiz/SKILL.md create mode 100644 skills/paradiz/references/price_template.csv create mode 100755 skills/paradiz/scripts/calc_quote.py diff --git a/skills/paradiz/SKILL.md b/skills/paradiz/SKILL.md new file mode 100644 index 0000000..8aefcc4 --- /dev/null +++ b/skills/paradiz/SKILL.md @@ -0,0 +1,45 @@ +--- +name: paradiz +description: Отвечать клиентам в VK по стоимости отдыха на основе Excel-прайса. Использовать, когда нужно быстро посчитать цену по датам, количеству гостей, номеру/питанию и выдать короткий продающий ответ для соцсети. +--- + +# paradiz + +Скилл для быстрых ответов в VK по стоимости отдыха. + +## Что делать + +1. Взять входные данные клиента: + - даты заезда/выезда + - число гостей + - при наличии: тип номера, питание +2. Посчитать стоимость через скрипт: + +```bash +python3 {baseDir}/scripts/calc_quote.py \ + --excel {baseDir}/references/prices.xlsx \ + --checkin 2026-07-15 \ + --checkout 2026-07-25 \ + --guests 3 +``` + +3. Если найдено несколько вариантов — показать 2–3 лучших (минимум/оптимум/комфорт). +4. Отдать короткий ответ для VK в дружелюбном стиле, без лишней воды. + +## Формат ответа в VK + +Использовать шаблон: + +- Период: <даты> +- Гостей: <кол-во> +- Вариант: <номер/тариф> +- Стоимость: <итог> +- Что включено: <питание/условия> +- Короткий CTA: «Если подходит — закреплю за вами этот вариант 👌» + +## Правила + +- Если данных не хватает (нет дат/гостей) — запросить только недостающее. +- Если в прайсе нет точного совпадения — предложить ближайший доступный тариф и явно написать это. +- Всегда указывать валюту. +- Если скрипт вернул ошибку по структуре Excel — попросить обновить файл по шаблону `references/price_template.csv`. diff --git a/skills/paradiz/references/price_template.csv b/skills/paradiz/references/price_template.csv new file mode 100644 index 0000000..955b54d --- /dev/null +++ b/skills/paradiz/references/price_template.csv @@ -0,0 +1,3 @@ +date_from,date_to,guests_min,guests_max,room,meal,price_per_night,currency,notes +2026-07-01,2026-07-31,1,3,Стандарт,Завтрак,6500,₽,Высокий сезон +2026-07-01,2026-07-31,1,4,Семейный,Завтрак,8900,₽,Высокий сезон diff --git a/skills/paradiz/scripts/calc_quote.py b/skills/paradiz/scripts/calc_quote.py new file mode 100755 index 0000000..6c68ea9 --- /dev/null +++ b/skills/paradiz/scripts/calc_quote.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +import argparse +import json +from datetime import date, datetime + + +def d(s: str) -> date: + return datetime.strptime(s, "%Y-%m-%d").date() + + +def normalize(h: str) -> str: + return (h or "").strip().lower().replace(" ", "_") + + +def pick(header_map, *aliases): + for a in aliases: + if a in header_map: + return header_map[a] + return None + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--excel", required=True) + p.add_argument("--checkin", required=True) + p.add_argument("--checkout", required=True) + p.add_argument("--guests", required=True, type=int) + p.add_argument("--room") + args = p.parse_args() + + try: + from openpyxl import load_workbook + except Exception: + print(json.dumps({"ok": False, "error": "Нужен openpyxl: pip install openpyxl"}, ensure_ascii=False)) + return + + checkin = d(args.checkin) + checkout = d(args.checkout) + nights = (checkout - checkin).days + if nights <= 0: + print(json.dumps({"ok": False, "error": "checkout должен быть позже checkin"}, ensure_ascii=False)) + return + + wb = load_workbook(args.excel, data_only=True) + ws = wb.active + + rows = list(ws.iter_rows(values_only=True)) + if not rows: + print(json.dumps({"ok": False, "error": "Пустой Excel"}, ensure_ascii=False)) + return + + header = [normalize(str(x) if x is not None else "") for x in rows[0]] + hm = {h: i for i, h in enumerate(header) if h} + + c_from = pick(hm, "date_from", "from", "checkin_from", "заезд_с") + c_to = pick(hm, "date_to", "to", "checkout_to", "выезд_по") + c_gmin = pick(hm, "guests_min", "min_guests", "гостей_от") + c_gmax = pick(hm, "guests_max", "max_guests", "гостей_до") + c_ppn = pick(hm, "price_per_night", "night_price", "цена_за_ночь") + c_total = pick(hm, "total_price", "цена_итого") + c_curr = pick(hm, "currency", "валюта") + c_room = pick(hm, "room", "room_type", "номер") + c_meal = pick(hm, "meal", "питание") + + required = [c_from, c_to] + if any(x is None for x in required) or (c_ppn is None and c_total is None): + print(json.dumps({ + "ok": False, + "error": "Неверная структура Excel. Нужны date_from/date_to и price_per_night или total_price" + }, ensure_ascii=False)) + return + + matches = [] + for r in rows[1:]: + if r is None: + continue + try: + rf = r[c_from] + rt = r[c_to] + if isinstance(rf, datetime): + rf = rf.date() + elif isinstance(rf, str): + rf = d(rf) + if isinstance(rt, datetime): + rt = rt.date() + elif isinstance(rt, str): + rt = d(rt) + except Exception: + continue + + if not (rf <= checkin and rt >= checkout): + continue + + gmin = int(r[c_gmin]) if c_gmin is not None and r[c_gmin] is not None else 1 + gmax = int(r[c_gmax]) if c_gmax is not None and r[c_gmax] is not None else 99 + if not (gmin <= args.guests <= gmax): + continue + + room = str(r[c_room]).strip() if c_room is not None and r[c_room] is not None else "Стандарт" + if args.room and room.lower() != args.room.lower(): + continue + + if c_total is not None and r[c_total] is not None: + total = float(r[c_total]) + else: + total = float(r[c_ppn]) * nights + + matches.append({ + "room": room, + "meal": str(r[c_meal]).strip() if c_meal is not None and r[c_meal] is not None else "без питания", + "currency": str(r[c_curr]).strip() if c_curr is not None and r[c_curr] is not None else "₽", + "total": round(total, 2), + "nights": nights, + }) + + if not matches: + print(json.dumps({"ok": True, "found": 0, "message": "Подходящих тарифов не найдено"}, ensure_ascii=False)) + return + + matches.sort(key=lambda x: x["total"]) + print(json.dumps({"ok": True, "found": len(matches), "options": matches[:3]}, ensure_ascii=False)) + + +if __name__ == "__main__": + main()