"""Function-level runtime profiles for protected AI calls.""" from __future__ import annotations import json import os from dataclasses import dataclass from typing import Any from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession from ..config.ai_function_runtime_profiles import ( AI_FUNCTION_RUNTIME_PROFILE_INTEGRATION_TYPE, DEFAULT_AI_FUNCTION_RUNTIME_PROFILES, default_runtime_profile_bundle, ) from ..models import Integration from ..services.ai_routing_scene_contracts import AI_SCENE_REGISTRY from ..utils.timezone import now_cn_naive PLATFORM_RUNTIME_TENANT_ID = int(os.getenv("PMFLOW_PLATFORM_RUNTIME_TENANT_ID", "1") or "1") FUNCTION_RUNTIME_MAX_TOKENS_CEILING = 20_000 DEFAULT_SLOT_PROFILE_BUDGETS: dict[str, dict[str, Any]] = { "low": { "max_tokens": 2600, "max_tokens_min": 300, "max_tokens_max": FUNCTION_RUNTIME_MAX_TOKENS_CEILING, "recommended_min_tokens": 1200, "repair_max_tokens": 600, "temperature": 0.1, "foreground_wait_seconds": 6, "provider_timeout_seconds": 60, "job_timeout_seconds": 90, "max_retries": 0, "large_input_policy": "direct_generate", "response_mode": "fast", "response_format": "text", }, "high": { "max_tokens": 6000, "max_tokens_min": 800, "max_tokens_max": FUNCTION_RUNTIME_MAX_TOKENS_CEILING, "recommended_min_tokens": 2400, "repair_max_tokens": 1200, "temperature": 0.1, "foreground_wait_seconds": 8, "provider_timeout_seconds": 120, "job_timeout_seconds": 240, "max_retries": 1, "large_input_policy": "preprocess_then_generate", "response_mode": "complete", "response_format": "text", }, "structured": { "max_tokens": 8000, "max_tokens_min": 700, "max_tokens_max": FUNCTION_RUNTIME_MAX_TOKENS_CEILING, "recommended_min_tokens": 3000, "repair_max_tokens": 1800, "temperature": 0.0, "foreground_wait_seconds": 8, "provider_timeout_seconds": 90, "job_timeout_seconds": 180, "max_retries": 1, "large_input_policy": "preprocess_then_generate", "response_mode": "fast", "response_format": "json_object", }, "long_context": { "max_tokens": 8000, "max_tokens_min": 700, "max_tokens_max": FUNCTION_RUNTIME_MAX_TOKENS_CEILING, "recommended_min_tokens": 3000, "repair_max_tokens": 1800, "temperature": 0.1, "foreground_wait_seconds": 10, "provider_timeout_seconds": 180, "job_timeout_seconds": 300, "max_retries": 1, "large_input_policy": "preprocess_then_generate", "response_mode": "complete", "response_format": "text", }, "tool": { "max_tokens": 3000, "max_tokens_min": 300, "max_tokens_max": FUNCTION_RUNTIME_MAX_TOKENS_CEILING, "recommended_min_tokens": 1200, "repair_max_tokens": 600, "temperature": 0.0, "foreground_wait_seconds": 8, "provider_timeout_seconds": 90, "job_timeout_seconds": 180, "max_retries": 0, "large_input_policy": "direct_generate", "response_mode": "tool", "response_format": "json_object", }, "external_query": { "max_tokens": 1000, "max_tokens_min": 1, "max_tokens_max": FUNCTION_RUNTIME_MAX_TOKENS_CEILING, "recommended_min_tokens": 1, "repair_max_tokens": 180, "temperature": 0.0, "foreground_wait_seconds": 8, "provider_timeout_seconds": 120, "job_timeout_seconds": 180, "max_retries": 0, "large_input_policy": "direct_generate", "response_mode": "tool", "response_format": "external_evidence", }, } def _json_clone(value: Any) -> Any: return json.loads(json.dumps(value, ensure_ascii=False)) def _safe_dict(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {} def _release_version() -> str: return f"profile-v{now_cn_naive().strftime('%Y%m%d%H%M%S')}" def _default_profile_for_scene_function(*, scene_key: str, scene: dict[str, Any], function_name: str) -> dict[str, Any]: required_slots = [str(item or "").strip() for item in list(scene.get("required_slots") or []) if str(item or "").strip()] default_slot = str(scene.get("default_slot") or scene.get("expected_slot_key") or (required_slots[0] if required_slots else "low")).strip() if default_slot == "mixed": default_slot = required_slots[0] if required_slots else "low" slot_budget = dict(DEFAULT_SLOT_PROFILE_BUDGETS.get(default_slot) or DEFAULT_SLOT_PROFILE_BUDGETS["low"]) response_contract = "structured" if default_slot == "structured" else str(slot_budget.get("response_format") or "text") if str(scene.get("output_contract") or "").lower().find("json") >= 0: response_contract = "json" return { "function_name": function_name, "label": function_name, "scene_key": scene_key, "prompt_key": "", "required_slot": default_slot, "response_contract": response_contract, **slot_budget, "retry_policy": {"max_attempts": max(1, int(slot_budget.get("max_retries") or 0) + 1)}, "health_policy": {"window_hours": 24, "consecutive_failure_threshold": 3, "smoke_required_for_publish": False}, "cost_policy": {"budget_class": default_slot, "estimated_cost_ceiling_cny": 0.02, "show_cost_warning_above_tokens": int(slot_budget.get("recommended_min_tokens") or 1000) * 2}, } @dataclass(frozen=True) class AIFunctionRuntimeProfile: function_name: str scene_key: str prompt_key: str required_slot: str response_contract: str max_tokens: int repair_max_tokens: int temperature: float foreground_wait_seconds: int provider_timeout_seconds: int job_timeout_seconds: int max_retries: int large_input_policy: str response_mode: str response_format: str runtime_mode: str quality_gate_mode: str profile_version: str profile_source: str def routing_meta(self) -> dict[str, Any]: structured_retry_max_tokens = min( max( int(self.max_tokens) + max(int(self.repair_max_tokens), 600), int(int(self.max_tokens) * 1.8), ), FUNCTION_RUNTIME_MAX_TOKENS_CEILING, ) return { "function_runtime_profile_version": self.profile_version, "function_runtime_profile_source": self.profile_source, "response_contract": self.response_contract, "response_format": self.response_format, "function_runtime_max_tokens": self.max_tokens, "function_runtime_repair_max_tokens": self.repair_max_tokens, "function_runtime_structured_retry_max_tokens": structured_retry_max_tokens, "foreground_wait_seconds": self.foreground_wait_seconds, "provider_timeout_seconds": self.provider_timeout_seconds, "job_timeout_seconds": self.job_timeout_seconds, "max_retries": self.max_retries, "large_input_policy": self.large_input_policy, "runtime_mode": self.runtime_mode, "quality_gate_mode": self.quality_gate_mode, "timeout_policy_source": "function_runtime_profile", "timeout_policy_reason": f"{self.function_name} function runtime profile controls provider/job timeout", } class AIFunctionRuntimeProfileService: """Load, validate, draft and publish function runtime profiles.""" CONTROLLED_SMOKE_FUNCTIONS = frozenset( { "pmflow_launch_draft_v1", "pmflow_change_review_extraction_v1", } ) def __init__(self, db: AsyncSession): self.db = db @classmethod def smoke_supported(cls, function_name: str) -> bool: return str(function_name or "").strip() in cls.CONTROLLED_SMOKE_FUNCTIONS async def resolve_profile( self, *, function_name: str, scene_key: str | None = None, prompt_key: str | None = None, stage: str = "active", ) -> AIFunctionRuntimeProfile: release = await self.get_profiles() normalized_stage = "draft" if str(stage or "").strip().lower() == "draft" else "active" profiles = release.get(f"{normalized_stage}_profiles") if isinstance(release.get(f"{normalized_stage}_profiles"), dict) else {} profile = _safe_dict(profiles.get(function_name)) or _safe_dict(DEFAULT_AI_FUNCTION_RUNTIME_PROFILES.get(function_name)) if not profile: raise ValueError(f"function runtime profile not found: {function_name}") normalized = self._normalize_profile(function_name=function_name, raw=profile) if scene_key and normalized.scene_key != str(scene_key).strip(): raise ValueError(f"function runtime profile scene mismatch: {function_name}") if prompt_key and normalized.prompt_key != str(prompt_key).strip(): raise ValueError(f"function runtime profile prompt mismatch: {function_name}") return AIFunctionRuntimeProfile( function_name=normalized.function_name, scene_key=normalized.scene_key, prompt_key=normalized.prompt_key, required_slot=normalized.required_slot, response_contract=normalized.response_contract, max_tokens=normalized.max_tokens, repair_max_tokens=normalized.repair_max_tokens, temperature=normalized.temperature, foreground_wait_seconds=normalized.foreground_wait_seconds, provider_timeout_seconds=normalized.provider_timeout_seconds, job_timeout_seconds=normalized.job_timeout_seconds, max_retries=normalized.max_retries, large_input_policy=normalized.large_input_policy, response_mode=normalized.response_mode, response_format=normalized.response_format, runtime_mode=normalized.runtime_mode, quality_gate_mode=normalized.quality_gate_mode, profile_version=str(release.get(f"{normalized_stage}_version") or release.get("active_version") or "profile-default-v1"), profile_source=normalized_stage, ) async def get_profiles(self) -> dict[str, Any]: bundle = self._normalize_bundle(await self._load_config()) active_profiles = _safe_dict(_safe_dict(bundle.get("active")).get("profiles")) draft_profiles = _safe_dict(_safe_dict(bundle.get("draft")).get("profiles")) validation = self.validate_bundle(bundle=bundle, stage="draft") return { "version": str(bundle.get("active_version") or "profile-default-v1"), "active_version": str(bundle.get("active_version") or "profile-default-v1"), "draft_version": str(bundle.get("draft_version") or "draft"), "published_at": _safe_dict(bundle.get("active")).get("published_at"), "published_by": _safe_dict(bundle.get("active")).get("published_by"), "updated_at": _safe_dict(bundle.get("draft")).get("updated_at"), "updated_by": _safe_dict(bundle.get("draft")).get("updated_by"), "dirty": self._bundle_dirty(bundle), "validation": validation, "history": bundle.get("history") if isinstance(bundle.get("history"), list) else [], "draft_last_smoke": _safe_dict(_safe_dict(bundle.get("draft")).get("last_smoke")), "active_profiles": active_profiles, "draft_profiles": draft_profiles, "profiles": draft_profiles, } async def update_profile(self, *, function_name: str, payload: dict[str, Any], updated_by: int) -> dict[str, Any]: integration, bundle = await self._get_or_init_bundle() draft = _safe_dict(bundle.get("draft")) profiles = _safe_dict(draft.get("profiles")) current = _safe_dict(profiles.get(function_name)) or _safe_dict(DEFAULT_AI_FUNCTION_RUNTIME_PROFILES.get(function_name)) if not current: raise ValueError("函数运行合同不存在") next_profile = _json_clone(current) for key in ("max_tokens", "repair_max_tokens"): if key in payload: next_profile[key] = int(payload.get(key) or 0) for key in ("foreground_wait_seconds", "provider_timeout_seconds", "job_timeout_seconds", "max_retries"): if key in payload: next_profile[key] = int(payload.get(key) or 0) for key in ("temperature",): if key in payload: next_profile[key] = float(payload.get(key) or 0.0) for key in ("response_mode", "large_input_policy", "runtime_mode", "quality_gate_mode"): if key in payload: next_profile[key] = str(payload.get(key) or "").strip() or current.get(key) if "required_slot" in payload: next_profile["required_slot"] = str(payload.get("required_slot") or "").strip() or current.get("required_slot") self._normalize_profile(function_name=function_name, raw=next_profile) profiles[function_name] = next_profile draft["profiles"] = profiles draft["updated_at"] = now_cn_naive().isoformat() draft["updated_by"] = int(updated_by) last_smoke = _safe_dict(draft.get("last_smoke")) last_smoke.pop(function_name, None) draft["last_smoke"] = last_smoke bundle["draft"] = draft bundle["draft_version"] = "draft" integration.config = _json_clone(bundle) integration.is_active = True await self.db.flush() return await self.get_profiles() async def record_smoke(self, *, function_name: str, status: str, summary: str, checked_by: int | None = None) -> dict[str, Any]: integration, bundle = await self._get_or_init_bundle() draft = _json_clone(_safe_dict(bundle.get("draft"))) last_smoke = _json_clone(_safe_dict(draft.get("last_smoke"))) last_smoke[function_name] = { "status": str(status or "unknown"), "summary": str(summary or "")[:500], "checked_at": now_cn_naive().isoformat(), "checked_by": int(checked_by) if checked_by is not None else None, } draft["last_smoke"] = last_smoke bundle["draft"] = draft integration.config = _json_clone(bundle) integration.is_active = True await self.db.flush() return await self.get_profiles() async def publish_profiles(self, *, published_by: int, summary: str | None = None) -> dict[str, Any]: integration, bundle = await self._get_or_init_bundle() validation = self.validate_bundle(bundle=bundle, stage="draft") blocking = [item for item in validation.get("items", []) if item.get("severity") == "critical"] if blocking: raise ValueError(f"函数运行合同草稿存在阻塞项,不能发布:{self._format_blocking_items(blocking)}") version = _release_version() now = now_cn_naive().isoformat() draft_profiles = _safe_dict(_safe_dict(bundle.get("draft")).get("profiles")) bundle["active"] = { "profiles": _json_clone(draft_profiles), "published_at": now, "published_by": int(published_by), } bundle["active_version"] = version bundle["version"] = version history = bundle.get("history") if isinstance(bundle.get("history"), list) else [] history.insert( 0, { "version": version, "published_at": now, "published_by": int(published_by), "summary": str(summary or "发布函数运行合同草稿").strip()[:200], "profiles": _json_clone(draft_profiles), }, ) bundle["history"] = history[:10] integration.config = _json_clone(bundle) integration.is_active = True await self.db.flush() return await self.get_profiles() def validate_bundle(self, *, bundle: dict[str, Any], stage: str = "draft") -> dict[str, Any]: profiles = _safe_dict(_safe_dict(bundle.get(stage)).get("profiles")) last_smoke = _safe_dict(_safe_dict(bundle.get("draft")).get("last_smoke")) items: list[dict[str, Any]] = [] for function_name, raw_profile in profiles.items(): try: profile = self._normalize_profile(function_name=str(function_name), raw=_safe_dict(raw_profile)) except ValueError as exc: items.append(self._validation_item(str(function_name), "critical", "profile_invalid", str(exc))) continue scene = _safe_dict(AI_SCENE_REGISTRY.get(profile.scene_key)) if profile.function_name not in set(scene.get("function_names") or []): items.append(self._validation_item(function_name, "critical", "scene_function_mismatch", "函数未登记在对应业务场景合同中。")) if profile.required_slot not in set(scene.get("required_slots") or []): items.append(self._validation_item(function_name, "critical", "slot_mismatch", "函数 required_slot 与业务场景必需槽不匹配。")) default_profile = _safe_dict(self._default_profiles().get(function_name)) recommended_min = int(default_profile.get("recommended_min_tokens") or default_profile.get("max_tokens_min") or 0) if recommended_min and profile.max_tokens < recommended_min: items.append(self._validation_item(function_name, "warning", "token_budget_below_recommendation", f"当前 max_tokens={profile.max_tokens},低于建议值 {recommended_min},长结构化草案可能被截断。")) smoke_required = bool(_safe_dict(profile.__dict__.get("health_policy")).get("smoke_required_for_publish")) if isinstance(profile.__dict__.get("health_policy"), dict) else bool(_safe_dict(default_profile.get("health_policy")).get("smoke_required_for_publish")) smoke = _safe_dict(last_smoke.get(function_name)) if stage == "draft" and smoke_required and str(smoke.get("status") or "").lower() != "success": items.append(self._validation_item(function_name, "critical", "smoke_required", "发布前必须运行函数运行合同 smoke,并且结果为 success。")) critical_count = sum(1 for item in items if item.get("severity") == "critical") warning_count = sum(1 for item in items if item.get("severity") == "warning") severity = "critical" if critical_count else ("warning" if warning_count else "ok") return { "stage": stage, "status": "blocked" if severity == "critical" else ("warning" if severity == "warning" else "passed"), "severity": severity, "can_publish": critical_count == 0, "summary": { "critical": critical_count, "warning": warning_count, "ok": max(0, len(profiles) - critical_count - warning_count), }, "items": items, } async def _load_config(self) -> dict[str, Any]: row = ( await self.db.execute( select(Integration).where( and_( Integration.tenant_id == PLATFORM_RUNTIME_TENANT_ID, Integration.type == AI_FUNCTION_RUNTIME_PROFILE_INTEGRATION_TYPE, ) ) ) ).scalar_one_or_none() return _safe_dict(row.config if row is not None else {}) async def _get_or_init_bundle(self) -> tuple[Integration, dict[str, Any]]: row = ( await self.db.execute( select(Integration).where( and_( Integration.tenant_id == PLATFORM_RUNTIME_TENANT_ID, Integration.type == AI_FUNCTION_RUNTIME_PROFILE_INTEGRATION_TYPE, ) ) ) ).scalar_one_or_none() if row is None: row = Integration( tenant_id=PLATFORM_RUNTIME_TENANT_ID, type=AI_FUNCTION_RUNTIME_PROFILE_INTEGRATION_TYPE, config=default_runtime_profile_bundle(), is_active=True, ) self.db.add(row) await self.db.flush() bundle = self._normalize_bundle(_safe_dict(row.config)) return row, bundle @classmethod def _default_profiles(cls) -> dict[str, dict[str, Any]]: profiles = _json_clone(DEFAULT_AI_FUNCTION_RUNTIME_PROFILES) for scene_key, scene in AI_SCENE_REGISTRY.items(): if not isinstance(scene, dict): continue for function_name in list(scene.get("function_names") or []): name = str(function_name or "").strip() if not name or name in profiles: continue profiles[name] = _default_profile_for_scene_function(scene_key=str(scene_key), scene=scene, function_name=name) return profiles @classmethod def _normalize_bundle(cls, raw: dict[str, Any]) -> dict[str, Any]: default = default_runtime_profile_bundle() bundle = _safe_dict(raw) active = _safe_dict(bundle.get("active")) draft = _safe_dict(bundle.get("draft")) default_profiles = cls._default_profiles() active_profiles = _safe_dict(active.get("profiles")) or _safe_dict(bundle.get("profiles")) or default_profiles draft_profiles = _safe_dict(draft.get("profiles")) or active_profiles allowed_functions = set(default_profiles) active_profiles = { str(function_name): _safe_dict(profile) for function_name, profile in active_profiles.items() if str(function_name) in allowed_functions } draft_profiles = { str(function_name): _safe_dict(profile) for function_name, profile in draft_profiles.items() if str(function_name) in allowed_functions } for function_name, profile in default_profiles.items(): default_profile = _json_clone(profile) active_profiles[function_name] = { **default_profile, **_safe_dict(active_profiles.get(function_name)), } active_profiles[function_name]["max_tokens_max"] = max( int(default_profile.get("max_tokens_max") or FUNCTION_RUNTIME_MAX_TOKENS_CEILING), int(active_profiles[function_name].get("max_tokens_max") or 0), FUNCTION_RUNTIME_MAX_TOKENS_CEILING, ) draft_profiles[function_name] = { **default_profile, **_safe_dict(active_profiles.get(function_name, profile)), **_safe_dict(draft_profiles.get(function_name)), } draft_profiles[function_name]["max_tokens_max"] = max( int(default_profile.get("max_tokens_max") or FUNCTION_RUNTIME_MAX_TOKENS_CEILING), int(draft_profiles[function_name].get("max_tokens_max") or 0), FUNCTION_RUNTIME_MAX_TOKENS_CEILING, ) return { "version": str(bundle.get("active_version") or bundle.get("version") or default["version"]), "active_version": str(bundle.get("active_version") or bundle.get("version") or default["active_version"]), "draft_version": str(bundle.get("draft_version") or "draft"), "active": {**active, "profiles": _json_clone(active_profiles)}, "draft": {**draft, "profiles": _json_clone(draft_profiles), "last_smoke": _safe_dict(draft.get("last_smoke"))}, "history": bundle.get("history") if isinstance(bundle.get("history"), list) else [], } @classmethod def _normalize_profile(cls, *, function_name: str, raw: dict[str, Any]) -> AIFunctionRuntimeProfile: profile = {**_safe_dict(cls._default_profiles().get(function_name)), **_safe_dict(raw)} min_tokens = int(profile.get("max_tokens_min") or 256) max_tokens_limit = int(profile.get("max_tokens_max") or FUNCTION_RUNTIME_MAX_TOKENS_CEILING) max_tokens = int(profile.get("max_tokens") or 0) repair_max_tokens = int(profile.get("repair_max_tokens") or 0) if max_tokens < min_tokens or max_tokens > max_tokens_limit: raise ValueError(f"max_tokens 必须在 {min_tokens}-{max_tokens_limit} 之间") if repair_max_tokens < 180 or repair_max_tokens > max_tokens: raise ValueError("repair_max_tokens 必须不小于 180 且不大于 max_tokens") temperature = float(profile.get("temperature") or 0.0) if temperature < 0 or temperature > 1: raise ValueError("temperature 必须在 0-1 之间") foreground_wait_seconds = int(profile.get("foreground_wait_seconds") or 0) provider_timeout_seconds = int(profile.get("provider_timeout_seconds") or 0) job_timeout_seconds = int(profile.get("job_timeout_seconds") or 0) max_retries = int(profile.get("max_retries") or 0) large_input_policy = str(profile.get("large_input_policy") or "preprocess_then_generate").strip() if foreground_wait_seconds < 3 or foreground_wait_seconds > 300: raise ValueError("foreground_wait_seconds 必须在 3-300 秒之间") if provider_timeout_seconds < 10 or provider_timeout_seconds > 300: raise ValueError("provider_timeout_seconds 必须在 10-300 秒之间") if job_timeout_seconds < provider_timeout_seconds or job_timeout_seconds > 900: raise ValueError("job_timeout_seconds 必须不小于 provider_timeout_seconds 且不大于 900 秒") if max_retries < 0 or max_retries > 3: raise ValueError("max_retries 必须在 0-3 之间") if large_input_policy not in {"direct_generate", "preprocess_then_generate"}: raise ValueError("large_input_policy 仅支持 direct_generate / preprocess_then_generate") runtime_mode = str(profile.get("runtime_mode") or "active").strip() quality_gate_mode = str(profile.get("quality_gate_mode") or "strict").strip() if runtime_mode not in {"active", "shadow", "bypass", "disabled"}: raise ValueError("runtime_mode 仅支持 active / shadow / bypass / disabled") if quality_gate_mode not in {"strict", "relaxed", "shadow", "off"}: raise ValueError("quality_gate_mode 仅支持 strict / relaxed / shadow / off") scene_key = str(profile.get("scene_key") or "").strip() scene = _safe_dict(AI_SCENE_REGISTRY.get(scene_key)) required_slot = str(profile.get("required_slot") or "").strip() allowed_slots = {str(item or "").strip() for item in list(scene.get("allowed_slots") or scene.get("required_slots") or []) if str(item or "").strip()} if allowed_slots and required_slot not in allowed_slots: raise ValueError("required_slot 不满足业务场景允许的能力槽") return AIFunctionRuntimeProfile( function_name=function_name, scene_key=scene_key, prompt_key=str(profile.get("prompt_key") or "").strip(), required_slot=required_slot, response_contract=str(profile.get("response_contract") or "structured").strip(), max_tokens=max_tokens, repair_max_tokens=repair_max_tokens, temperature=temperature, foreground_wait_seconds=foreground_wait_seconds, provider_timeout_seconds=provider_timeout_seconds, job_timeout_seconds=job_timeout_seconds, max_retries=max_retries, large_input_policy=large_input_policy, response_mode=str(profile.get("response_mode") or "fast").strip(), response_format=str(profile.get("response_format") or "json_object").strip(), runtime_mode=runtime_mode, quality_gate_mode=quality_gate_mode, profile_version="", profile_source="", ) @staticmethod def _bundle_dirty(bundle: dict[str, Any]) -> bool: return _safe_dict(_safe_dict(bundle.get("active")).get("profiles")) != _safe_dict(_safe_dict(bundle.get("draft")).get("profiles")) @staticmethod def _validation_item(function_name: str, severity: str, code: str, message: str) -> dict[str, Any]: return { "function_name": function_name, "severity": severity, "code": code, "title": function_name, "message": message, } @staticmethod def _format_blocking_items(items: list[dict[str, Any]]) -> str: if not items: return "无明细" chunks: list[str] = [] for item in items[:5]: function_name = str(item.get("function_name") or item.get("title") or "unknown_function") code = str(item.get("code") or "unknown") message = str(item.get("message") or "存在阻塞项") chunks.append(f"{function_name} [{code}] {message}") if len(items) > 5: chunks.append(f"另有 {len(items) - 5} 项") return ";".join(chunks) __all__ = ["AIFunctionRuntimeProfile", "AIFunctionRuntimeProfileService"]