- Created tables for AI benchmark runs and results to facilitate model evaluation. - Added expected answers column to benchmark results. - Introduced tables for internet connection change cases and vTiger archive management, including records, relations, and checkpoints. - Implemented triggers to enforce append-only behavior for vTiger archive records and files. - Enhanced solution management with soft delete capabilities for sag_solutions and knowledge_articles. feat(scripts): add CRM benchmarking script for Ollama models - Developed a Python script to benchmark CRM models exposed through Ollama, including various test cases and scoring mechanisms. test(tests): add comprehensive tests for new features - Implemented tests for internet change case service, vTiger archive functionality, and sag solution knowledge management. - Ensured coverage for edge cases and error handling in the new features.
169 lines
8.1 KiB
Python
169 lines
8.1 KiB
Python
"""Create deduplicated procurement cases for externally detected internet changes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any, Mapping, Optional
|
|
|
|
from app.core.config import settings
|
|
from app.core.database import execute_query, execute_query_single
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
RELEVANT_FIELDS = {
|
|
"address", "service_address", "monthly_cost", "sales_price", "technology",
|
|
"connection_type", "circuit_number", "provider_reference", "provider", "vendor_id",
|
|
"speed_mbps", "download_mbps", "upload_mbps", "status", "sla_subscription_id",
|
|
"sla_price", "sla_status", "ip_range", "cidr", "contract_number", "range_added",
|
|
"range_removed", "range_monthly_cost", "range_sales_price",
|
|
}
|
|
|
|
FIELD_LABELS = {
|
|
"address": "Adresse", "service_address": "Serviceadresse", "monthly_cost": "Indkøbspris",
|
|
"sales_price": "Salgspris", "technology": "Teknologi", "connection_type": "Forbindelsestype",
|
|
"circuit_number": "Kredsløbsnummer", "provider_reference": "Leverandørreference",
|
|
"provider": "Leverandør", "vendor_id": "Leverandør", "speed_mbps": "Hastighed",
|
|
"download_mbps": "Download", "upload_mbps": "Upload", "status": "Status",
|
|
"sla_subscription_id": "SLA-aftale", "sla_price": "SLA-pris", "sla_status": "SLA-status",
|
|
"ip_range": "IP-range", "cidr": "IP-range", "contract_number": "Kontraktnummer",
|
|
"range_added": "Nyt IP-range", "range_removed": "Fjernet IP-range",
|
|
"range_monthly_cost": "IP-range indkøbspris", "range_sales_price": "IP-range salgspris",
|
|
}
|
|
|
|
|
|
def filter_relevant_changes(changes: Mapping[str, Any] | None) -> dict[str, dict[str, Any]]:
|
|
filtered: dict[str, dict[str, Any]] = {}
|
|
for field, raw in (changes or {}).items():
|
|
if field not in RELEVANT_FIELDS:
|
|
continue
|
|
change = raw if isinstance(raw, Mapping) else {"from": None, "to": raw}
|
|
before, after = change.get("from"), change.get("to")
|
|
if str(before or "").strip() == str(after or "").strip():
|
|
continue
|
|
filtered[field] = {"from": before, "to": after}
|
|
return filtered
|
|
|
|
|
|
def _procurement_customer_id() -> int:
|
|
configured = getattr(settings, "PROCUREMENT_CASE_CUSTOMER_ID", None)
|
|
if configured:
|
|
row = execute_query_single("SELECT id FROM customers WHERE id=%s AND is_active=true", (configured,))
|
|
if row:
|
|
return int(row["id"])
|
|
row = execute_query_single(
|
|
"""SELECT id FROM customers WHERE is_active=true AND LOWER(name) LIKE %s
|
|
ORDER BY CASE WHEN LOWER(name) LIKE %s THEN 0 ELSE 1 END, id LIMIT 1""",
|
|
("%bmc%", "%bmc networks%"),
|
|
)
|
|
if not row:
|
|
raise ValueError("BMC's interne indkøbskunde blev ikke fundet")
|
|
return int(row["id"])
|
|
|
|
|
|
def _economy_group_id() -> Optional[int]:
|
|
row = execute_query_single(
|
|
"""SELECT id FROM groups WHERE LOWER(name) LIKE ANY(%s)
|
|
ORDER BY id LIMIT 1""", (["%økonomi%", "%okonomi%", "%economic%"],),
|
|
)
|
|
return int(row["id"]) if row else None
|
|
|
|
|
|
def _render_description(
|
|
*, connection_id: int, connection_name: str, reference: str, provider: str,
|
|
source_label: str, source_url: Optional[str], changes: Mapping[str, Mapping[str, Any]],
|
|
) -> str:
|
|
lines = [
|
|
"Automatisk oprettet efter en ekstern ændring af en internetforbindelse.", "",
|
|
f"Forbindelse: {connection_name}", f"Reference: {reference or '-'}",
|
|
f"Leverandør: {provider or '-'}", f"Kilde: {source_label}",
|
|
f"Link til forbindelse: /economy/internet-connections/{connection_id}",
|
|
]
|
|
if source_url:
|
|
lines.append(f"Link til kilde: {source_url}")
|
|
lines.extend(["", "Registrerede ændringer:"])
|
|
for field, change in changes.items():
|
|
lines.append(f"- {FIELD_LABELS.get(field, field)}: {change.get('from')} → {change.get('to')}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def ensure_external_change_case(
|
|
*, connection_id: int, source_type: str, source_key: str, source_label: str,
|
|
changes: Mapping[str, Any], connection_name: str = "Internetforbindelse",
|
|
reference: str = "", provider: str = "", owner_customer_id: Optional[int] = None,
|
|
source_url: Optional[str] = None,
|
|
) -> dict[str, Any]:
|
|
relevant = filter_relevant_changes(changes)
|
|
if not relevant:
|
|
return {"case_id": None, "created": False, "changes": {}, "error": None}
|
|
source_type = str(source_type or "external").strip().lower()
|
|
source_key = str(source_key or "").strip()
|
|
if not source_key:
|
|
return {"case_id": None, "created": False, "changes": relevant, "error": "Kilden mangler en stabil nøgle"}
|
|
|
|
try:
|
|
existing = execute_query_single(
|
|
"""SELECT id, sag_id, changes FROM internet_connection_change_cases
|
|
WHERE connection_id=%s AND source_type=%s AND source_key=%s""",
|
|
(connection_id, source_type, source_key),
|
|
)
|
|
merged = dict((existing or {}).get("changes") or {})
|
|
merged.update(relevant)
|
|
case_customer_id = int(owner_customer_id) if owner_customer_id else _procurement_customer_id()
|
|
title = f"Internetændring {reference or connection_name} · {source_label}"[:255]
|
|
description = _render_description(
|
|
connection_id=connection_id, connection_name=connection_name, reference=reference,
|
|
provider=provider, source_label=source_label, source_url=source_url, changes=merged,
|
|
)
|
|
case_id = int(existing["sag_id"]) if existing and existing.get("sag_id") else None
|
|
created = False
|
|
if case_id:
|
|
execute_query(
|
|
"UPDATE sag_sager SET beskrivelse=%s, updated_at=NOW() WHERE id=%s AND deleted_at IS NULL",
|
|
(description, case_id), fetch=False,
|
|
)
|
|
else:
|
|
row = execute_query_single(
|
|
"""INSERT INTO sag_sager
|
|
(titel, beskrivelse, type, status, customer_id, assigned_group_id, created_by_user_id)
|
|
VALUES (%s,%s,'indkøb','åben',%s,%s,1) RETURNING id""",
|
|
(title, description, case_customer_id, _economy_group_id()),
|
|
)
|
|
case_id = int(row["id"])
|
|
created = True
|
|
|
|
if existing:
|
|
execute_query(
|
|
"""UPDATE internet_connection_change_cases SET sag_id=%s, source_label=%s,
|
|
source_url=%s, changes=%s::jsonb, last_error=NULL, updated_at=NOW() WHERE id=%s""",
|
|
(case_id, source_label, source_url, json.dumps(merged, ensure_ascii=False), existing["id"]), fetch=False,
|
|
)
|
|
else:
|
|
execute_query(
|
|
"""INSERT INTO internet_connection_change_cases
|
|
(connection_id,source_type,source_key,source_label,source_url,sag_id,changes)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s::jsonb)""",
|
|
(connection_id, source_type, source_key, source_label, source_url, case_id,
|
|
json.dumps(merged, ensure_ascii=False)), fetch=False,
|
|
)
|
|
return {"case_id": case_id, "created": created, "changes": merged, "error": None}
|
|
except Exception as exc:
|
|
logger.warning("Could not create internet change case for connection %s: %s", connection_id, exc)
|
|
# Keep the import operational, but persist a visible control item whenever
|
|
# the audit table itself is available.
|
|
try:
|
|
execute_query(
|
|
"""INSERT INTO internet_connection_change_cases
|
|
(connection_id,source_type,source_key,source_label,source_url,changes,last_error)
|
|
VALUES (%s,%s,%s,%s,%s,%s::jsonb,%s)
|
|
ON CONFLICT (connection_id,source_type,source_key) DO UPDATE
|
|
SET changes=internet_connection_change_cases.changes || EXCLUDED.changes,
|
|
last_error=EXCLUDED.last_error, updated_at=NOW()""",
|
|
(connection_id, source_type, source_key, source_label, source_url,
|
|
json.dumps(relevant, ensure_ascii=False), str(exc)),
|
|
fetch=False,
|
|
)
|
|
except Exception:
|
|
logger.exception("Could not persist failed internet change-case audit")
|
|
return {"case_id": None, "created": False, "changes": relevant, "error": str(exc)}
|