Files
GerbilManager/tools/import/rpro3_tickets.py
Gulum 4e30dc7fe3 feat(import): RennmausPro-III-Analyse — Abgleich, Dedup, Feature-Gap, Ticket-Generierung
- compare_rpro3.py: RPRO3-SQLite ↔ aktueller Stand (Dedup Name+DOB+Farbe+Herkunft)
- rpro3-vergleich.md / rpro3-feature-gap.md: erzeugte Reports
- rpro3_tickets.py / rpro3_post_tickets.py: 413 Rückfrage-Tickets mit Herkunft + Links

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:38:24 +02:00

163 lines
6.5 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Erzeugt aus dem RPRO3-Dedup (compare_rpro3.dedup) Rückfrage-Tickets für die Züchterin:
mehrdeutige Namen mit >=2 informativen Varianten (echte „gleich/verschieden?"-Entscheidungen).
Pro Variante wird die HERKUNFT hinterlegt (RennmausPro-Datensatz-Nr., eigenes Tier vs. externer
Ahn, Züchter/Herkunft). Wo ein passendes BESTEHENDES Tier im aktuellen Bestand gefunden wird,
kommt ein Hyperlink in die Frage ([Name ansehen](/rennmaeuse/<id>)), den die Tickets-UI klickbar
rendert. Das Ticket bekommt zusätzlich GerbilId (für den „Datensatz ansehen"-Link in der Kopfzeile)
und ein internes AgentContext (Varianten + rids + gefundene Tier-IDs) für den späteren Importer.
Postet NICHTS — schreibt nur die Kandidaten-JSON. Das Posten macht rpro3_post_tickets.py.
Aufruf: python rpro3_tickets.py <_rpro3.db> <scratch-dir mit cur_gerbils.json> [out.json]
"""
from __future__ import annotations
import sys, json, re, datetime
import compare_rpro3 as C
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
SUFFIX_RE = re.compile(r"\s+(von den |von der |von dem |vom |von |of |gen\.\s|v\.\s|de\s|du\s)", re.IGNORECASE)
def informative(v):
return bool(v["dob"] or v["farbe"] or v["origin"])
def strip_suffix(name):
"""Zucht-Suffix abtrennen (z. B. „Blacky von den Kleinen Chaoten" -> „blacky")."""
n = name or ""
m = SUFFIX_RE.search(n)
if m:
n = n[:m.start()]
return C.norm_name(n)
def parse_dob(s):
if not s:
return None
try:
return datetime.date.fromisoformat(str(s)[:10])
except ValueError:
return None
def build_current_index(scratch):
g = json.load(open(f"{scratch}\\cur_gerbils.json", encoding="utf-8"))
g = g["items"] if isinstance(g, dict) else g
idx_full, idx_strip = {}, {}
for x in g:
rec = {"id": x["id"], "name": x["name"], "dob": parse_dob(x.get("dateOfBirth"))}
idx_full.setdefault(C.norm_name(x["name"]), []).append(rec)
idx_strip.setdefault(strip_suffix(x["name"]), []).append(rec)
return idx_full, idx_strip
def match_current(name, dob, idx_full, idx_strip):
"""Eindeutiges bestehendes Tier zu (Name, DOB) finden — sonst None (keine falschen Links)."""
key = C.norm_name(name)
cands = idx_full.get(key) or idx_strip.get(strip_suffix(name)) or []
if not cands:
return None
if dob:
near = [c for c in cands if c["dob"] and abs((c["dob"] - dob).days) <= 31]
if len(near) == 1:
return near[0]
if near:
return None # mehrdeutig -> nicht verlinken
if len(cands) == 1:
return cands[0]
return None # mehrdeutig -> nicht verlinken
def source_label(v):
rid = v["rids"][0] if v["rids"] else "?"
if v["is_own"]:
return f"eigenes Tier (RennmausPro-Nr. {rid})"
return f"externer Ahn (RennmausPro-Nr. {rid})"
def fmt_variant(letter, v, match):
dob = ", ".join(v["dob"]) or "unbekannt"
farbe = ", ".join(v["farbe"]) or "unbekannt"
orig = ", ".join(v["origin"]) or "unbekannt"
cnt = f", {v['n']}× im Import" if v["n"] > 1 else ""
link = f" — [Tier ansehen](/rennmaeuse/{match['id']})" if match else ""
return (f"• **{letter}**: {source_label(v)} — geboren {dob}, Farbe {farbe}, "
f"Herkunft {orig}{cnt}{link}")
def build_question(name, rows):
lines = [
f"Beim Import aus RennmausPro gibt es mehrere Tiere mit dem Namen „{name}\". "
"Manchmal hat RennmausPro dasselbe Tier mehrfach gespeichert, manchmal sind es "
"wirklich verschiedene Tiere mit zufällig gleichem Namen.",
"",
"Welche davon sind **dasselbe Tier**, welche sind **verschiedene**? "
"Die Herkunft jeder Variante steht dabei:",
"",
]
for letter, v, match in rows:
lines.append(fmt_variant(letter, v, match))
lines += [
"",
"Antworte am einfachsten mit den Buchstaben, z. B.: "
"„A und B sind dasselbe Tier, C ist ein anderes\" · „alle dasselbe\" · „alle verschieden\".",
]
return "\n".join(lines)
def main():
db = sys.argv[1]
scratch = sys.argv[2]
out = sys.argv[3] if len(sys.argv) > 3 else None
R = C.load_rpro3(db)
_, ambiguous, _ = C.dedup(R["animals"])
idx_full, idx_strip = build_current_index(scratch)
tickets = []
for name, variants in ambiguous:
info = [v for v in variants if informative(v)]
if len(info) < 2:
continue
info.sort(key=lambda v: (not v["is_own"], -v["n"]))
rows, matches = [], []
for i, v in enumerate(info):
dob = parse_dob(v["dob"][0]) if v["dob"] else None
m = match_current(name, dob, idx_full, idx_strip)
rows.append((LETTERS[i], v, m))
matches.append(m)
# Ticket-GerbilId: bevorzugt das gematchte eigene Tier, sonst erster Treffer.
primary = next((m for (l, v, m) in rows if v["is_own"] and m), None) or next((m for m in matches if m), None)
agent_ctx = {
"source": "rpro3-ambiguity",
"name": name,
"variants": [
{"label": LETTERS[i], "rids": v["rids"], "count": v["n"],
"dob": v["dob"], "farbe": v["farbe"], "origin": v["origin"],
"is_own": v["is_own"], "matched_gerbil_id": (matches[i]["id"] if matches[i] else None)}
for i, v in enumerate(info)
],
}
tickets.append({
"Message": f"RennmausPro-Import: Name „{name}\" kommt mehrfach vor "
f"({len(info)} Varianten) — dasselbe Tier oder verschiedene?",
"Context": "rpro3-import",
"EntityName": name,
"GerbilId": primary["id"] if primary else None,
"Question": build_question(name, rows),
"AgentContext": json.dumps(agent_ctx, ensure_ascii=False),
"_own": any(v["is_own"] for v in info),
"_nvars": len(info),
"_linked": sum(1 for m in matches if m),
})
tickets.sort(key=lambda t: (not t["_own"], -t["_nvars"]))
linked = sum(t["_linked"] for t in tickets)
print(f"Ticket-Kandidaten: {len(tickets)} (eigenes Tier beteiligt: {sum(1 for t in tickets if t['_own'])})")
print(f"Varianten mit Tier-Hyperlink: {linked}")
print(f"Tickets mit GerbilId (Kopf-Link): {sum(1 for t in tickets if t['GerbilId'])}")
if out:
json.dump(tickets, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(f"geschrieben: {out}")
if __name__ == "__main__":
main()