Files
idriz/idriz/apps/raspored/manifest.py
idriz cb4e021111 Raspored: vannastavne aktivnosti (treninzi, mekteb) + upit "obaveze"
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
2026-09-13 08:41:33 +02:00

187 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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")],
)