- deploy/bot/ — XMPP bot核心(xmpp_agent_core + xmpp_zhiwei_bot) - deploy/profile-scripts/ — cron脚本(price_monitor等) - 运行时文件已替换为指向MoFin的符号链接 - 改代码只需改MoFin,系统自动生效
169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""json_failure_monitor.py — JSON解析失败监控 v1
|
|
|
|
监控知微pipeline中LLM输出JSON解析失败事件,记录模型名+原始输出前200字符。
|
|
配合 cron_health_monitor.py 使用。
|
|
|
|
运行方式:
|
|
python3 json_failure_monitor.py # 扫描agent.log最近的JSON解析失败
|
|
python3 json_failure_monitor.py --watch # 持续跟踪新条目
|
|
|
|
输出:JSON解析失败事件写入 ~/.hermes/profiles/position-analyst/logs/json_failures.jsonl
|
|
每行一个JSON对象,字段:timestamp, model, provider, session_id, raw_preview(前200字)
|
|
"""
|
|
|
|
import json, os, re, sys, time
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
|
|
PROFILE_DIR = Path.home() / ".hermes" / "profiles" / "position-analyst"
|
|
LOG_DIR = PROFILE_DIR / "logs"
|
|
FAILURES_LOG = LOG_DIR / "json_failures.jsonl"
|
|
|
|
# 匹配JSON解析失败的模式(从 agent.log 中提取)
|
|
# 常见模式:json.loads/json.JSONDecodeError/SyntaxError/Invalid JSON
|
|
JSON_FAILURE_PATTERNS = [
|
|
re.compile(r'json\.loads.*Error', re.I),
|
|
re.compile(r'JSONDecodeError', re.I),
|
|
re.compile(r'Expecting.*value.*char', re.I),
|
|
re.compile(r'Invalid control character', re.I),
|
|
re.compile(r'SyntaxError.*invalid syntax.*json', re.I),
|
|
re.compile(r'not (valid |)JSON', re.I),
|
|
re.compile(r'JSON schema.*mismatch', re.I),
|
|
re.compile(r'fail.*parse.*JSON', re.I),
|
|
re.compile(r'Failed to decode JSON', re.I),
|
|
re.compile(r'output.*not.*valid.*json', re.I),
|
|
]
|
|
|
|
# 提取模型名
|
|
MODEL_PATTERN = re.compile(r'model=([\w.-]+)')
|
|
PROVIDER_PATTERN = re.compile(r'provider=([\w.-]+)')
|
|
SESSION_PATTERN = re.compile(r'\[([\w_-]+)\]')
|
|
|
|
|
|
def scan_log(log_path, since_pos=0):
|
|
"""扫描日志文件中的JSON解析失败事件"""
|
|
if not log_path.exists():
|
|
return [], since_pos
|
|
|
|
events = []
|
|
size = log_path.stat().st_size
|
|
|
|
with open(log_path, 'r', errors='replace') as f:
|
|
if since_pos > 0:
|
|
f.seek(since_pos)
|
|
|
|
for line in f:
|
|
# 检查是否匹配JSON失败模式
|
|
is_failure = any(p.search(line) for p in JSON_FAILURE_PATTERNS)
|
|
if not is_failure:
|
|
continue
|
|
|
|
# 提取元数据
|
|
model_m = MODEL_PATTERN.search(line)
|
|
provider_m = PROVIDER_PATTERN.search(line)
|
|
session_m = SESSION_PATTERN.search(line)
|
|
|
|
event = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"model": model_m.group(1) if model_m else "unknown",
|
|
"provider": provider_m.group(1) if provider_m else "unknown",
|
|
"session_id": session_m.group(1) if session_m else "unknown",
|
|
"raw_preview": line[:200].strip(),
|
|
}
|
|
events.append(event)
|
|
|
|
since_pos = f.tell()
|
|
|
|
return events, since_pos
|
|
|
|
|
|
def write_events(events):
|
|
"""写入JSON失败事件到jsonl文件"""
|
|
if not events:
|
|
return 0
|
|
|
|
with open(FAILURES_LOG, 'a') as f:
|
|
for ev in events:
|
|
f.write(json.dumps(ev, ensure_ascii=False) + '\n')
|
|
|
|
return len(events)
|
|
|
|
|
|
def summarize_recent(hours=24):
|
|
"""输出最近N小时的JSON解析失败统计"""
|
|
if not FAILURES_LOG.exists():
|
|
return "暂无JSON解析失败记录"
|
|
|
|
cutoff = time.time() - hours * 3600
|
|
events = []
|
|
|
|
with open(FAILURES_LOG, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
ev = json.loads(line)
|
|
events.append(ev)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
# 筛选近期
|
|
recent = []
|
|
for ev in events:
|
|
try:
|
|
t = datetime.fromisoformat(ev.get("timestamp", "")).timestamp()
|
|
if t >= cutoff:
|
|
recent.append(ev)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
if not recent:
|
|
return f"最近{hours}小时内无JSON解析失败事件"
|
|
|
|
# 按模型聚合
|
|
by_model = {}
|
|
for ev in recent:
|
|
model = ev.get("model", "unknown")
|
|
by_model.setdefault(model, {"count": 0, "sessions": set()})
|
|
by_model[model]["count"] += 1
|
|
by_model[model]["sessions"].add(ev.get("session_id", ""))
|
|
|
|
lines = [f"最近{hours}h JSON解析失败统计:共{len(recent)}次"]
|
|
for model, info in sorted(by_model.items(), key=lambda x: -x[1]["count"]):
|
|
lines.append(f" {model}: {info['count']}次 (涉及{len(info['sessions'])}个session)")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) > 1 and sys.argv[1] == "--summarize":
|
|
print(summarize_recent())
|
|
sys.exit(0)
|
|
|
|
# 扫描所有agent.log*
|
|
log_files = sorted(LOG_DIR.glob("agent.log*"), reverse=True)
|
|
total = 0
|
|
state_file = PROFILE_DIR / ".json_failure_monitor.state"
|
|
|
|
since_pos = 0
|
|
if state_file.exists():
|
|
try:
|
|
since_pos = int(state_file.read_text().strip())
|
|
except (ValueError, OSError):
|
|
since_pos = 0
|
|
|
|
for lf in log_files:
|
|
events, since_pos = scan_log(lf, since_pos)
|
|
n = write_events(events)
|
|
total += n
|
|
if n > 0:
|
|
print(f"[{datetime.now().isoformat()}] {lf.name}: 发现{n}个JSON解析失败")
|
|
|
|
# 保存扫描位置
|
|
state_file.write_text(str(since_pos))
|
|
|
|
if total == 0:
|
|
print(f"[{datetime.now().isoformat()}] 未发现新的JSON解析失败")
|