Aktivnosti su fiksnog vremena, sedmične, van smjena, mogu i vikendom.
- store.py: aktivnosti po djetetu (dan iz svih 7 dana, pocetak, kraj, naziv);
activity_rows/set_activities/activities_for; canonical_dan_any (uklj. subota/nedjelja).
- manifest.py: sta="obaveze" (škola od-do + aktivnosti, sortirano po vremenu) i
sta="aktivnosti"; rade i vikendom/bez škole. Instrumental oblici dana ("petkom",
"subotom") u parsiranju.
- admin: editor aktivnosti na stranici djeteta (dodaj/uredi/obriši).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
187 lines
8.4 KiB
Python
187 lines
8.4 KiB
Python
"""Mini-app 'Raspored časova': odgovara o rasporedu djece za dati dan.
|
||
|
||
Podržava više djece; smjena se računa iz sidra (v. store.py). Sva logika je lokalna
|
||
(bez API-ja), pa je alat inline. Tačan datum se oslanja na sistemski sat (NTP).
|
||
"""
|
||
import datetime as dt
|
||
import logging
|
||
import re
|
||
|
||
from ..base import AppContext, AppSpec, AppTool
|
||
from . import store
|
||
|
||
log = logging.getLogger("idriz.raspored")
|
||
|
||
INSTRUCTIONS = """RASPORED ČASOVA (alat: raspored_upit): odgovaraš o školskom rasporedu djece (koje časove imaju, kad počinju/završavaju, koja je smjena). Podržano je više djece.
|
||
- Kad dijete/roditelj pita o rasporedu ("koje časove Ahmed ima danas?", "kad Hatidža danas počinje/završava školu?", "koja je smjena za Ahmeda sutra?"), pozovi raspored_upit.
|
||
- Kad pita o OBAVEZAMA ("koje obaveze Ahmed ima danas/sutra/petkom?"), koristi sta="obaveze" — alat nabroji školu (od–do) i vannastavne aktivnosti (treninzi, mekteb...) tog dana. Za samo aktivnosti koristi sta="aktivnosti".
|
||
- Argumenti: `dijete` (ime kako je izgovoreno), `kada` (danas/sutra/prekosutra/juce/prekjuce, naziv dana i u obliku "petkom"/"subotom", ili datum kao "15.09." ili "2026-09-15"), i `sta`: casovi | pocetak | kraj | smjena | obaveze | aktivnosti.
|
||
- Ako nije rečeno šta tačno, koristi sta="casovi". Vannastavne aktivnosti nisu vezane za smjenu (fiksno vrijeme, sedmično, mogu biti i vikendom).
|
||
- Alat vraća gotovu rečenicu na bosanskom — izgovori je prirodno. Ne izmišljaj časove ni vremena.
|
||
- Ako alat kaže da dijete nije poznato ili da smjena nije postavljena, prenesi to."""
|
||
|
||
SCHEMA = {
|
||
"type": "function",
|
||
"name": "raspored_upit",
|
||
"description": "Raspored časova za jedno dijete i jedan dan (koji časovi, početak, kraj, smjena).",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"dijete": {"type": "string", "description": "Ime djeteta kako je izgovoreno."},
|
||
"kada": {"type": "string", "description": "danas, sutra, prekosutra, juce, prekjuce, naziv dana, ili datum (npr. '15.09.' ili '2026-09-15'). Prazno = danas."},
|
||
"sta": {"type": "string", "enum": ["casovi", "pocetak", "kraj", "smjena", "obaveze", "aktivnosti"],
|
||
"description": "Šta se traži: 'casovi' (školski časovi), 'pocetak'/'kraj' škole, 'smjena', 'obaveze' (škola + vannastavne aktivnosti zajedno), 'aktivnosti' (samo vannastavne). Podrazumijevano 'casovi'."},
|
||
},
|
||
"required": ["dijete"],
|
||
},
|
||
}
|
||
|
||
_REL_FRAZA = {0: "danas", 1: "sutra", 2: "prekosutra", -1: "jučer", -2: "prekjučer"}
|
||
# akuzativ za "u <dan>" (srijeda/subota/nedjelja mijenjaju oblik)
|
||
_DAN_AKUZATIV = ["ponedjeljak", "utorak", "srijedu", "četvrtak", "petak", "subotu", "nedjelju"]
|
||
|
||
|
||
def _cas_rijec(n: int) -> str:
|
||
if 11 <= n % 100 <= 14:
|
||
return "časova"
|
||
d = n % 10
|
||
return "čas" if d == 1 else ("časa" if 2 <= d <= 4 else "časova")
|
||
|
||
|
||
def _date_fraza(d: dt.date) -> str:
|
||
return f"u {_DAN_AKUZATIV[d.weekday()]}, {d.day}.{d.month}."
|
||
|
||
|
||
def _parse_kada(s: str) -> tuple[dt.date, str]:
|
||
today = dt.date.today()
|
||
s = (s or "").strip()
|
||
if not s:
|
||
return today, "danas"
|
||
sn = store.norm(s)
|
||
rel = {"danas": 0, "sutra": 1, "prekosutra": 2, "juce": -1, "jucer": -1, "prekjuce": -2, "prekjucer": -2}
|
||
if sn in rel:
|
||
d = today + dt.timedelta(days=rel[sn])
|
||
return d, _REL_FRAZA[rel[sn]]
|
||
try:
|
||
d = dt.date.fromisoformat(s)
|
||
return d, _date_fraza(d)
|
||
except ValueError:
|
||
pass
|
||
m = re.match(r"^(\d{1,2})\.\s*(\d{1,2})\.?\s*(\d{2,4})?", s)
|
||
if m:
|
||
dd, mm = int(m.group(1)), int(m.group(2))
|
||
yy = m.group(3)
|
||
year = int(yy) if yy else today.year
|
||
if year < 100:
|
||
year += 2000
|
||
try:
|
||
d = dt.date(year, mm, dd)
|
||
return d, _date_fraza(d)
|
||
except ValueError:
|
||
pass
|
||
# nominativ + akuzativ + instrumental ("petkom", "subotom"...) -> weekday
|
||
wd = {
|
||
"ponedjeljak": 0, "ponedjeljkom": 0,
|
||
"utorak": 1, "utorkom": 1,
|
||
"srijeda": 2, "srijedu": 2, "srijedom": 2,
|
||
"cetvrtak": 3, "cetvrtkom": 3,
|
||
"petak": 4, "petkom": 4,
|
||
"subota": 5, "subotu": 5, "subotom": 5,
|
||
"nedjelja": 6, "nedjelju": 6, "nedjeljom": 6, "nedelja": 6, "nedeljom": 6,
|
||
}
|
||
# naziv dana -> sljedeće NAILAZEĆE pojavljivanje (danas ako je taj dan; inače
|
||
# ova sedmica ako još nije prošao, a sljedeća ako jeste — bitno zbog smjena).
|
||
# "sljedeći/naredni/idući <dan>" uvijek gura u narednu sedmicu.
|
||
qual = any(k in sn for k in ("sljedec", "naredn", "iduc"))
|
||
for wname, wi in wd.items():
|
||
if sn.endswith(wname):
|
||
if qual:
|
||
# dan u narednoj kalendarskoj sedmici
|
||
next_monday = today - dt.timedelta(days=today.weekday()) + dt.timedelta(days=7)
|
||
d = next_monday + dt.timedelta(days=wi)
|
||
else:
|
||
# sljedeće nailazeće pojavljivanje (danas ako je taj dan)
|
||
d = today + dt.timedelta(days=(wi - today.weekday()) % 7)
|
||
return d, _date_fraza(d)
|
||
return today, "danas"
|
||
|
||
|
||
def _obaveze(name: str, date: dt.date, fraza: str, samo_aktivnosti: bool) -> str:
|
||
stavke = [] # (vrijeme_za_sortiranje, tekst)
|
||
napomena = ""
|
||
|
||
if not samo_aktivnosti:
|
||
res = store.lessons_for(name, date)
|
||
st = res["status"]
|
||
if st == "ok":
|
||
casovi = res["casovi"]
|
||
stavke.append((casovi[0]["pocetak"],
|
||
f"škola od {casovi[0]['pocetak']} do {casovi[-1]['kraj']} ({res['smjena']}. smjena)"))
|
||
elif st == "nema_sidra":
|
||
napomena = " (za školu nije postavljena smjena u adminu, pa je ne mogu izračunati)"
|
||
|
||
for a in store.activities_for(name, date):
|
||
raspon = f" od {a['pocetak']} do {a['kraj']}" if a.get("pocetak") else ""
|
||
stavke.append((a.get("pocetak", "99:99"), f"{a['naziv']}{raspon}"))
|
||
|
||
stavke.sort(key=lambda x: x[0] or "99:99")
|
||
sta_rijec = "vannastavnih aktivnosti" if samo_aktivnosti else "obaveza"
|
||
if not stavke:
|
||
return f'Reci djetetu: "{name} {fraza} nema {sta_rijec}.{napomena}"'
|
||
spisak = "; ".join(t for _, t in stavke)
|
||
return f'Reci djetetu prirodno na bosanskom: "{name} {fraza} ima: {spisak}.{napomena}"'
|
||
|
||
|
||
async def raspored_upit(ctx: AppContext, args: dict) -> str:
|
||
ime_izgovoreno = (args.get("dijete") or "").strip()
|
||
name = store.match_child(ime_izgovoreno)
|
||
if not name:
|
||
djeca = store.list_children()
|
||
lista = ", ".join(djeca) if djeca else "nijedno dijete nije dodano"
|
||
return (f"Ne znam za dijete \"{ime_izgovoreno}\". Poznata djeca: {lista}. "
|
||
f"Reci to i pitaj za jedno od njih (ili dodaj dijete u admin panelu).")
|
||
|
||
date, fraza = _parse_kada(args.get("kada", ""))
|
||
sta = (args.get("sta") or "casovi").strip().lower()
|
||
|
||
# OBAVEZE / AKTIVNOSTI: rade i vikendom i kad nema škole
|
||
if sta in ("obaveze", "aktivnosti"):
|
||
return _obaveze(name, date, fraza, samo_aktivnosti=(sta == "aktivnosti"))
|
||
|
||
res = store.lessons_for(name, date)
|
||
st = res["status"]
|
||
|
||
if st == "vikend":
|
||
return f'Reci djetetu: "{name} nema nastave {fraza} — vikend je."'
|
||
if st == "nema_sidra":
|
||
return (f"Za {name} nije postavljena smjena u admin panelu (treba jedan datum i koja je smjena tad bila), "
|
||
f"pa ne mogu izračunati raspored. Reci to.")
|
||
smjena = res["smjena"]
|
||
if st == "nema_casova":
|
||
return f'Reci djetetu: "{name} {fraza} nema časova ({smjena}. smjena)."'
|
||
|
||
casovi = res["casovi"]
|
||
prvi, zadnji = casovi[0], casovi[-1]
|
||
|
||
if sta == "smjena":
|
||
return f'Reci djetetu: "{name} je {fraza} u {smjena}. smjeni."'
|
||
if sta == "pocetak":
|
||
return f'Reci djetetu: "{name} {fraza} treba biti u školi u {prvi["pocetak"]} ({smjena}. smjena)."'
|
||
if sta == "kraj":
|
||
return f'Reci djetetu: "{name} {fraza} završava školu u {zadnji["kraj"]} ({smjena}. smjena)."'
|
||
|
||
# casovi — nabroji
|
||
stavke = "; ".join(f'{c["br"]}. {c["predmet"]} u {c["pocetak"]}' for c in casovi)
|
||
recenica = (f"{name} {fraza} ima {len(casovi)} {_cas_rijec(len(casovi))} u {smjena}. smjeni "
|
||
f"(od {prvi['pocetak']} do {zadnji['kraj']}): {stavke}.")
|
||
return f'Reci djetetu prirodno na bosanskom, možeš i sažeti ako je dugačko: "{recenica}"'
|
||
|
||
|
||
SPEC = AppSpec(
|
||
name="raspored",
|
||
title="Raspored časova",
|
||
instructions=INSTRUCTIONS,
|
||
greeting="reći raspored časova za djecu (kad počinju/završavaju, koji časovi)",
|
||
tools=[AppTool(SCHEMA, raspored_upit, "inline")],
|
||
)
|