from __future__ import annotations from .shared import * class AIStewardJsonParsingMixin: @staticmethod def _looks_like_project_creation_stub_response(text: str) -> bool: normalized = re.sub(r"\s+", "", str(text or "")).lower() if not normalized: return True return any(marker in normalized for marker in PROJECT_CREATION_STUB_MARKERS) @staticmethod def _project_creation_high_tier_function(function_name: str) -> str: return str(PROJECT_CREATION_LOW_TO_HIGH_FUNCTION_MAP.get(str(function_name or "").strip(), "") or "").strip() @staticmethod def _strip_code_fence(text: str) -> str: raw = str(text or "").strip() if not raw.startswith("```"): return raw chunks = raw.split("```") if len(chunks) < 2: return raw candidate = chunks[1].strip() if candidate.lower().startswith("json"): candidate = candidate[4:].strip() return candidate @staticmethod def _extract_balanced_json_segment(text: str, open_char: str, close_char: str) -> str: raw = str(text or "") start = raw.find(open_char) if start < 0: return "" depth = 0 in_str = False escaped = False for idx in range(start, len(raw)): ch = raw[idx] if in_str: if escaped: escaped = False elif ch == "\\": escaped = True elif ch == '"': in_str = False continue if ch == '"': in_str = True continue if ch == open_char: depth += 1 elif ch == close_char: depth -= 1 if depth == 0: return raw[start : idx + 1].strip() return "" def _extract_json_candidate(self, text: str) -> str: raw = self._strip_code_fence(text) if raw.startswith("{") or raw.startswith("["): return raw object_candidate = self._extract_balanced_json_segment(raw, "{", "}") array_candidate = self._extract_balanced_json_segment(raw, "[", "]") if object_candidate and array_candidate: return object_candidate if raw.find("{") <= raw.find("[") else array_candidate return object_candidate or array_candidate or raw @staticmethod def _repair_json_text(raw: str) -> str: text = str(raw or "") if not text: return text buf: list[str] = [] in_str = False escaped = False for ch in text: if in_str: if escaped: escaped = False elif ch == "\\": escaped = True elif ch == '"': in_str = False elif ch == "\n": buf.append("\\n") continue elif ch == "\r": buf.append("\\r") continue elif ch == "\t": buf.append("\\t") continue else: if ch == '"': in_str = True buf.append(ch) repaired = "".join(buf) repaired = re.sub(r",\s*([}\]])", r"\1", repaired) return repaired def _parse_json_object_response(self, response_text: str, *, stage_label: str) -> Dict[str, Any]: normalized = self._extract_json_candidate(response_text) try: payload = json.loads(normalized) except Exception as exc: repaired = self._repair_json_text(normalized) try: payload = json.loads(repaired) except Exception: try: literal_payload = ast.literal_eval(normalized) except Exception: raise RuntimeError(f"{stage_label}:{exc}") from exc payload = literal_payload if not isinstance(payload, dict): raise RuntimeError(f"{stage_label}:返回内容不是 JSON 对象") return payload async def _generate_project_creation_object( self, *, prompt: str, tenant_id: int, function_name: str, stage_label: str, timeout_seconds: float | None = None, ) -> Dict[str, Any]: max_rounds = PROJECT_CREATION_JSON_PARSE_RETRY_COUNT + 1 last_error: Exception | None = None for round_index in range(max_rounds): response = await self._generate_project_creation_text( prompt=prompt, tenant_id=tenant_id, function_name=function_name, timeout_seconds=float(timeout_seconds) if timeout_seconds is not None else PROJECT_CREATION_DRAFT_TIMEOUT_SECONDS, ) try: return self._parse_json_object_response(response, stage_label=stage_label) except Exception as exc: last_error = exc if isinstance(exc, Exception) else RuntimeError(str(exc)) logger.warning( "ai_steward_project_creation_json_retry", tenant_id=int(tenant_id), function_name=function_name, stage_label=stage_label, round=round_index + 1, error_type=type(last_error).__name__, error=str(last_error), ) raise RuntimeError(str(last_error or f"{stage_label}:unknown_error")) from last_error async def _generate_project_creation_text( self, *, prompt: str, tenant_id: int, function_name: str, timeout_seconds: float = PROJECT_CREATION_DRAFT_TIMEOUT_SECONDS, ) -> str: attempts_per_function = PROJECT_CREATION_STEP_RETRY_COUNT + 1 last_error: Exception | None = None for attempt_index in range(attempts_per_function): started_at = time.perf_counter() try: response = await asyncio.wait_for( self.llm.generate( prompt, tenant_id=tenant_id, function_name=function_name, response_mode="fast", use_prompt_profile=False, temperature=0.0, max_tokens=2200, ), timeout=timeout_seconds, ) cleaned_response = str(response or "").strip() elapsed_seconds = round(time.perf_counter() - started_at, 2) if self._looks_like_project_creation_stub_response(cleaned_response): raise RuntimeError("project_creation_stub_response") logger.info( "ai_steward_project_creation_step_ok", tenant_id=int(tenant_id), function_name=function_name, attempt=attempt_index + 1, elapsed_seconds=elapsed_seconds, ) return cleaned_response except Exception as exc: last_error = exc if isinstance(exc, Exception) else RuntimeError(str(exc)) high_tier_function = self._project_creation_high_tier_function(function_name) if high_tier_function: high_started_at = time.perf_counter() try: fallback_response = await asyncio.wait_for( self.llm.generate( prompt, tenant_id=tenant_id, function_name=high_tier_function, response_mode="fast", use_prompt_profile=False, temperature=0.0, max_tokens=2200, ), timeout=timeout_seconds, ) cleaned_fallback = str(fallback_response or "").strip() high_elapsed_seconds = round(time.perf_counter() - high_started_at, 2) if self._looks_like_project_creation_stub_response(cleaned_fallback): raise RuntimeError("project_creation_stub_response") logger.info( "ai_steward_project_creation_step_escalated_ok", tenant_id=int(tenant_id), function_name=function_name, high_tier_function=high_tier_function, attempt=attempt_index + 1, elapsed_seconds=high_elapsed_seconds, ) return cleaned_fallback except Exception as high_exc: last_error = high_exc if isinstance(high_exc, Exception) else RuntimeError(str(high_exc)) logger.warning( "ai_steward_project_creation_step_escalated_retry", tenant_id=int(tenant_id), function_name=function_name, high_tier_function=high_tier_function, attempt=attempt_index + 1, elapsed_seconds=round(time.perf_counter() - high_started_at, 2), error_type=type(high_exc).__name__, error=str(high_exc), ) logger.warning( "ai_steward_project_creation_step_retry", tenant_id=int(tenant_id), function_name=function_name, attempt=attempt_index + 1, elapsed_seconds=round(time.perf_counter() - started_at, 2), error_type=type(exc).__name__, error=str(exc), ) raise RuntimeError( f"AI 管家项目创建步骤失败:function={function_name}, reason={last_error or 'unknown_error'}" ) from last_error