168 lines
6.1 KiB
Python
168 lines
6.1 KiB
Python
"""Scene-level AI runtime policy.
|
|
|
|
This module keeps product SLA decisions out of the low-level transport layer.
|
|
The transport only consumes explicit routing metadata such as
|
|
``provider_timeout_seconds``; scene services decide those values from business
|
|
context and input scale.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
|
|
POLICY_VERSION = "scene-timeout-v1"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SceneRuntimePolicy:
|
|
scene_key: str
|
|
function_name: str
|
|
input_bucket: str
|
|
foreground_wait_seconds: int
|
|
provider_timeout_seconds: int
|
|
job_timeout_seconds: int
|
|
max_retries: int
|
|
fallback_policy: str
|
|
large_input_policy: str
|
|
source: str = "default_scene_policy"
|
|
version: str = POLICY_VERSION
|
|
|
|
def routing_meta(self, *, input_tokens_estimate: int, reason: str = "") -> dict[str, Any]:
|
|
return {
|
|
"scene_key": self.scene_key,
|
|
"scene_timeout_policy_id": f"{self.scene_key}:{self.function_name}:{self.input_bucket}",
|
|
"scene_timeout_policy_version": self.version,
|
|
"input_bucket": self.input_bucket,
|
|
"input_tokens_estimate": int(input_tokens_estimate),
|
|
"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,
|
|
"scene_fallback_policy": self.fallback_policy,
|
|
"large_input_policy": self.large_input_policy,
|
|
"timeout_policy_source": self.source,
|
|
"timeout_policy_reason": reason or self._default_reason(input_tokens_estimate),
|
|
}
|
|
|
|
def _default_reason(self, input_tokens_estimate: int) -> str:
|
|
return (
|
|
f"{self.scene_key}/{self.function_name} uses {self.input_bucket} policy "
|
|
f"for about {int(input_tokens_estimate)} input tokens"
|
|
)
|
|
|
|
|
|
class AISceneRuntimePolicyService:
|
|
"""Resolve timeout and job policies for AI business scenes."""
|
|
|
|
PROJECT_LAUNCH_FUNCTION = "pmflow_launch_draft_v1"
|
|
|
|
@classmethod
|
|
def estimate_tokens(cls, text: str) -> int:
|
|
value = str(text or "")
|
|
if not value:
|
|
return 0
|
|
# Conservative estimate used only for policy selection and audit.
|
|
return max(1, len(value) // 4)
|
|
|
|
@classmethod
|
|
def input_bucket_for_tokens(cls, tokens: int) -> str:
|
|
count = max(int(tokens or 0), 0)
|
|
if count < 1500:
|
|
return "small"
|
|
if count <= 8000:
|
|
return "normal"
|
|
return "large"
|
|
|
|
@classmethod
|
|
def resolve(
|
|
cls,
|
|
*,
|
|
scene_key: str,
|
|
function_name: str,
|
|
input_text: str,
|
|
attachment_count: int = 0,
|
|
) -> tuple[SceneRuntimePolicy, int]:
|
|
normalized_scene = str(scene_key or "").strip().lower()
|
|
normalized_function = str(function_name or "").strip().lower()
|
|
input_tokens = cls.estimate_tokens(input_text)
|
|
bucket = cls.input_bucket_for_tokens(input_tokens)
|
|
if int(attachment_count or 0) > 0 and bucket == "small":
|
|
bucket = "normal"
|
|
|
|
if normalized_scene == "project_launch" and normalized_function == cls.PROJECT_LAUNCH_FUNCTION:
|
|
return cls._project_launch_policy(function_name=normalized_function, bucket=bucket), input_tokens
|
|
|
|
return (
|
|
SceneRuntimePolicy(
|
|
scene_key=normalized_scene or "unknown",
|
|
function_name=normalized_function or "unknown",
|
|
input_bucket=bucket,
|
|
foreground_wait_seconds=10,
|
|
provider_timeout_seconds=60,
|
|
job_timeout_seconds=180,
|
|
max_retries=1,
|
|
fallback_policy="same_slot_fallback",
|
|
large_input_policy="preprocess_then_generate",
|
|
),
|
|
input_tokens,
|
|
)
|
|
|
|
@staticmethod
|
|
def _project_launch_policy(*, function_name: str, bucket: str) -> SceneRuntimePolicy:
|
|
specs = {
|
|
"small": {
|
|
"foreground": 8,
|
|
"provider": 60,
|
|
"job": 180,
|
|
"large_input_policy": "direct_generate",
|
|
},
|
|
"normal": {
|
|
"foreground": 10,
|
|
"provider": 90,
|
|
"job": 180,
|
|
"large_input_policy": "direct_generate",
|
|
},
|
|
"large": {
|
|
"foreground": 12,
|
|
"provider": 120,
|
|
"job": 300,
|
|
"large_input_policy": "preprocess_then_generate",
|
|
},
|
|
}
|
|
spec = specs.get(bucket, specs["normal"])
|
|
return SceneRuntimePolicy(
|
|
scene_key="project_launch",
|
|
function_name=function_name,
|
|
input_bucket=bucket,
|
|
foreground_wait_seconds=spec["foreground"],
|
|
provider_timeout_seconds=spec["provider"],
|
|
job_timeout_seconds=spec["job"],
|
|
max_retries=1,
|
|
fallback_policy="same_slot_fallback",
|
|
large_input_policy=spec["large_input_policy"],
|
|
)
|
|
|
|
@staticmethod
|
|
def timeout_recommendation(*, status: str, routing_meta: dict[str, Any]) -> dict[str, Any] | None:
|
|
text = str(status or "").strip().lower()
|
|
if text != "structured_timeout":
|
|
return None
|
|
current_timeout = routing_meta.get("provider_timeout_seconds")
|
|
bucket = str(routing_meta.get("input_bucket") or "unknown")
|
|
scene_key = str(routing_meta.get("scene_key") or "unknown")
|
|
return {
|
|
"code": "model_read_timeout",
|
|
"label": "模型读超时",
|
|
"summary": "上游模型在场景策略规定时间内没有返回可解析结构化结果。",
|
|
"recommended_actions": [
|
|
"检查同场景 p95/p99 延迟后调整 provider timeout",
|
|
"为 structured 槽配置同能力 fallback runtime",
|
|
"输入进入 large 桶时先做材料预处理或分段结构化",
|
|
],
|
|
"current_provider_timeout_seconds": current_timeout,
|
|
"input_bucket": bucket,
|
|
"scene_key": scene_key,
|
|
}
|