skill vysvetlovac
This commit is contained in:
93
.cursor/skills/ems-plan-explain/SKILL.md
Normal file
93
.cursor/skills/ems-plan-explain/SKILL.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: ems-plan-explain
|
||||||
|
description: >-
|
||||||
|
Explains EMS dispatch plans from live Postgres (MCP): why battery/grid/PV/curtailment
|
||||||
|
for a site and time window. If the user does not explicitly name a site (id, code, or
|
||||||
|
unambiguous name), query ems.site (with active plan hint), show a numbered list, and
|
||||||
|
ask which site to use — do not run plan analysis for multiple sites in one turn. Use when the
|
||||||
|
user asks why the plan looks a certain way, planning_interval rows, negative prices,
|
||||||
|
export zero, rolling replan, or says „vysvětli plán“, „proč nabíjí“, „proč škrtí FVE“.
|
||||||
|
---
|
||||||
|
|
||||||
|
# EMS — vysvětlení plánu (živá DB + kontext kódu)
|
||||||
|
|
||||||
|
## Kdy skill použít
|
||||||
|
|
||||||
|
- Otázky typu **proč** plán dělá X (nabíjení, export, curtailment, režim, ceny).
|
||||||
|
- Uživatel zmíní **kód lokality** (`BA81`, …) nebo „aktuální plán“.
|
||||||
|
- Porovnání **model vs realita** (záporná cena, nulový export, pole A/B).
|
||||||
|
|
||||||
|
## Tvrdá pravidla
|
||||||
|
|
||||||
|
1. **Nejdřív data z DB přes MCP** (`user-postgres-ems`, nástroj `query`, pouze `SELECT`). Nevysvětlovat konkrétní sloty „z hlavy“ bez dotazu.
|
||||||
|
2. Pokud MCP selže: uvést **přesnou chybu** a praktické kroky (VPN, MCP zapnutý, dostupnost DB).
|
||||||
|
3. **`site_id` jen po explicitní volbě uživatele** (kód, id, potvrzení jedné řádky), nebo když uživatel **lokalitu v dotazu sám pojmenoval**. Neuvedená lokalita → **nejprve jen dotaz na výběr** (viz Krok 1); **zakázáno** analyzovat plán pro více `site` v jedné odpovědi „preventivně“.
|
||||||
|
4. V odpovědi rozlišit: **co říká plán v DB** vs **co předpokládá LP model** vs **co omeží hardware** (např. taper nabíjení u vysokého SoC dnes v LP **není**).
|
||||||
|
|
||||||
|
## Postup (zkopíruj checklist)
|
||||||
|
|
||||||
|
```
|
||||||
|
- [ ] Zjistit site_id: uživatel ji v dotazu pojmenoval? → případně MCP lookup. Jinak MCP seznam + **zeptat se** (viz Krok 1); až po odpovědi → jedna `site_id`
|
||||||
|
- [ ] MCP: fn_plan_explain_bundle(site_id, hours) — default hours=6
|
||||||
|
- [ ] Z JSONu: operating_mode, grid limity, battery limity, intervals_next_window
|
||||||
|
- [ ] Potřebuji konkrétní čas? → doplnit SELECT na planning_interval (viz reference.md)
|
||||||
|
- [ ] Vysvětlit bilanci slotu + relevantní LP pravidla (solve_dispatch)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Krok 1 — `site_id`
|
||||||
|
|
||||||
|
**Co znamená „lokalita explicitně zmíněná“:** v textu uživatele je **číselné `site_id`**, **kód lokality** (`BA81`, `home-01`, …), nebo **jednoznačný** název/fragment, ze kterého MCP vrátí **právě jednu** řádku `ems.site`.
|
||||||
|
|
||||||
|
- Pokud uživatel dal **`site_id` jako číslo**: ověřit MCP, že řádek v `ems.site` existuje → použít.
|
||||||
|
- Pokud dal **kód nebo část názvu** (`BA81`, …): MCP `select id, code, name from ems.site where code ilike … or name ilike …`.
|
||||||
|
- **0 řádků** → nabídnout seznam z [reference.md §0](reference.md) (všechny lokality) + **zeptat se**, kterou myslí.
|
||||||
|
- **1 řádek** → použít jeho `id`.
|
||||||
|
- **Více řádků** → číslovaný výpis + **zeptat se** na jednu (můžeš hintnout *kdo má aktivní plán*, ale **nepouštěj** analýzu dřív než výběr).
|
||||||
|
- Pokud **lokalita vůbec zmíněná není** („vysvětli plán“, „proč nabíjí“ bez kódu apod.):
|
||||||
|
1. MCP: SQL z **reference.md §0** (seřazený seznam `site` + `active_planning_run_id`).
|
||||||
|
2. V odpovědi uvést **číslovaný seznam** `id | code | name | má aktivní plán?`.
|
||||||
|
3. **Výslovně se zeptat uživatele**, kterou lokalitu myslí (číslo z výpisu, `code`, nebo `id`).
|
||||||
|
4. **`fn_plan_explain_bundle` ani rozšířený SELECT na `planning_interval` pro tuto otázku nespouštěj**, dokud uživatel **nevybere jednu** lokalitu (kód / číslo řádku / id / jednoznačné „tu s BA81“). **Nepředvybírej** „beru první řádek“ ani nespouštěj paralelně bundle pro všechny `site_id` — je to zbytečná zátěž a matoucí výstup.
|
||||||
|
5. Je v DB **jen jeden** záznam `ems.site`: stejně **nejdřív** napiš *která* lokalita to je a **zeptej se** na krátké potvrzení (např. *„Mám plán vysvětlit pro **CODE**?“* / stačí „ano“) — **bez** `fn_plan_explain_bundle` před odpovědí. Výjimku tvoří jen situace, kdy uživatel **v téže zprávě** současně explicitně odkáže na tuto jedinou lokalitu (pak není „neuvedená“).
|
||||||
|
|
||||||
|
### Krok 2 — balík pro vysvětlení
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select ems.fn_plan_explain_bundle(<site_id>, <hours>);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`<hours>`**: default **6**. Jiná hodnota jen když uživatel explicitně chce delší/kratší okno.
|
||||||
|
- Výstup je **jeden JSONB** (`bundle`): viz `.cursor/rules/plan-explain-bundle.mdc` — které klíče číst.
|
||||||
|
|
||||||
|
### Krok 3 — interpretace (struktura odpovědi)
|
||||||
|
|
||||||
|
Krátce a v pořadí:
|
||||||
|
|
||||||
|
1. **Kontext**: `operating_mode.mode_code`, `active_planning_run` (`run_type`, `triggered_by`, `soc_at_replan_wh`, `forecast_correction_factor`).
|
||||||
|
2. **Slot(y)**: z `intervals_next_window` nebo z dodatečného SQL — pro každý relevantní interval:
|
||||||
|
- **Výkon**: `battery_setpoint_w` (+ nabíjení / − vybíjení), `grid_setpoint_w` (+ import / − export), `load_baseline_w`.
|
||||||
|
- **FVE**: `pv_a_forecast_solver_w`, `pv_b_forecast_solver_w`, `pv_a_curtailed_w` (useknuté W na **pole A**).
|
||||||
|
- **Ceny**: `effective_buy_price`, `effective_sell_price`, `is_predicted_price`.
|
||||||
|
- **Exekuce Deye** (pokud je ve sloupcích): `deye_physical_mode`, `deye_gen_cutoff_enabled`.
|
||||||
|
3. **Proč** (odkaz na logiku, ne dlouhá citace):
|
||||||
|
- Záporná **prodejní** cena → export do sítě v LP **neekonomický** / u části instalací **tvrdě 0**; přebytek → nabíjení / curtailment **A** / GEN cutoff (viz `solve_dispatch` v `backend/services/planning_engine.py`).
|
||||||
|
- **Pole B** je v modelu **nekontrolovatelné** — nelze ho `pv_a_curtailed` omezit.
|
||||||
|
- **Zelený bonus** není v účelové funkci LP; počítá se v auditu (`fn_green_bonus_revenue`) — viz `docs/04-modules/planning.md`.
|
||||||
|
4. **Mezery modelu** (upozornit jednou větu, když je to relevantní):
|
||||||
|
- LP používá horní strop **`max_charge_power_w`** bez závislosti na SoC → u vysokého SoC může reálný proud být nižší než plán.
|
||||||
|
|
||||||
|
### Kdy se zeptat uživatele
|
||||||
|
|
||||||
|
- **Lokalita neuvedená nebo nejednoznačná** — vždy **nejdřív** výběr / potvrzení (viz Krok 1); **nikdy** hned neanalyzovat všechny lokality najednou.
|
||||||
|
- **Čas bez časové zóny** („v 11:15“) — potvrdit **Europe/Prague** nebo explicitní offset.
|
||||||
|
- **Širší horizont** než pár hodin — domluvit `hours` nebo přesné `from`/`to` UTC pro doplnkový SELECT.
|
||||||
|
|
||||||
|
## Další SQL a šablony
|
||||||
|
|
||||||
|
→ [reference.md](reference.md)
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Hromadná analýza** (`fn_plan_explain_bundle`, `planning_interval` pro více `site_id`) jen proto, že uživatel **neřekl kterou** lokalitu — vždy se **nejprve** zeptat.
|
||||||
|
- Nevyhledávat plán přes desítky ad-hoc dotazů, když stačí **`fn_plan_explain_bundle`** a případně jeden doplnkový `SELECT` na časové okno.
|
||||||
|
- Nezaměňovat **`pv_a_curtailed_w`** (plán) s tím, **co je vždy zapsané na Modbus** — exekuce curtailmentu na Deye může být instalacně závislá; při pochybnostech říct „ověřit v `docs/05-todo.md` / modbus docs“.
|
||||||
104
.cursor/skills/ems-plan-explain/reference.md
Normal file
104
.cursor/skills/ems-plan-explain/reference.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# EMS plan explain — reference SQL (MCP)
|
||||||
|
|
||||||
|
Všechno jen **read-only** `SELECT`. Server MCP: **`user-postgres-ems`**, nástroj **`query`**, argument `{"sql": "…"}`.
|
||||||
|
|
||||||
|
## 0) Lokalita neuvedená v dotazu — seznam pro výběr
|
||||||
|
|
||||||
|
Spusť jeden dotaz; výsledek **vyrenderuj uživateli jako číslovaný seznam** (`id`, `code`, `name`, příznaky).
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select s.id,
|
||||||
|
s.code,
|
||||||
|
s.name,
|
||||||
|
coalesce(s.active, true) as site_active,
|
||||||
|
pr.id as active_planning_run_id,
|
||||||
|
pr.created_at as active_plan_created_at
|
||||||
|
from ems.site s
|
||||||
|
left join lateral (
|
||||||
|
select id, site_id, created_at
|
||||||
|
from ems.planning_run
|
||||||
|
where site_id = s.id
|
||||||
|
and status = 'active'
|
||||||
|
order by created_at desc
|
||||||
|
limit 1
|
||||||
|
) pr on true
|
||||||
|
order by (pr.id is not null) desc,
|
||||||
|
coalesce(s.active, true) desc,
|
||||||
|
s.id;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Po seznamu vždy zeptej se uživatele** na jednu lokalitu (číslo řádku, `code`, nebo `id`). **Nespouštěj** `fn_plan_explain_bundle` pro více lokalit najednou ani „tiše“ pro první řádek — viz skill `ems-plan-explain` Krok 1. Volitelně můžeš v jedné větě upozornit, kdo má `active_planning_run_id`, ale **výběr nech na uživateli** (u jediného záznamu v tabulce stačí krátké potvrzení typu „ano“).
|
||||||
|
|
||||||
|
Až uživatel lokalitu vybere nebo potvrdí, pokračuj `fn_plan_explain_bundle(s.id, hours)`.
|
||||||
|
|
||||||
|
## 1) `site_id` z kódu lokality
|
||||||
|
|
||||||
|
Nahraď literál v uvozovkách (příklad `BA81`):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select id, code, name, timezone
|
||||||
|
from ems.site
|
||||||
|
where code ilike 'BA81'
|
||||||
|
or name ilike '%BA81%';
|
||||||
|
```
|
||||||
|
|
||||||
|
Pokud více řádků → **zeptat se uživatele**, kterou lokalitu myslí.
|
||||||
|
|
||||||
|
## 2) Primární balík (doporučeno pro vysvětlení)
|
||||||
|
|
||||||
|
Druhý argument = **počet hodin** od začátku aktuálního 15min slotu (UTC), stejně jako plánovač.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select ems.fn_plan_explain_bundle(3, 6) as bundle;
|
||||||
|
```
|
||||||
|
|
||||||
|
Typicky druhý argument **6**. Větší okno jen když uživatel chce delší výhled (více tokenů).
|
||||||
|
|
||||||
|
## 3) Konkrétní sloty v čase (Europe/Prague)
|
||||||
|
|
||||||
|
Intervaly v DB jsou **`timestamptz` (UTC)**. Pro „zítra 11:15“ převeď na UTC v dotazu nebo použij okno:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select pi.interval_start,
|
||||||
|
pi.battery_setpoint_w,
|
||||||
|
pi.grid_setpoint_w,
|
||||||
|
pi.load_baseline_w,
|
||||||
|
pi.pv_a_forecast_solver_w,
|
||||||
|
pi.pv_b_forecast_solver_w,
|
||||||
|
pi.pv_a_curtailed_w,
|
||||||
|
pi.effective_buy_price,
|
||||||
|
pi.effective_sell_price,
|
||||||
|
pi.deye_physical_mode,
|
||||||
|
pi.deye_gen_cutoff_enabled
|
||||||
|
from ems.planning_interval pi
|
||||||
|
where pi.run_id = (
|
||||||
|
select id from ems.planning_run
|
||||||
|
where site_id = 3 and status = 'active'
|
||||||
|
order by created_at desc
|
||||||
|
limit 1
|
||||||
|
)
|
||||||
|
and pi.interval_start >= '2026-04-27T08:00:00+00:00'
|
||||||
|
and pi.interval_start < '2026-04-27T14:00:00+00:00'
|
||||||
|
order by pi.interval_start;
|
||||||
|
```
|
||||||
|
|
||||||
|
Hodnoty `site_id` a časové meziráky nahraď podle kontextu.
|
||||||
|
|
||||||
|
## 4) Žádný aktivní plán
|
||||||
|
|
||||||
|
Když `fn_plan_explain_bundle` vrátí chybu / `no_active_plan`, ověř:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select id, status, run_type, created_at, horizon_start, horizon_end
|
||||||
|
from ems.planning_run
|
||||||
|
where site_id = 3
|
||||||
|
order by created_at desc
|
||||||
|
limit 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5) Dokumentace v repu
|
||||||
|
|
||||||
|
- `docs/07-mcp-postgres-ems.md` — MCP bezpečnost a příklady
|
||||||
|
- `.cursor/rules/plan-explain-bundle.mdc` — co číst z JSONu
|
||||||
|
- `backend/services/planning_engine.py` — `solve_dispatch` (omezení `sell < 0`, `buy < 0`, curtailment)
|
||||||
|
- `docs/04-modules/planning.md` — bilance, účelovka, edge cases
|
||||||
Reference in New Issue
Block a user