- Removes source_dialect and target_dialect from TerminologyDictionary model, schemas, routes, helper serialization, and all tests - Adds allowed_languages config field (BCP-47 language codes) to GlobalSettings with validation, consolidated settings response, and API endpoint - Adds alembic migration f1a2b3c4d5e6 to drop the two columns
193 lines
9.7 KiB
Python
193 lines
9.7 KiB
Python
# #region DictionaryCorrectionService [C:3] [TYPE Class] [SEMANTICS dictionary, correction, bulk]
|
|
# @BRIEF Submit term corrections and bulk corrections to dictionaries with conflict detection.
|
|
# @RELATION DEPENDS_ON -> [DictionaryEntry]
|
|
# @RELATION DEPENDS_ON -> [TerminologyDictionary]
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ...core.logger import belief_scope, logger
|
|
from ...models.translate import DictionaryEntry, TerminologyDictionary
|
|
from ._utils import _normalize_term
|
|
|
|
|
|
class DictionaryCorrectionService:
|
|
"""Submit term corrections to dictionaries with conflict detection and bulk support."""
|
|
|
|
# region submit_correction [TYPE Function]
|
|
# @PURPOSE: Submit a term correction from a run result.
|
|
@staticmethod
|
|
def submit_correction(
|
|
db: Session,
|
|
dict_id: str,
|
|
source_term: str,
|
|
incorrect_target_term: str,
|
|
corrected_target_term: str,
|
|
origin_run_id: str | None = None,
|
|
origin_row_key: str | None = None,
|
|
origin_user_id: str | None = None,
|
|
on_conflict: str = "overwrite",
|
|
context_data: dict[str, Any] | None = None,
|
|
usage_notes: str | None = None,
|
|
keep_context: bool = True,
|
|
) -> dict[str, Any]:
|
|
with belief_scope("DictionaryCorrectionService.submit_correction"):
|
|
dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()
|
|
if not dictionary:
|
|
raise ValueError(f"Dictionary '{dict_id}' not found")
|
|
|
|
effective_context = context_data
|
|
effective_context_source = "auto"
|
|
if not keep_context:
|
|
effective_context = None
|
|
effective_context_source = "manual"
|
|
|
|
normalized = _normalize_term(source_term)
|
|
entry_src_lang = "und"
|
|
entry_tgt_lang = "und"
|
|
|
|
existing = (
|
|
db.query(DictionaryEntry)
|
|
.filter(
|
|
DictionaryEntry.dictionary_id == dict_id,
|
|
DictionaryEntry.source_term_normalized == normalized,
|
|
or_(
|
|
(DictionaryEntry.source_language == entry_src_lang) & (DictionaryEntry.target_language == entry_tgt_lang),
|
|
(DictionaryEntry.source_language == "und") & (DictionaryEntry.target_language == "und"),
|
|
),
|
|
)
|
|
.first()
|
|
)
|
|
|
|
result: dict[str, Any] = {"source_term": source_term, "target_term": corrected_target_term, "action": "created", "entry_id": None, "conflict": None, "message": None}
|
|
|
|
if existing:
|
|
if on_conflict == "keep_existing":
|
|
result["action"] = "conflict_detected"
|
|
result["conflict"] = {"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target_term, "action": "keep_existing"}
|
|
result["message"] = f"Existing entry kept: '{existing.target_term}'"
|
|
return result
|
|
elif on_conflict == "overwrite":
|
|
existing.target_term = corrected_target_term.strip()
|
|
if origin_run_id:
|
|
existing.origin_run_id = origin_run_id
|
|
if origin_row_key:
|
|
existing.origin_row_key = origin_row_key
|
|
if origin_user_id:
|
|
existing.origin_user_id = origin_user_id
|
|
if context_data is not None or not keep_context:
|
|
existing.context_data = effective_context
|
|
existing.has_context = bool(effective_context)
|
|
existing.context_source = effective_context_source
|
|
if usage_notes is not None:
|
|
existing.usage_notes = usage_notes
|
|
db.flush()
|
|
result["action"] = "updated"
|
|
result["entry_id"] = existing.id
|
|
result["message"] = f"Entry updated from '{existing.target_term}' to '{corrected_target_term}'"
|
|
else:
|
|
result["action"] = "skipped"
|
|
result["conflict"] = {"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target_term, "action": "cancel"}
|
|
result["message"] = "Correction cancelled by conflict mode"
|
|
return result
|
|
else:
|
|
entry = DictionaryEntry(
|
|
dictionary_id=dict_id, source_term=source_term.strip(),
|
|
source_term_normalized=normalized, target_term=corrected_target_term.strip(),
|
|
source_language=entry_src_lang, target_language=entry_tgt_lang,
|
|
context_data=effective_context, usage_notes=usage_notes,
|
|
has_context=bool(effective_context), context_source=effective_context_source if effective_context else None,
|
|
origin_run_id=origin_run_id, origin_row_key=origin_row_key, origin_user_id=origin_user_id,
|
|
)
|
|
db.add(entry)
|
|
db.flush()
|
|
result["entry_id"] = entry.id
|
|
result["message"] = f"Entry created for '{source_term}' -> '{corrected_target_term}'"
|
|
|
|
db.commit()
|
|
logger.reflect("Correction processed", result)
|
|
return result
|
|
# endregion submit_correction
|
|
|
|
# region submit_bulk_corrections [TYPE Function]
|
|
# @PURPOSE: Submit multiple term corrections atomically.
|
|
@staticmethod
|
|
def submit_bulk_corrections(
|
|
db: Session,
|
|
dict_id: str,
|
|
corrections: list[dict[str, Any]],
|
|
origin_user_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
with belief_scope("DictionaryCorrectionService.submit_bulk_corrections"):
|
|
dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()
|
|
if not dictionary:
|
|
raise ValueError(f"Dictionary '{dict_id}' not found")
|
|
|
|
results: list[dict[str, Any]] = []
|
|
conflicts: list[dict[str, Any]] = []
|
|
all_ok = True
|
|
|
|
for corr in corrections:
|
|
source_term = corr.get("source_term", "").strip()
|
|
corrected_target = corr.get("corrected_target_term", "").strip()
|
|
|
|
if not source_term or not corrected_target:
|
|
results.append({"source_term": source_term, "action": "error", "message": "source_term and corrected_target_term are required"})
|
|
all_ok = False
|
|
continue
|
|
|
|
normalized = _normalize_term(source_term)
|
|
bulk_src_lang = "und"
|
|
bulk_tgt_lang = "und"
|
|
|
|
existing = (
|
|
db.query(DictionaryEntry)
|
|
.filter(
|
|
DictionaryEntry.dictionary_id == dict_id,
|
|
DictionaryEntry.source_term_normalized == normalized,
|
|
or_(
|
|
(DictionaryEntry.source_language == bulk_src_lang) & (DictionaryEntry.target_language == bulk_tgt_lang),
|
|
(DictionaryEntry.source_language == "und") & (DictionaryEntry.target_language == "und"),
|
|
),
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if existing:
|
|
on_conflict = corr.get("on_conflict", "overwrite")
|
|
if on_conflict == "keep_existing":
|
|
conflicts.append({"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target, "action": "keep_existing"})
|
|
results.append({"source_term": source_term, "action": "conflict_detected", "message": f"Existing entry kept: '{existing.target_term}'"})
|
|
continue
|
|
elif on_conflict == "overwrite":
|
|
existing.target_term = corrected_target
|
|
existing.origin_user_id = origin_user_id
|
|
results.append({"source_term": source_term, "action": "updated", "entry_id": existing.id, "message": f"Updated to '{corrected_target}'"})
|
|
else:
|
|
conflicts.append({"source_term": source_term, "existing_target_term": existing.target_term, "submitted_target_term": corrected_target, "action": "cancel"})
|
|
results.append({"source_term": source_term, "action": "skipped", "message": "Cancelled by conflict mode"})
|
|
else:
|
|
entry = DictionaryEntry(
|
|
dictionary_id=dict_id, source_term=source_term, source_term_normalized=normalized,
|
|
target_term=corrected_target, source_language=bulk_src_lang, target_language=bulk_tgt_lang,
|
|
origin_run_id=corr.get("origin_run_id"), origin_row_key=corr.get("origin_row_key"),
|
|
origin_user_id=origin_user_id,
|
|
)
|
|
db.add(entry)
|
|
results.append({"source_term": source_term, "action": "created", "message": f"Entry created for '{source_term}' -> '{corrected_target}'"})
|
|
|
|
if conflicts and not all_ok:
|
|
db.rollback()
|
|
return {"status": "conflicts", "results": results, "conflicts": conflicts, "message": "Conflicts detected. Resolve before retrying."}
|
|
|
|
db.commit()
|
|
return {
|
|
"status": "completed", "results": results, "conflicts": conflicts,
|
|
"total": len(corrections), "applied": sum(1 for r in results if r["action"] in ("created", "updated")),
|
|
}
|
|
# endregion submit_bulk_corrections
|
|
|
|
# #endregion DictionaryCorrectionService
|