Files

107 lines
3.6 KiB
Python
Raw Permalink 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.
"""Deterministic rules: Misc evaluation, match calculation, description markdown.
Replicates SKILL.md Schritt 3b/4/5. No LLM here — the numbers written to the
CRM must never be hallucinated.
"""
from __future__ import annotations
import datetime as dt
import json
import urllib.parse
import urllib.request
from math import asin, cos, radians, sin, sqrt
SAUERLACH = (47.9721357, 11.6528398)
NOMINATIM_UA = "projekt-matching/1.0 (Thomas.Langer@destengs.com)"
YES, NO, UNKNOWN = "yes", "no", "unknown"
SYMBOL = {YES: "", NO: "", UNKNOWN: ""}
RATING_VALUE = {YES: 100, UNKNOWN: 25, NO: 0}
KAT_ORDER = {"Must": 0, "Nice": 1, "Misc": 2}
def geocode(place: str):
"""Nominatim lookup -> (lat, lon) or None. Policy: max 1 request/s."""
url = "https://nominatim.openstreetmap.org/search?" + urllib.parse.urlencode(
{"q": place, "format": "json", "countrycodes": "de", "limit": 1})
req = urllib.request.Request(url, headers={"User-Agent": NOMINATIM_UA})
data = json.load(urllib.request.urlopen(req, timeout=20))
return (float(data[0]["lat"]), float(data[0]["lon"])) if data else None
def distance_to_sauerlach_km(place: str, geocode_fn=geocode):
"""Haversine distance; None on geocoding failure/ambiguity."""
try:
pos = geocode_fn(place)
except Exception:
return None
if pos is None:
return None
dlat = radians(pos[0] - SAUERLACH[0])
dlon = radians(pos[1] - SAUERLACH[1])
h = (sin(dlat / 2) ** 2
+ cos(radians(SAUERLACH[0])) * cos(radians(pos[0])) * sin(dlon / 2) ** 2)
return 2 * 6371 * asin(sqrt(h))
def eval_misc(item: dict, today: dt.date, geocode_fn=geocode) -> str:
kind = item.get("miscType") or "other"
if kind == "security":
return NO # K.-o.-Kriterium (SÜ dauert >= 3 Monate)
if kind == "start":
iso = item.get("startDate")
if not iso:
return UNKNOWN
try:
start = dt.date.fromisoformat(iso)
except ValueError:
return UNKNOWN
return YES if start <= today + dt.timedelta(days=56) else NO
if kind == "workload":
pct = item.get("workloadPercent")
if pct is None:
return UNKNOWN
return YES if 75 <= pct <= 100 else UNKNOWN
if kind == "duration":
return YES
if kind == "location":
if item.get("remotePercent") == 100:
return YES
place = item.get("onsiteLocation")
if not place:
return NO # Einsatzort/Remote-Anteil unklar
km = distance_to_sauerlach_km(place, geocode_fn)
if km is None:
return UNKNOWN
if km <= 50:
return YES
if km <= 60:
return UNKNOWN
return NO
return UNKNOWN
def calc_match(rows: list, kat: str):
vals = [RATING_VALUE[r["rating"]] for r in rows if r["kat"] == kat]
if not vals:
return None
return int(sum(vals) / len(vals) + 0.5) # half-up, not banker's rounding
def fmt_match(value):
return "" if value is None else f"{value} %"
def order_rows(rows: list) -> list:
return sorted(rows, key=lambda r: KAT_ORDER[r["kat"]]) # stable sort
def build_description(rows: list) -> str:
"""rows already ordered Must -> Nice -> Misc (use order_rows)."""
must, nice = calc_match(rows, "Must"), calc_match(rows, "Nice")
lines = [f"Must-have-Match: {fmt_match(must)} · Nice-to-have-Match: {fmt_match(nice)}",
"", "| Nr. | Kat. | ❔ | Anforderung |", "|---|---|---|---|"]
for i, r in enumerate(rows, 1):
lines.append(f"| {i} | {r['kat']} | {SYMBOL[r['rating']]} | {r['text']} |")
return "\n".join(lines)