"""统一反馈中心数据映射器。""" from __future__ import annotations from datetime import date, datetime from decimal import Decimal from typing import Any from .reason_catalog import build_reason_candidates def _jsonable(value: Any) -> Any: if value is None: return None if isinstance(value, (str, int, float, bool)): return value if isinstance(value, Decimal): return float(value) if isinstance(value, (datetime, date)): return value.isoformat() if isinstance(value, dict): return {str(key): _jsonable(item) for key, item in value.items()} if isinstance(value, (list, tuple, set)): return [_jsonable(item) for item in value] model_dump = getattr(value, "model_dump", None) if callable(model_dump): return _jsonable(model_dump(mode="json")) if hasattr(value, "__dict__"): raw = {key: item for key, item in vars(value).items() if not key.startswith("_")} if raw: return _jsonable(raw) return str(value) def _safe_int(value: Any) -> int | None: try: if value is None or value == "": return None return int(str(value).strip()) except (TypeError, ValueError): return None def _extract_task_breakdown_object(task: Any) -> dict[str, Any]: payload = getattr(task, "ai_breakdown_payload", None) if isinstance(payload, str): try: import json payload = json.loads(payload) except Exception: payload = None if isinstance(payload, dict): if isinstance(payload.get("task"), dict): return _jsonable(payload["task"]) tasks = payload.get("tasks") if isinstance(tasks, list) and tasks: current_title = str(getattr(task, "title", "") or "").strip().lower() current_id = str(getattr(task, "id", "") or "").strip() for item in tasks: if not isinstance(item, dict): continue title = str(item.get("title") or "").strip().lower() task_key = str(item.get("task_key") or "").strip() if (current_title and title == current_title) or (current_id and task_key == current_id): return _jsonable(item) first = tasks[0] if isinstance(first, dict): return _jsonable(first) return { "task_key": str(getattr(task, "id", "") or "").strip() or str(getattr(task, "title", "") or "").strip(), "title": getattr(task, "title", None), "description": getattr(task, "description", None), "type": "task", "estimated_hours": _jsonable(getattr(task, "estimated_hours", None)), "required_skill_names": [], "required_skill_tag_ids": list(getattr(task, "required_skill_tag_ids", None) or []), "depends_on_task_keys": [], "risk_level": "medium", "risk_notes": None, "confidence": 0.0, "evidence_refs": [], "priority": getattr(task, "priority", "medium"), } def map_task_breakdown_to_record_payload( *, task: Any, project_title: str | None = None, source_context: dict[str, Any] | None = None, ) -> dict[str, Any]: task_object = _extract_task_breakdown_object(task) ai_snapshot = _jsonable(getattr(task, "ai_breakdown_payload", None)) reason_candidates = build_reason_candidates( source_type="task_breakdown", field_name="task_object", before_value=None, after_value=task_object, ) return { "tenant_id": int(getattr(task, "tenant_id", 1) or 1), "project_id": int(getattr(task, "project_id", 0) or 0) or None, "source_type": "task_breakdown", "source_id": int(getattr(task, "id", 0) or 0), "object_type": "task", "object_id": int(getattr(task, "id", 0) or 0), "field_name": "task_object", "source_title": str(project_title or getattr(task, "title", "") or "")[:500] or None, "object_title": str(getattr(task, "title", "") or "")[:500] or None, "before_value": None, "after_value": task_object, "ai_snapshot": ai_snapshot, "ai_confidence": float(task_object.get("confidence") or 0.0) if isinstance(task_object, dict) else None, "reason_candidates": reason_candidates, "reason_code": reason_candidates[0]["code"] if reason_candidates else None, "reason_text": None, "review_status": "pending", "reviewed_by": None, "reviewed_at": None, "auto_assigned": bool(getattr(task, "assignee_id", None)), } def map_task_override_to_record_payloads(*, task: Any) -> list[dict[str, Any]]: ai_object = _extract_task_breakdown_object(task) raw_ai_payload = getattr(task, "ai_breakdown_payload", None) if isinstance(raw_ai_payload, str): try: import json raw_ai_payload = json.loads(raw_ai_payload) except Exception: raw_ai_payload = None raw_ai_map = raw_ai_payload if isinstance(raw_ai_payload, dict) else {} overrides = getattr(task, "manual_overrides", None) or {} if isinstance(overrides, str): try: import json overrides = json.loads(overrides) except Exception: overrides = {} if not isinstance(overrides, dict): overrides = dict(overrides or {}) payloads: list[dict[str, Any]] = [] for field_name, after_value in overrides.items(): if field_name == "manual_overrides": continue before_value = raw_ai_map.get(field_name) if before_value is None: before_value = ai_object.get(field_name) if isinstance(ai_object, dict) else getattr(task, field_name, None) if _jsonable(before_value) == _jsonable(after_value): continue field_candidates = build_reason_candidates( source_type="task_override", field_name=str(field_name), before_value=before_value, after_value=after_value, ) payloads.append( { "tenant_id": int(getattr(task, "tenant_id", 1) or 1), "project_id": int(getattr(task, "project_id", 0) or 0) or None, "source_type": "task_override", "source_id": int(getattr(task, "id", 0) or 0), "object_type": "task", "object_id": int(getattr(task, "id", 0) or 0), "field_name": str(field_name), "source_title": str(getattr(task, "title", "") or "")[:500] or None, "object_title": str(getattr(task, "title", "") or "")[:500] or None, "before_value": _jsonable(before_value), "after_value": _jsonable(after_value), "ai_snapshot": { "ai_breakdown_payload": _jsonable(getattr(task, "ai_breakdown_payload", None)), "manual_overrides": _jsonable(overrides), }, "ai_confidence": float(ai_object.get("confidence") or 0.0) if isinstance(ai_object, dict) else None, "reason_candidates": field_candidates, "reason_code": field_candidates[0]["code"] if field_candidates else None, "reason_text": None, "review_status": "accepted", "reviewed_by": None, "reviewed_at": None, "auto_assigned": bool(getattr(task, "assignee_id", None)), } ) return payloads def map_qa_feedback_to_record_payload(*, qa_feedback: Any, qa_record: Any) -> dict[str, Any]: rating = str(getattr(qa_feedback, "rating", "") or "").strip().lower() helpfulness = getattr(qa_feedback, "helpfulness", None) error_types = list(getattr(qa_feedback, "error_types", None) or []) correct_answer = getattr(qa_feedback, "correct_answer", None) before_value = { "question": getattr(qa_record, "question", None), "answer": getattr(qa_record, "answer", None), "sources": _jsonable(getattr(qa_record, "sources", None) or []), "citations": _jsonable(getattr(qa_record, "citations", None) or []), "sources_used": _jsonable(getattr(qa_record, "sources_used", None) or []), } after_value = { "rating": rating, "helpfulness": helpfulness, "error_types": [str(item).strip() for item in error_types if str(item).strip()], "correct_answer": correct_answer, } reason_candidates = build_reason_candidates( source_type="qa_feedback", field_name="qa_feedback", before_value=before_value, after_value=after_value, ) return { "tenant_id": int(getattr(qa_feedback, "tenant_id", getattr(qa_record, "tenant_id", 1)) or 1), "project_id": int(getattr(qa_record, "project_id", 0) or 0) or None, "source_type": "qa_feedback", "source_id": int(getattr(qa_feedback, "id", 0) or 0), "object_type": "qa_record", "object_id": int(getattr(qa_feedback, "qa_id", 0) or 0), "field_name": "qa_feedback", "source_title": str(getattr(qa_record, "question", "") or "")[:500] or None, "object_title": str(getattr(qa_record, "question", "") or "")[:500] or None, "before_value": before_value, "after_value": after_value, "ai_snapshot": { "question": getattr(qa_record, "question", None), "answer": getattr(qa_record, "answer", None), "sources": _jsonable(getattr(qa_record, "sources", None) or []), "domains_used": _jsonable(getattr(qa_record, "domains_used", None) or []), "answer_mode": getattr(qa_record, "answer_mode", None), "grounding_status": getattr(qa_record, "grounding_status", None), "policy_version": getattr(qa_record, "policy_version", None), "citations": _jsonable(getattr(qa_record, "citations", None) or []), "sources_used": _jsonable(getattr(qa_record, "sources_used", None) or []), "evidence_pack": _jsonable(getattr(qa_record, "evidence_pack", None) or {}), "tool_trace": _jsonable(getattr(qa_record, "tool_trace", None) or []), "retrieval_metrics": _jsonable(getattr(qa_record, "retrieval_metrics", None) or {}), }, "ai_confidence": None, "reason_candidates": reason_candidates, "reason_code": reason_candidates[0]["code"] if reason_candidates else None, "reason_text": correct_answer or None, "review_status": "accepted", "reviewed_by": int(getattr(qa_feedback, "submitted_by", 0) or 0) or None, "reviewed_at": None, "auto_assigned": False, } def map_signal_feedback_to_record_payload(*, signal_feedback: Any, signal: Any) -> dict[str, Any]: predicted = bool(getattr(signal_feedback, "ai_predicted_strategic", False)) actual = bool(getattr(signal_feedback, "was_strategic", False)) before_value = { "ai_predicted_strategic": predicted, "ai_confidence": float(getattr(signal_feedback, "ai_confidence", 0.0) or 0.0), } after_value = { "was_strategic": actual, "reason": getattr(signal_feedback, "reason", None), } reason_candidates = build_reason_candidates( source_type="signal_feedback", field_name="strategic_classification", before_value=before_value, after_value=after_value, ) return { "tenant_id": int(getattr(signal_feedback, "tenant_id", getattr(signal, "tenant_id", 1)) or 1), "project_id": int(getattr(signal, "matched_project_id", 0) or 0) or None, "source_type": "signal_feedback", "source_id": int(getattr(signal_feedback, "id", 0) or 0), "object_type": "signal", "object_id": int(getattr(signal_feedback, "signal_id", 0) or 0), "field_name": "strategic_classification", "source_title": str(getattr(signal, "title", "") or getattr(signal, "source", "") or "")[:500] or None, "object_title": str(getattr(signal, "title", "") or getattr(signal, "source", "") or "")[:500] or None, "before_value": before_value, "after_value": after_value, "ai_snapshot": { "signal_id": getattr(signal_feedback, "signal_id", None), "title": getattr(signal, "title", None), "content": getattr(signal, "content", None), "impact_score": getattr(signal, "impact_score", None), "priority": getattr(signal, "priority", None), "strategic_analysis": _jsonable(getattr(signal, "strategic_analysis", None)), }, "ai_confidence": float(getattr(signal_feedback, "ai_confidence", 0.0) or 0.0), "reason_candidates": reason_candidates, "reason_code": reason_candidates[0]["code"] if reason_candidates else None, "reason_text": getattr(signal_feedback, "reason", None), "review_status": "accepted", "reviewed_by": _safe_int(getattr(signal_feedback, "user_id", None)), "reviewed_at": None, "auto_assigned": False, } __all__ = [ "map_qa_feedback_to_record_payload", "map_signal_feedback_to_record_payload", "map_task_breakdown_to_record_payload", "map_task_override_to_record_payloads", ]