시리즈: Humanoid
python
185 줄
· 업데이트 2026-05-12
diagnostics.py
Humanoid/May_12/RoboOS/master/task_manager/diagnostics.py
# -*- coding: utf-8 -*-
"""TaskDiagnostics:故障識別、分類、寫上下文、廣播。
對應架構教學文件 §5.4 / §10.5、interfaces.md §7 故障碼總表。
"""
from __future__ import annotations
import json
import logging
import threading
import time
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional
from .context import ContextStore, TaskContext
from .events import FaultRecord, SkillEvent
logger = logging.getLogger(__name__)
# 任務級故障碼表(interfaces.md §7.1)
FAULT_TABLE: Dict[str, Dict[str, str]] = {
"TM-001": {"name": "PLAN_FAIL", "severity": "error", "default_action": "abort"},
"TM-002": {"name": "SCENE_EMPTY", "severity": "info", "default_action": "none"},
"TM-003": {"name": "GRASP_TIMEOUT", "severity": "warn", "default_action": "retry"},
"TM-004": {"name": "REORIENT_FAIL", "severity": "warn", "default_action": "replan"},
"TM-005": {
"name": "PLACE_OUT_OF_ZONE",
"severity": "warn",
"default_action": "retry",
},
"TM-006": {"name": "SKILL_FAULT", "severity": "error", "default_action": "replan"},
"TM-007": {
"name": "SOFTWARE_FAULT",
"severity": "critical",
"default_action": "pause",
},
"TM-008": {"name": "MODE_BLOCKED", "severity": "info", "default_action": "none"},
}
@dataclass
class DiagnoseDecision:
code: str
action: str # retry / replan / pause / abort / none
reason: str = ""
class TaskDiagnostics:
"""負責把任意故障事件 → FaultRecord → 寫 Redis + 廣播 + 回給 Scheduler。
Scheduler 在註冊時傳入回呼(`scheduler_action_callback`),
這樣 Diagnostics 不需要直接 import Scheduler,避免循環依賴。
"""
def __init__(
self,
collaborator: Any,
context_store: ContextStore,
scheduler_action_callback: Optional[Callable[[DiagnoseDecision], None]] = None,
fault_channel: str = "task_faults",
) -> None:
self.collaborator = collaborator
self.context_store = context_store
self.fault_channel = fault_channel
self._scheduler_cb = scheduler_action_callback
self._lock = threading.Lock()
# --- 對外 API ---------------------------------------------------------
def raise_fault(
self,
task_id: str,
code: str,
details: Optional[Dict[str, Any]] = None,
subtask_id: Optional[str] = None,
package_id: Optional[str] = None,
action_override: Optional[str] = None,
) -> FaultRecord:
info = FAULT_TABLE.get(code) or {
"name": "UNKNOWN",
"severity": "error",
"default_action": "pause",
}
action = action_override or info["default_action"]
record = FaultRecord(
code=code,
name=info["name"],
severity=info["severity"],
task_id=task_id,
subtask_id=subtask_id,
package_id=package_id,
details=details or {},
action_taken=action,
)
self._publish_and_persist(record)
self._dispatch_action(record)
return record
def from_skill_event(self, evt: SkillEvent) -> Optional[FaultRecord]:
"""技能事件 → 任務級故障(自動翻譯 SK-* 為 TM-006)。"""
if evt.status == "ok":
return None
details = {
"skill_name": evt.skill_name,
"phase": evt.phase,
"status": evt.status,
"upstream_code": evt.fault_code,
}
details.update(evt.details or {})
return self.raise_fault(
task_id=evt.task_id,
code="TM-006",
details=details,
subtask_id=evt.subtask_id,
)
def soft_error(self, task_id: str, exc: BaseException) -> FaultRecord:
return self.raise_fault(
task_id=task_id,
code="TM-007",
details={"exception": repr(exc)},
)
def list_faults(self, task_id: str) -> List[Dict[str, Any]]:
ctx = self.context_store.get(task_id)
return list(ctx.faults) if ctx else []
def clear_fault(self, task_id: str, code: str) -> int:
with self._lock:
ctx = self.context_store.get(task_id)
if not ctx:
return 0
before = len(ctx.faults)
ctx.faults = [f for f in ctx.faults if f.get("code") != code]
self.context_store.save(ctx)
return before - len(ctx.faults)
# --- 內部 -------------------------------------------------------------
def _publish_and_persist(self, record: FaultRecord) -> None:
# 1) 廣播
try:
self.collaborator.send(self.fault_channel, json.dumps(record.to_dict()))
except Exception as e: # noqa: BLE001
logger.warning("[Diagnostics] publish failed: %s", e)
# 2) 寫 context.faults
try:
ctx: Optional[TaskContext] = self.context_store.get(record.task_id)
if ctx:
self.context_store.append_fault(ctx, record)
else:
logger.info(
"[Diagnostics] context for task_id=%s not found, skip persist",
record.task_id,
)
except Exception as e: # noqa: BLE001
logger.warning("[Diagnostics] persist failed: %s", e)
# 3) 統計
try:
self.context_store.bump_stat(f"fault_{record.code}", 1)
except Exception: # noqa: BLE001
pass
logger.warning(
"[Diagnostics] fault raised: %s (%s) action=%s details=%s",
record.code,
record.name,
record.action_taken,
record.details,
)
def _dispatch_action(self, record: FaultRecord) -> None:
if not self._scheduler_cb:
return
decision = DiagnoseDecision(
code=record.code,
action=record.action_taken,
reason=f"{record.code}:{record.name}",
)
try:
self._scheduler_cb(decision)
except Exception as e: # noqa: BLE001
logger.exception("[Diagnostics] scheduler callback error: %s", e)
__all__ = ["TaskDiagnostics", "DiagnoseDecision", "FAULT_TABLE"]
관련 글
Humanoid
python
업데이트 2026-03-12
run.py
run.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/deploy/run.py).
글 읽기 →
Humanoid
python
업데이트 2026-03-12
utils.py
utils.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/deploy/utils.py).
글 읽기 →
Humanoid
python
업데이트 2026-03-12
agent.py
agent.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/agents/agent.py).
글 읽기 →
Humanoid
python
업데이트 2026-03-12
planner.py
planner.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/agents/planner.py).
글 읽기 →
Humanoid
python
업데이트 2026-05-12
prompts.py
prompts.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/agents/prompts.py).
글 읽기 →
Humanoid
python
업데이트 2026-05-12
run.py
run.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/run.py).
글 읽기 →