feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from http import HTTPStatus
|
||||
from time import monotonic, sleep
|
||||
from typing import Any
|
||||
@@ -27,6 +28,39 @@ DEFAULT_RUNTIME_CHAT_FAILURE_COOLDOWN_SECONDS = 90
|
||||
_slot_failure_until: dict[str, float] = {}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RuntimeChatCallTrace:
|
||||
slot: str
|
||||
provider: str
|
||||
model: str
|
||||
attempt: int
|
||||
status: str
|
||||
duration_ms: int = 0
|
||||
error_message: str | None = None
|
||||
skipped_reason: str | None = None
|
||||
|
||||
def model_dump(self) -> dict[str, Any]:
|
||||
return {
|
||||
"slot": self.slot,
|
||||
"provider": self.provider,
|
||||
"model": self.model,
|
||||
"attempt": self.attempt,
|
||||
"status": self.status,
|
||||
"duration_ms": self.duration_ms,
|
||||
"error_message": self.error_message,
|
||||
"skipped_reason": self.skipped_reason,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RuntimeChatResult:
|
||||
text: str | None
|
||||
calls: list[RuntimeChatCallTrace]
|
||||
|
||||
def calls_as_dicts(self) -> list[dict[str, Any]]:
|
||||
return [item.model_dump() for item in self.calls]
|
||||
|
||||
|
||||
class RuntimeChatService:
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
@@ -43,11 +77,47 @@ class RuntimeChatService:
|
||||
slot_timeouts: dict[str, int] | None = None,
|
||||
max_attempts: int | None = None,
|
||||
) -> str | None:
|
||||
configs = [
|
||||
config
|
||||
for slot in slot_priority
|
||||
if (config := self._load_chat_slot(slot)) is not None
|
||||
]
|
||||
return self.complete_with_trace(
|
||||
messages,
|
||||
slot_priority=slot_priority,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
timeout_seconds=timeout_seconds,
|
||||
slot_timeouts=slot_timeouts,
|
||||
max_attempts=max_attempts,
|
||||
).text
|
||||
|
||||
def complete_with_trace(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
*,
|
||||
slot_priority: tuple[str, ...] = ("main", "backup"),
|
||||
max_tokens: int = 500,
|
||||
temperature: float = 0.2,
|
||||
timeout_seconds: int | None = None,
|
||||
slot_timeouts: dict[str, int] | None = None,
|
||||
max_attempts: int | None = None,
|
||||
) -> RuntimeChatResult:
|
||||
configs: list[dict[str, str]] = []
|
||||
calls: list[RuntimeChatCallTrace] = []
|
||||
for slot in slot_priority:
|
||||
config = self._load_chat_slot(slot)
|
||||
if config is None:
|
||||
calls.append(
|
||||
RuntimeChatCallTrace(
|
||||
slot=slot,
|
||||
provider="",
|
||||
model="",
|
||||
attempt=0,
|
||||
status="skipped",
|
||||
skipped_reason="not_configured",
|
||||
)
|
||||
)
|
||||
continue
|
||||
configs.append(config)
|
||||
if not configs:
|
||||
return RuntimeChatResult(None, calls)
|
||||
|
||||
resolved_timeout_seconds = timeout_seconds or DEFAULT_RUNTIME_CHAT_TIMEOUT_SECONDS
|
||||
resolved_slot_timeouts = dict(slot_timeouts or {})
|
||||
resolved_max_attempts = max_attempts or DEFAULT_RUNTIME_CHAT_RETRY_ATTEMPTS
|
||||
@@ -61,7 +131,18 @@ class RuntimeChatService:
|
||||
config["slot"],
|
||||
config["provider"],
|
||||
)
|
||||
calls.append(
|
||||
RuntimeChatCallTrace(
|
||||
slot=config["slot"],
|
||||
provider=config["provider"],
|
||||
model=config["model"],
|
||||
attempt=attempt,
|
||||
status="skipped",
|
||||
skipped_reason="cooldown",
|
||||
)
|
||||
)
|
||||
continue
|
||||
started = monotonic()
|
||||
try:
|
||||
response_text = self._request_chat_completion(
|
||||
config,
|
||||
@@ -73,13 +154,47 @@ class RuntimeChatService:
|
||||
resolved_timeout_seconds,
|
||||
),
|
||||
)
|
||||
duration_ms = int((monotonic() - started) * 1000)
|
||||
if response_text:
|
||||
_slot_failure_until.pop(cache_key, None)
|
||||
return response_text.strip()
|
||||
calls.append(
|
||||
RuntimeChatCallTrace(
|
||||
slot=config["slot"],
|
||||
provider=config["provider"],
|
||||
model=config["model"],
|
||||
attempt=attempt,
|
||||
status="succeeded",
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
)
|
||||
return RuntimeChatResult(response_text.strip(), calls)
|
||||
calls.append(
|
||||
RuntimeChatCallTrace(
|
||||
slot=config["slot"],
|
||||
provider=config["provider"],
|
||||
model=config["model"],
|
||||
attempt=attempt,
|
||||
status="empty",
|
||||
duration_ms=duration_ms,
|
||||
error_message="模型返回空内容。",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
duration_ms = int((monotonic() - started) * 1000)
|
||||
_slot_failure_until[cache_key] = (
|
||||
monotonic() + DEFAULT_RUNTIME_CHAT_FAILURE_COOLDOWN_SECONDS
|
||||
)
|
||||
calls.append(
|
||||
RuntimeChatCallTrace(
|
||||
slot=config["slot"],
|
||||
provider=config["provider"],
|
||||
model=config["model"],
|
||||
attempt=attempt,
|
||||
status="failed",
|
||||
duration_ms=duration_ms,
|
||||
error_message=str(exc),
|
||||
)
|
||||
)
|
||||
logger.warning(
|
||||
"Runtime chat request failed slot=%s provider=%s attempt=%s/%s: %s",
|
||||
config["slot"],
|
||||
@@ -91,7 +206,7 @@ class RuntimeChatService:
|
||||
if attempt < resolved_max_attempts:
|
||||
sleep(DEFAULT_RUNTIME_CHAT_RETRY_DELAY_SECONDS)
|
||||
|
||||
return None
|
||||
return RuntimeChatResult(None, calls)
|
||||
|
||||
@staticmethod
|
||||
def _build_slot_cache_key(config: dict[str, str]) -> str:
|
||||
|
||||
Reference in New Issue
Block a user