from __future__ import annotations from .shared import * class LLMStructuredCandidatesMixin: def _iter_structured_candidates( self, *, message: Any, choice: Any, response: Any, include_reasoning: bool = True, ) -> list[tuple[str, Any]]: candidates: list[tuple[str, Any]] = [] if isinstance(message, dict): tool_calls = message.get("tool_calls") if isinstance(tool_calls, list) and tool_calls: first = tool_calls[0] if isinstance(tool_calls[0], dict) else {} fn = first.get("function") if isinstance(first, dict) else {} candidates.append(("tool_calls.function.arguments", fn.get("arguments") if isinstance(fn, dict) else None)) candidates.append(("message.content", message.get("content"))) if isinstance(choice, dict): candidates.append(("choice.text", choice.get("text"))) if isinstance(response, dict): candidates.append(("response.output_text", response.get("output_text"))) if include_reasoning: if isinstance(message, dict): candidates.append(("message.reasoning_content", message.get("reasoning_content"))) candidates.append(("message.reasoning_content", message.get("reasoning"))) if isinstance(choice, dict): candidates.append(("choice.reasoning_content", choice.get("reasoning_content"))) candidates.append(("choice.reasoning_content", choice.get("reasoning"))) if isinstance(response, dict): candidates.append(("response.reasoning_content", response.get("reasoning_content"))) candidates.append(("response.reasoning_content", response.get("reasoning"))) return candidates @classmethod def _unwrap_structured_payload_once(cls, payload: Any) -> Any: if not isinstance(payload, dict): return payload for key in ("json", "data", "result", "arguments"): if key not in payload: continue inner = payload.get(key) if isinstance(inner, str): candidate = cls._extract_json_candidate(cls._strip_code_fence(inner)) if not candidate: continue try: return json.loads(candidate) except Exception: return inner if isinstance(inner, (dict, list)): return inner return payload @staticmethod def _matches_expected_type(value: Any, expected: str) -> bool: if expected == "bool": return isinstance(value, bool) if expected == "str": return isinstance(value, str) if expected == "list": return isinstance(value, list) if expected == "number": return isinstance(value, (int, float)) and not isinstance(value, bool) if expected == "number_or_none": return value is None or (isinstance(value, (int, float)) and not isinstance(value, bool)) if expected == "str_or_none": return value is None or isinstance(value, str) if expected == "dict_or_none": return value is None or isinstance(value, dict) return True @staticmethod def _coerce_number(value: Any) -> Optional[float]: if isinstance(value, bool): return None if isinstance(value, (int, float)): return float(value) text = str(value or "").strip() if not text: return None try: return float(text) except Exception: return None @staticmethod def _coerce_int(value: Any) -> Optional[int]: number = LLMStructuredCandidatesMixin._coerce_number(value) if number is None: return None try: return int(number) except Exception: return None @staticmethod def _coerce_bool(value: Any) -> Optional[bool]: if isinstance(value, bool): return value if isinstance(value, (int, float)) and not isinstance(value, bool): return bool(int(value)) text = str(value or "").strip().lower() if not text: return None if text in BOOL_TRUE_VALUES: return True if text in {"0", "false", "no", "off", "n"}: return False return None @classmethod def _coerce_intent_structured_payload(cls, payload: dict[str, Any]) -> dict[str, Any]: normalized = dict(payload) confidence = cls._coerce_number(normalized.get("confidence")) if confidence is None: for alias in ("score", "probability", "confidence_score"): confidence = cls._coerce_number(normalized.get(alias)) if confidence is not None: break if confidence is not None: normalized["confidence"] = confidence reasoning_value = normalized.get("reasoning") if isinstance(reasoning_value, list): text_items = [str(item).strip() for item in reasoning_value if str(item).strip()] normalized["reasoning"] = ";".join(text_items) elif isinstance(reasoning_value, dict): normalized["reasoning"] = json.dumps(reasoning_value, ensure_ascii=False) else: reasoning_text = str(reasoning_value or "").strip() if reasoning_text: normalized["reasoning"] = reasoning_text else: for alias in ("reason", "rationale", "analysis", "explanation"): alias_text = str(normalized.get(alias) or "").strip() if alias_text: normalized["reasoning"] = alias_text break if "requires_confirmation" in normalized: bool_value = cls._coerce_bool(normalized.get("requires_confirmation")) if bool_value is not None: normalized["requires_confirmation"] = bool_value for field in ("project_id", "requested_count"): if field in normalized: number = cls._coerce_int(normalized.get(field)) normalized[field] = number if number is not None else None route_hint = str( normalized.get("workflow_route") or normalized.get("route") or "" ).strip().lower() if route_hint and "." in route_hint: family, action = route_hint.split(".", 1) if not str(normalized.get("family") or "").strip(): normalized["family"] = family if not str(normalized.get("action") or "").strip(): normalized["action"] = action return normalized @classmethod def _coerce_structured_payload(cls, *, function_name: str, payload: Any) -> Any: fn = str(function_name or "").strip().lower() if not isinstance(payload, dict): return payload if fn in {"classify_intent_ai_pmo_kernel", "classify_intent_complex"}: return cls._coerce_intent_structured_payload(payload) return payload def _validate_structured_payload(self, *, function_name: str, payload: Any) -> Optional[str]: fn = str(function_name or "").strip().lower() schema = STRUCTURED_OBJECT_SCHEMAS.get(fn) payload = self._coerce_structured_payload(function_name=fn, payload=payload) if schema is None: return None if isinstance(payload, (dict, list)) else "structured_json_invalid" if not isinstance(payload, dict): return "structured_schema_invalid" required_keys = schema.get("required_keys") or () for key in required_keys: if key not in payload: return "structured_schema_invalid" type_checks = schema.get("type_checks") or {} for key, expected in type_checks.items(): if not self._matches_expected_type(payload.get(key), str(expected)): return "structured_schema_invalid" optional_type_checks = schema.get("optional_type_checks") or {} for key, expected_values in optional_type_checks.items(): if key not in payload: continue if not any(self._matches_expected_type(payload.get(key), str(expected)) for expected in expected_values): return "structured_schema_invalid" return None