3535log = logging .getLogger ("MEC.error_assistant" )
3636
3737# =====================================================================
38- # Tier 1 — deterministic patterns
38+ # Tier 1 — deterministic patterns (Doctor-style: JSON pattern packs)
3939# =====================================================================
4040@dataclass
4141class Pattern :
@@ -45,14 +45,115 @@ class Pattern:
4545 message_re : re .Pattern [str ] # case-insensitive by default
4646 cause : str
4747 fixes : List [str ] = field (default_factory = list )
48+ # Doctor-style metadata
49+ category : str = "uncategorized"
50+ priority : int = 100
51+ confidence : float = 0.8
52+ source : str = "builtin" # provenance: which pack file
4853
4954
5055def _r (pat : str ) -> re .Pattern [str ]:
5156 return re .compile (pat , re .IGNORECASE | re .DOTALL )
5257
5358
59+ # ---------------------------------------------------------------------
60+ # JSON pattern-pack loader (hot-reload on file mtime change)
61+ # ---------------------------------------------------------------------
62+ def _patterns_root () -> str :
63+ here = os .path .dirname (os .path .abspath (__file__ ))
64+ pack_root = os .path .dirname (here )
65+ return os .path .join (pack_root , "patterns" )
66+
67+
68+ _PATTERNS_LOCK = threading .Lock ()
69+ _PATTERNS_CACHE : List [Pattern ] = []
70+ _PATTERNS_MTIME : Dict [str , float ] = {} # path -> last-seen mtime
71+
72+
73+ def _scan_pattern_files () -> List [str ]:
74+ root = _patterns_root ()
75+ out : List [str ] = []
76+ if not os .path .isdir (root ):
77+ return out
78+ for dirpath , _dirs , files in os .walk (root ):
79+ for f in files :
80+ if f .endswith (".json" ):
81+ out .append (os .path .join (dirpath , f ))
82+ return sorted (out )
83+
84+
85+ def _parse_pattern_dict (d : Dict [str , Any ], source : str ) -> Optional [Pattern ]:
86+ try :
87+ pid = str (d ["id" ])
88+ regex = d ["regex" ]
89+ cause = d .get ("cause" , "" )
90+ exc_types = tuple (d .get ("exc_types" ) or ())
91+ return Pattern (
92+ name = pid ,
93+ exc_types = exc_types ,
94+ message_re = _r (regex ),
95+ cause = cause ,
96+ fixes = list (d .get ("fixes" ) or []),
97+ category = str (d .get ("category" , "uncategorized" )),
98+ priority = int (d .get ("priority" , 100 )),
99+ confidence = float (d .get ("confidence" , 0.8 )),
100+ source = source ,
101+ )
102+ except Exception as e :
103+ log .warning ("[error_assistant] bad pattern in %s: %s" , source , e )
104+ return None
105+
106+
107+ def _load_patterns_from_json () -> List [Pattern ]:
108+ import json
109+ out : List [Pattern ] = []
110+ for path in _scan_pattern_files ():
111+ try :
112+ with open (path , "r" , encoding = "utf-8" ) as f :
113+ data = json .load (f )
114+ rel = os .path .relpath (path , _patterns_root ())
115+ for entry in data .get ("patterns" , []):
116+ p = _parse_pattern_dict (entry , source = rel .replace ("\\ " , "/" ))
117+ if p is not None :
118+ out .append (p )
119+ except Exception as e :
120+ log .warning ("[error_assistant] failed to load pattern pack %s: %s" , path , e )
121+ # Lower priority value = evaluated first (more specific). Stable sort.
122+ out .sort (key = lambda p : (p .priority , p .name ))
123+ return out
124+
125+
126+ def _get_patterns () -> List [Pattern ]:
127+ """Return current patterns; hot-reload on any pack file mtime change."""
128+ global _PATTERNS_CACHE , _PATTERNS_MTIME
129+ with _PATTERNS_LOCK :
130+ files = _scan_pattern_files ()
131+ cur_mtimes : Dict [str , float ] = {}
132+ for p in files :
133+ try :
134+ cur_mtimes [p ] = os .path .getmtime (p )
135+ except OSError :
136+ cur_mtimes [p ] = 0.0
137+ if cur_mtimes != _PATTERNS_MTIME or not _PATTERNS_CACHE :
138+ loaded = _load_patterns_from_json ()
139+ if loaded :
140+ _PATTERNS_CACHE = loaded
141+ _PATTERNS_MTIME = cur_mtimes
142+ log .info ("[error_assistant] loaded %d patterns from %d pack(s)" ,
143+ len (loaded ), len (files ))
144+ elif not _PATTERNS_CACHE :
145+ # JSON missing/corrupt on first run: fall back to bootstrap list.
146+ _PATTERNS_CACHE = list (_BOOTSTRAP_PATTERNS )
147+ _PATTERNS_MTIME = cur_mtimes
148+ log .warning ("[error_assistant] no JSON packs found; using %d "
149+ "bootstrap patterns" , len (_PATTERNS_CACHE ))
150+ return _PATTERNS_CACHE
151+
152+
54153# Order matters — first match wins. Most-specific patterns first.
55- PATTERNS : List [Pattern ] = [
154+ # This list is ONLY used as a bootstrap fallback when patterns/builtin/core.json
155+ # is missing or unreadable. The canonical source of truth is the JSON pack.
156+ _BOOTSTRAP_PATTERNS : List [Pattern ] = [
56157 # ── VRAM / CUDA ────────────────────────────────────────────────
57158 Pattern (
58159 "cuda_oom" ,
@@ -304,7 +405,7 @@ def _r(pat: str) -> re.Pattern[str]:
304405
305406
306407def match_pattern (exc_type : str , msg : str ) -> Optional [Pattern ]:
307- for p in PATTERNS :
408+ for p in _get_patterns () :
308409 if p .exc_types and exc_type not in p .exc_types and "Exception" not in p .exc_types :
309410 continue
310411 if p .message_re .search (msg ):
@@ -481,6 +582,13 @@ def explain(exc: BaseException,
481582 "cause" : p .cause ,
482583 "fixes" : list (p .fixes ),
483584 "pattern_id" : p .name ,
585+ "category" : p .category ,
586+ "confidence" : p .confidence ,
587+ "provenance" : {
588+ "pack" : "ComfyUI-CustomNodePacks" ,
589+ "source" : p .source ,
590+ "priority" : p .priority ,
591+ },
484592 }
485593
486594 if mode == "deterministic_only" :
@@ -526,9 +634,21 @@ def _fallback(exc_type: str, msg: str) -> Dict[str, Any]:
526634 "Enable Tier 2 (local model) or Tier 3 (cloud) in Settings → MEC Error Assistant for richer explanations." ,
527635 ],
528636 "pattern_id" : "no_match" ,
637+ "category" : "uncategorized" ,
638+ "confidence" : 0.0 ,
639+ "provenance" : {"pack" : "ComfyUI-CustomNodePacks" , "source" : "fallback" },
529640 }
530641
531642
643+ def reload_patterns () -> int :
644+ """Force a re-scan of patterns/ pack files. Returns number of patterns loaded."""
645+ global _PATTERNS_CACHE , _PATTERNS_MTIME
646+ with _PATTERNS_LOCK :
647+ _PATTERNS_CACHE = []
648+ _PATTERNS_MTIME = {}
649+ return len (_get_patterns ())
650+
651+
532652def _format_llm_result (text : str , * , tier : int , tier1 : Optional [dict ],
533653 exc_type : str , msg : str , provider : str , model : str ) -> Dict [str , Any ]:
534654 """Parse loose CAUSE/FIXES sections out of the LLM's reply."""
@@ -553,4 +673,12 @@ def _format_llm_result(text: str, *, tier: int, tier1: Optional[dict],
553673 "provider" : provider ,
554674 "model" : model ,
555675 "tier1_match" : (tier1 or {}).get ("pattern_id" ),
676+ "category" : (tier1 or {}).get ("category" , "uncategorized" ),
677+ "confidence" : (tier1 or {}).get ("confidence" , 0.5 ),
678+ "provenance" : {
679+ "pack" : "ComfyUI-CustomNodePacks" ,
680+ "source" : (tier1 or {}).get ("provenance" , {}).get ("source" , "llm" ),
681+ "llm_provider" : provider ,
682+ "llm_model" : model ,
683+ },
556684 }
0 commit comments