Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016679RwmHsuTFfxt26wP6rk
138 lines
7.1 KiB
Python
138 lines
7.1 KiB
Python
"""Lēmuma punkta strukturēšana ar noteikumiem (bez valodas modeļa): darbības veids, adresāti un termiņš.
|
|
|
|
Punkta teksts paliek burtiski; šie lauki ir tikai norādes, kas izgūtas no teksta:
|
|
kind — pēc punkta sākuma darbības vārda (Pieņemt zināšanai, Atbalstīt, Uzdot …);
|
|
parties — institūcijas, kas tekstā minētas datīvā („Ekonomikas ministrijai”; ne „… pašvaldībai piederošs”), ar VPK ID,
|
|
un pašvaldības ar ATVK kodu no Pašvaldību reģistra;
|
|
deadline — „līdz 2026. gada 10. novembrim” → 2026-11-10.
|
|
Ja kaut ko nevar droši noteikt, lauks paliek tukšs — neko nemin.
|
|
"""
|
|
import re
|
|
|
|
from lxml import etree
|
|
|
|
MONTHS = {"janvārim": 1, "februārim": 2, "martam": 3, "aprīlim": 4, "maijam": 5, "jūnijam": 6, "jūlijam": 7,
|
|
"augustam": 8, "septembrim": 9, "oktobrim": 10, "novembrim": 11, "decembrim": 12}
|
|
RE_DATE = re.compile(r"līdz\s+(\d{4})\.\s*gada\s+(\d{1,2})\.\s*(" + "|".join(MONTHS) + r")", re.I)
|
|
RE_DATE2 = re.compile(r"līdz\s+(\d{1,2})\.(\d{1,2})\.(\d{4})\.?")
|
|
|
|
# darbības vārds → darbības veids. Meklē pirmo no šiem vārdiem punkta tekstā (ārpus pēdiņām), jo punkts var sākties
|
|
# ar pamatojumu („Saskaņā ar … likuma 4. pantu atļaut …”, „Lai nodrošinātu …, atbalstīt …”).
|
|
KINDS = [
|
|
("pieņemt zināšanai", "note"), ("zināšanai", "note"), ("ņemt vērā", "note"),
|
|
("atzīt par spēku zaudējušu", "repeal"), ("atcelt", "repeal"),
|
|
("pieņemt", "adopt"), ("apstiprināt", "approve"), ("atbalstīt", "support"), ("neatbalstīt", "reject"),
|
|
("aicināt", "invite"), ("uzdot", "task"), ("piešķirt", "allocate"), ("atļaut", "permit"), ("pilnvarot", "authorise"),
|
|
("noteikt", "determine"), ("atzīt", "determine"), ("izteikt", "amend"), ("papildināt", "amend"), ("svītrot", "amend"),
|
|
("izdarīt", "amend"), ("grozīt", "amend"), ("nodot", "transfer"), ("pārņemt", "transfer"), ("izveidot", "establish"),
|
|
("iecelt", "appoint"), ("atbrīvot", "dismiss"), ("pirkt", "purchase"), ("iegādāties", "purchase"),
|
|
]
|
|
KIND_VALUES = sorted({k for _, k in KINDS} | {"task", "part", "provision"})
|
|
|
|
|
|
def dative_word(w):
|
|
lw = w.lower()
|
|
for a, b in (("ija", "ijai"), ("ais", "ajam"), ("ā", "ajai"), ("is", "im"), ("us", "um"), ("a", "ai"), ("e", "ei"),
|
|
("s", "am"), ("š", "am")):
|
|
if lw.endswith(a):
|
|
return w[: len(w) - len(a)] + b
|
|
return None
|
|
|
|
|
|
def dative(name):
|
|
"""„Valsts ieņēmumu dienests” → „Valsts ieņēmumu dienestam”; „Centrālā finanšu un līgumu aģentūra” → „Centrālajai finanšu un
|
|
līgumu aģentūrai”. Locās galvenais vārds (pēdējais) un noteiktās galotnes īpašības vārdi (-ais, -ā); ģenitīva vārdi paliek."""
|
|
words = name.split()
|
|
d = dative_word(words[-1])
|
|
if not d:
|
|
return None
|
|
out = [dative_word(w) if re.search(r"(ais|ā)$", w.lower()) else w for w in words[:-1]] + [d]
|
|
return " ".join(out)
|
|
|
|
|
|
def title_case(name):
|
|
"""Reģistrā budžeta iestāžu nosaukumi ir lielajiem burtiem; salīdzināšanai — mazajiem."""
|
|
return name.lower()
|
|
|
|
|
|
class Registers:
|
|
"""Valsts institūciju reģistrs (VPK ID) un Pašvaldību reģistrs (ATVK) → datīva formas."""
|
|
|
|
def __init__(self, org_xml, pas_xml):
|
|
self.forms = [] # (forma mazajiem burtiem, 'org'|'atvk', id)
|
|
r = etree.fromstring(org_xml)
|
|
for o in r:
|
|
if not isinstance(o.tag, str) or etree.QName(o).localname != "Organization" or o.get("validTo"):
|
|
continue
|
|
name = next((c.text for c in o if etree.QName(c).localname == "Name"), "") or ""
|
|
name = re.sub(r"^LATVIJAS REPUBLIKAS ", "", name)
|
|
if o.get("kind") in ("group", "office"):
|
|
continue
|
|
d = dative(title_case(name))
|
|
if d:
|
|
self.forms.append((d, "org", o.get("id")))
|
|
if o.get("kind") == "ministry" and title_case(name).endswith(" ministrija"):
|
|
stem = title_case(name)[: -len("ministrija")]
|
|
self.forms += [(stem + "ministram", "minister", o.get("id")), (stem + "ministrei", "minister", o.get("id"))]
|
|
self.forms.append(("ministru prezidentam", "org", "00-0001"))
|
|
p = etree.fromstring(pas_xml)
|
|
for m in p:
|
|
if not isinstance(m.tag, str) or etree.QName(m).localname != "Municipality":
|
|
continue
|
|
name = next(c.text for c in m if etree.QName(c).localname == "Name")
|
|
if m.get("kind") == "novads":
|
|
stem = name[: -len(" novads")]
|
|
forms = [f"{stem} novada pašvaldībai"]
|
|
else:
|
|
gen = name + "s" if name[-1] in "ae" else name
|
|
forms = [f"{gen} valstspilsētas pašvaldībai", f"{gen} pašvaldībai"]
|
|
for f in forms:
|
|
self.forms.append((f.lower(), "atvk", m.get("atvk")))
|
|
# garākās formas pirmās, lai „Valsts ieņēmumu dienestam” nesakristu ar īsāku formu tās iekšienē
|
|
self.forms.sort(key=lambda x: -len(x[0]))
|
|
|
|
def parties(self, text):
|
|
low, found, taken = text.lower(), [], []
|
|
for form, kind, ident in self.forms:
|
|
for m in re.finditer(r"(?<![\wāčēģīķļņšūž])" + re.escape(form) + r"(?![\wāčēģīķļņšūž])", low):
|
|
if any(m.start() < e and s < m.end() for s, e in taken):
|
|
continue
|
|
if re.match(r"\s+piederoš", low[m.end():]): # „pašvaldībai piederošo īpašumu” — piederība, ne adresāts
|
|
continue
|
|
taken.append((m.start(), m.end()))
|
|
found.append((m.start(), kind, ident, text[m.start():m.end()]))
|
|
seen, out = set(), []
|
|
for _, kind, ident, printed in sorted(found):
|
|
if (kind, ident) not in seen:
|
|
seen.add((kind, ident))
|
|
out.append((kind, ident, printed))
|
|
return out
|
|
|
|
|
|
def kind_of(text, parties, is_child=False):
|
|
"""Darbības veids. Apakšpunkts, kas turpina augstākā punkta teikumu (sākas ar mazo burtu), ir „part”."""
|
|
if is_child and text[:1].islower():
|
|
return "part"
|
|
low = re.sub(r'"[^"]*"|„[^”]*”', " ", text.lower())
|
|
low = re.sub(r"^lai [^,]*,\s*", "", low)
|
|
if parties and any(low.startswith(p[2].lower()) for p in parties):
|
|
return "task" # „Ekonomikas ministrijai … iesniegt …”
|
|
if re.match(r"^[a-zāčēģīķļņšūž ]+(ministram|ministrei|ministrijai|kancelejai|pašvaldībai)\b", low):
|
|
return "task"
|
|
best = None
|
|
for verb, k in KINDS:
|
|
m = re.search(r"(?<![a-zāčēģīķļņšūž])" + verb + r"(?![a-zāčēģīķļņšūž])", low)
|
|
if m and (best is None or m.start() < best[0]):
|
|
best = (m.start(), k)
|
|
return best[1] if best else "provision"
|
|
|
|
|
|
def deadline(text):
|
|
m = RE_DATE.search(text)
|
|
if m:
|
|
return f"{int(m.group(1)):04d}-{MONTHS[m.group(3).lower()]:02d}-{int(m.group(2)):02d}", m.group(0)
|
|
m = RE_DATE2.search(text)
|
|
if m:
|
|
return f"{int(m.group(3)):04d}-{int(m.group(2)):02d}-{int(m.group(1)):02d}", m.group(0)
|
|
return None, None
|