ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.pyc
.env
# Agent 自动生成并持久化的解析器(运行时产物),保留目录但忽略生成的 .py
parsers/*.py
!parsers/.gitkeep
+332
View File
@@ -0,0 +1,332 @@
# Experiment 5-7: Adaptive Log Parser / 自适应的日志解析系统(实验 5-7)
> Companion lab for *AI Agents in Depth*, Chapter 5 — self-evolving log parser: on unknown formats, Agent generates `parse`, tests, hot-reloads into the engine.
> 《深入理解 AI Agent》第 5 章「代码作为系统适配器」:遇新格式不报错,Agent 生成解析代码,测试通过后热更新。
← [Chapter 5 index / 返回第 5 章目录](../README.md)
---
## English
### Overview
A **self-evolving** Agent log-parsing system. It starts with basic formats; on unparseable new formats it does not just error—it sends the failed sample + error to an Agent, which generates parsing code, auto-tests, and **hot-updates** the registry. Fully automatic; no human in the loop.
### Self-heal loop
```
one log line
[parse engine] try registered parsers in order
├── some parser matches → structured fields ✅
└── all fail (new format) ❌
│ failed sample + error
[codegen Agent] ← OpenAI(gpt-5.6-luna)
│ emit def parse(line)->dict|None
[auto test] structural asserts (tester.py)
├── fail → feedback to Agent, retry (max 3)
└── pass → [hot-load register] + persist to parsers/*.py
system parses that format ✅ (reuse after restart; no Agent)
```
Code map:
- `engine.py`: parse engine + registry + hot-load (`importlib`). Built-in `builtin_json_parser`.
- `agent.py`: codegen Agent; OpenAI generates parsers; iterative fix with failure feedback.
- `tester.py`: auto tests / structural asserts on generated `parse`.
- `demo.py`: full loop with step-by-step prints.
- `parsers/`: persisted learned parsers for reuse.
### Three progressive formats in the demo
1. Basic JSON lines (native): `{"timestamp": "...", "level": "INFO", "message": "..."}`
2. New format A — pipe-separated: `2026-07-17T10:23:01Z|INFO|agent.planner|step=3|Generated plan...`
3. New format B — nested brackets: `[2026-07-17 10:24:55] (ERROR) <tool=web_search> {latency_ms=812 status=timeout} :: ...`
A and B fail on first parse → Agent generates parser → tests pass → hot update succeeds.
### Run
```bash
# From the repository root: use the shared Chapter 5 environment
uv sync --locked --python 3.12 --extra ch5
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch5]"
cd chapter5/adaptive-log-parser
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
cp env.example .env # OPENAI_API_KEY (default model gpt-5.6-luna); or OPENROUTER_API_KEY fallback
python demo.py # full demo (two new formats, two real Agent calls; needs API key)
python demo.py --offline # offline: canned parsers; no API key
python demo.py --quick # one new format only; one fewer API call
python demo.py --log-file logs.txt # step 3 uses external log file (one line per entry)
python demo.py --output out.jsonl # write structured results as JSONL
python demo.py --help
```
CLI:
| Flag | Description |
| --- | --- |
| `--offline` | Use **canned** parser source instead of OpenAI; no key; deterministic fail-detect → gen → test → hot-reload → persist |
| `--quick` | Only pipe-separated new format; skip nested-brackets; one fewer Agent/API call |
| `--model MODEL` | Override codegen model; else `MODEL` env then `gpt-5.6-luna`. Display-only under `--offline` |
| `--log-file PATH` | External log file (one line each). Step 3 uses the learned system on that file instead of built-in mixed samples |
| `--output PATH` | Write step-3 structured results as JSONL |
Default `demo.py` calls OpenAI for real: (a) detect new-format failure; (b) Agent codegen + auto-test; (c) hot-update parse success; then a new engine loads from `parsers/` to prove persistence (no Agent).
**No API key → `--offline`**: uses `OfflineCodeGenAgent` in `agent.py` (lookup table of prewritten parsers by required fields—not a live LLM), but **fail detect → auto-test → hot-load → persist** matches online mode.
### Sample output (real run excerpt)
From `python demo.py` with gpt-5.6-luna:
```text
步骤 1:遇到新格式 A —— 自定义竖线分隔格式
(a) 先让系统解析,预期【失败】:
❌ 解析失败:2026-07-17T10:23:01Z|INFO|agent.planner|step=3|Generated plan with 5 actions
触发自愈闭环:
🔎 检测到无法解析的新格式,触发自愈。报错:没有任何已注册解析器能解析该行:...
--- 第 1/3 次:Agent 生成解析代码 ---
| import re
| _PATTERN = re.compile(
| r"^\s*"
| r"(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"
| r"\s*\|\s*(?P<level>[A-Za-z]+)\s*\|\s*(?P<module>[^|]+?)"
| r"\s*\|\s*step\s*=\s*(?P<step>\d+)\s*\|\s*(?P<message>\S(?:.*\S)?)\s*$"
| )
| def parse(line: str) -> dict | None:
| match = _PATTERN.match(line)
| if not match:
| return None
| fields = match.groupdict()
| fields["module"] = fields["module"].strip()
| fields["message"] = fields["message"].strip()
| fields["step"] = int(fields["step"])
| return fields
🧪 自动测试(数据结构断言):
[样本1] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']
✅ 自动测试通过,已热更新注册解析器 'pipe_parser' 并持久化到 parsers/pipe_parser.py
(c) 热更新后重新解析同样的日志,预期【成功】:
✅ [pipe_parser] {'_parser': 'pipe_parser', 'timestamp': '2026-07-17T10:23:01Z',
'level': 'INFO', 'module': 'agent.planner', 'step': 3, 'message': 'Generated plan with 5 actions'}
...
演示结束
新格式 A(竖线分隔)自愈结果:成功
新格式 B(嵌套括号)自愈结果:成功
持久化复用(混合格式全部解析):成功
```
> LLM code may vary (names, regex); success = auto-test pass. `python demo.py --offline` makes the loop **deterministic** without a key.
### Adapt / extend
- **Model / provider**: OpenAI-compatible; env only.
- `MODEL` or `python demo.py --model gpt-5.6`.
- `OPENAI_BASE_URL` + matching `OPENAI_API_KEY` / `MODEL`.
- Read in `CodeGenAgent.__init__` in `agent.py`.
- **New log formats**: add samples and required keys like `PIPE_LOGS` / `BRACKET_LOGS` in `demo.py`, then `self_heal(engine, agent, "your_parser", XXX_LOGS, XXX_REQUIRED)`. `required_keys` define test acceptance.
- **Live streams**: call `engine.parse_line(line)` in your read loop; catch `ParseError` to trigger self-heal. Learned parsers in `parsers/*.py` load via `engine.load_persisted()`.
### Limitations
- **Visual QA degraded**: book design renders viz code in a virtual browser + Vision LLM; here that step is **structural asserts** on `parse` (no playwright/browser). Core loop (fail → codegen → test → hot-load → persist) is **real**.
- **Safety**: generated code runs via `importlib`—trusted lab only; production needs sandbox/AST allowlists/resource limits. System prompt already constrains stdlib-only, no side effects.
- **Non-determinism**: LLM codegen is noisy; “fail → feedback retry” up to 3 times; remaining failures are normal—re-run.
---
## 中文
### 概述
一个**能自我进化**的 Agent 日志解析系统。系统初始只支持基础日志格式;遇到无法解析的新格式时,不是报错,
而是自动把失败样本 + 报错交给 Agent,让它生成能正确解析的代码,自动测试通过后**热更新**
注册进解析系统。全流程自动化,无需人工介入。
### 自愈闭环
```
一行日志
[解析引擎] 依次尝试已注册的解析器
├── 有解析器认识 → 输出结构化字段 ✅
└── 全部失败(检测到新格式)❌
│ 失败样本 + 报错
[代码生成 Agent] ← OpenAI(gpt-5.6-luna)
│ 生成 def parse(line)->dict|None
[自动测试] 数据结构断言(tester.py
├── 不通过 → 把失败报告反馈给 Agent 重试(最多 3 次)
└── 通过 → [热加载注册] + 持久化到 parsers/*.py
系统现在能正确解析该新格式 ✅(下次重启直接复用,不再问 Agent)
```
对应代码:
- `engine.py`:解析引擎 + 解析器注册表 + 热加载(`importlib`)。内置 `builtin_json_parser`
- `agent.py`:代码生成 Agent,调用 OpenAI 生成解析函数,支持带失败反馈迭代修复。
- `tester.py`:自动测试,对生成的 `parse` 函数做数据结构断言。
- `demo.py`:串起整条闭环并逐步打印。
- `parsers/`:Agent 学会的解析器持久化到这里,供下次直接复用。
### 演示的三种递进格式
1. 基础 JSON 行(系统原生支持):`{"timestamp": "...", "level": "INFO", "message": "..."}`
2. 新格式 A —— 自定义竖线分隔:`2026-07-17T10:23:01Z|INFO|agent.planner|step=3|Generated plan...`
3. 新格式 B —— 嵌套括号:`[2026-07-17 10:24:55] (ERROR) <tool=web_search> {latency_ms=812 status=timeout} :: ...`
格式 A、B 初次解析都会失败,触发 Agent 生成解析器 → 自动测试通过 → 热更新后能正确解析。
### 运行
```bash
# 在仓库根目录使用统一的第 5 章环境
uv sync --locked --python 3.12 --extra ch5
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch5]"
cd chapter5/adaptive-log-parser
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
cp env.example .env # 填入 OPENAI_API_KEY(默认模型 gpt-5.6-luna);未配置时设 OPENROUTER_API_KEY 自动改走 OpenRouter
python demo.py # 完整演示(两种新格式,两次真实 Agent 调用,需 API Key
python demo.py --offline # 离线演示:用预置解析器跑完整机制,无需 API Key
python demo.py --quick # 快速模式:只演示 1 种新格式,省一次 API 调用
python demo.py --log-file logs.txt # 步骤 3 改用外部日志文件(每行一条)验证复用
python demo.py --output out.jsonl # 把解析出的结构化结果写成 JSONL
python demo.py --help # 查看全部参数
```
命令行参数:
| 参数 | 说明 |
| --- | --- |
| `--offline` | 用**预置**(canned)解析器代码代替调用 OpenAI,无需 API Key,确定性地演示整条机制(失败检测→生成→测试→热重载→持久化)。 |
| `--quick` | 只演示 1 种新格式(竖线分隔),跳过嵌套括号格式,省一次 Agent/API 调用。 |
| `--model MODEL` | 覆盖代码生成模型;默认读 `MODEL` 环境变量再回落 `gpt-5.6-luna``--offline` 下仅作展示。 |
| `--log-file PATH` | 外部日志文件(每行一条)。给定后步骤 3 改用学到的解析系统解析该文件,替代内置混合样本,验证解析器可复用到真实日志流。 |
| `--output PATH` | 把步骤 3 解析出的结构化结果以 JSONL(每行一条 JSON)写入该文件。 |
`demo.py` 默认真实调用 OpenAI,依次演示:(a) 新格式初次解析失败被检测到;
(b) Agent 生成解析代码并通过自动测试;(c) 热更新后系统正确解析该新格式并打印结构化结果;
最后新建一个引擎,直接从 `parsers/` 加载已学会的解析器,验证持久化复用(不再调用 Agent)。
**没有 API Key 时用 `--offline`**:离线模式换用 `agent.py` 里的 `OfflineCodeGenAgent`,它按必需字段
查表返回预写好的解析器源码(并非真让 LLM 现写),但**失败检测→自动测试→热加载注册→持久化**这些
运行时机制与在线模式完全一致,可完整跑通并验证闭环。
### 预期输出示例(真实运行片段)
以下摘自一次真实运行(`python demo.py`,模型 gpt-5.6-luna):
```text
步骤 1:遇到新格式 A —— 自定义竖线分隔格式
(a) 先让系统解析,预期【失败】:
❌ 解析失败:2026-07-17T10:23:01Z|INFO|agent.planner|step=3|Generated plan with 5 actions
触发自愈闭环:
🔎 检测到无法解析的新格式,触发自愈。报错:没有任何已注册解析器能解析该行:...
--- 第 1/3 次:Agent 生成解析代码 ---
| import re
| _PATTERN = re.compile(
| r"^\s*"
| r"(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"
| r"\s*\|\s*(?P<level>[A-Za-z]+)\s*\|\s*(?P<module>[^|]+?)"
| r"\s*\|\s*step\s*=\s*(?P<step>\d+)\s*\|\s*(?P<message>\S(?:.*\S)?)\s*$"
| )
| def parse(line: str) -> dict | None:
| match = _PATTERN.match(line)
| if not match:
| return None
| fields = match.groupdict()
| fields["module"] = fields["module"].strip()
| fields["message"] = fields["message"].strip()
| fields["step"] = int(fields["step"])
| return fields
🧪 自动测试(数据结构断言):
[样本1] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']
✅ 自动测试通过,已热更新注册解析器 'pipe_parser' 并持久化到 parsers/pipe_parser.py
(c) 热更新后重新解析同样的日志,预期【成功】:
✅ [pipe_parser] {'_parser': 'pipe_parser', 'timestamp': '2026-07-17T10:23:01Z',
'level': 'INFO', 'module': 'agent.planner', 'step': 3, 'message': 'Generated plan with 5 actions'}
...
演示结束
新格式 A(竖线分隔)自愈结果:成功
新格式 B(嵌套括号)自愈结果:成功
持久化复用(混合格式全部解析):成功
```
> LLM 生成的代码每次可能略有不同(如变量名、正则写法),但只要通过自动测试即视为成功。
> 若用 `python demo.py --offline`,预置解析器让输出**确定性**复现上述闭环(无需 API Key)。
### 如何适配 / 扩展
- **换模型 / 供应商**:本项目统一走 OpenAI 兼容协议,改环境变量即可,无需改代码。
- `MODEL`:换模型,例如 `MODEL=gpt-5.6`;也可在命令行用 `python demo.py --model gpt-5.6` 临时覆盖。
- `OPENAI_BASE_URL`:换成任意 OpenAI 兼容端点(如自建网关、Moonshot/火山方舟等),
再把 `OPENAI_API_KEY` 换成对应服务的 key、`MODEL` 换成该服务的模型名即可。
- 三者的读取逻辑集中在 `agent.py``CodeGenAgent.__init__`
- **换输入日志格式**:在 `demo.py` 里按现有 `PIPE_LOGS` / `BRACKET_LOGS` 的写法,加一组
你自己的样本(`XXX_LOGS`)和必需字段列表(`XXX_REQUIRED`),再调一次
`self_heal(engine, agent, "your_parser", XXX_LOGS, XXX_REQUIRED)` 即可让系统自学。
`required_keys` 决定自动测试的验收标准(哪些字段必须被解析出且非空)。
- **接入真实日志流**:把 `engine.parse_line(line)` 接到你的日志读取循环上;捕获
`ParseError` 即触发自愈闭环。已学会的解析器持久化在 `parsers/*.py`,重启后由
`engine.load_persisted()` 自动加载复用。
### 局限与说明
- **可视化验证降级**:书中原方案是把生成的可视化代码放进**虚拟浏览器**渲染,再用
**Vision LLM** 检查渲染效果。本机没有 playwright/浏览器环境,因此把这一步降级为对
生成的解析函数做**数据结构断言**(用样本数据断言解析出的结构化字段正确)。核心闭环
(检测失败 → 生成解析代码 → 自动测试 → 热加载注册新解析器 → 持久化复用)是**真实实现**的。
- **安全性**:Agent 生成的代码通过 `importlib` 直接执行,仅适用于可信实验环境;生产中应
加沙箱、AST 白名单、资源限制等隔离手段。系统提示已约束只用标准库、无副作用。
- **确定性**:LLM 生成代码存在不确定性,故设置了「测试不通过→带反馈重试」的迭代修复
(最多 3 次);仍可能失败,属正常现象,重跑即可。
---
## Notes / 说明
- Use `--offline` without a key. / 无 Key 用 `--offline`
- Commands/code/paths/env vars are identical in both language sections. / 命令、代码、路径与环境变量在中英文两侧保持一致。
+200
View File
@@ -0,0 +1,200 @@
"""
agent.py —— 代码生成 Agent(自愈闭环的“大脑”)
职责:拿到无法解析的失败样本 + 报错,调用 OpenAI,生成一个能正确解析该格式的
Python 解析函数 `def parse(line: str) -> dict | None`。支持把上一轮自动测试的
失败报告作为反馈再次生成(迭代修复)。
"""
from __future__ import annotations
import os
import re
from typing import List, Optional
from openai import OpenAI
# .env 加载(可选依赖)
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
SYSTEM_PROMPT = """你是一个"日志解析器代码生成器"。用户会给你一批**同一种未知格式**的日志样本,
以及现有系统解析失败的报错。你的任务:编写一个 Python 函数,把这种格式的每一行解析成结构化字段。
严格要求:
1. 只输出一个 Python 代码块(```python ... ```),不要任何解释文字。
2. 代码块里必须定义一个函数:def parse(line: str) -> dict | None
- 输入是一行日志(字符串)。
- 如果这行符合你要解析的格式,返回一个 dict,键为字段名(英文小写下划线),值为解析出的内容。
- 如果这行**不符合**这种格式,必须返回 None(不要抛异常,把机会让给其它解析器)。
3. 只能使用 Python 标准库(re、json、datetime 等),不要 import 第三方库。
4. 不要有任何 print、input、文件读写、网络访问等副作用。
5. 必须解析出用户指定的**所有必需字段**(required_keys),字段值不能为空。
6. 尽量健壮:用正则/分隔符解析,容忍字段顺序内的空格。
"""
def _build_user_prompt(
samples: List[str],
required_keys: List[str],
error_report: str,
feedback: Optional[str],
) -> str:
sample_block = "\n".join(samples)
parts = [
"现有系统无法解析下面这种格式的日志,请生成解析函数。",
"",
"【失败样本(同一种新格式)】",
sample_block,
"",
f"【系统报错】\n{error_report}",
"",
f"【必需解析出的字段 required_keys】\n{required_keys}",
]
if feedback:
parts += [
"",
"【上一版代码没通过自动测试,请修复后重新生成】",
feedback,
]
return "\n".join(parts)
def _extract_code(text: str) -> str:
"""从模型回复中抽取 Python 代码块;没有围栏时退回整段文本。"""
m = re.search(r"```(?:python)?\s*(.*?)```", text, re.DOTALL)
return (m.group(1) if m else text).strip()
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def _map_to_openrouter_model(model: str) -> str:
"""把直连模型名映射为 OpenRouter 上的 id(非可映射 id 统一兜底到当前廉价旗舰)。"""
if not model or "/" in model:
return model or "openai/gpt-5.6-luna"
m = model.lower()
if m.startswith(("gpt-", "o1", "o3", "o4")):
return "openai/" + model
if m.startswith("claude"):
if "haiku" in m:
return "anthropic/claude-haiku-4.5"
if "sonnet" in m:
return "anthropic/claude-sonnet-4.6"
return "anthropic/claude-opus-4.8"
if m.startswith("gemini"):
return "google/" + model
return "openai/gpt-5.6-luna"
class CodeGenAgent:
def __init__(self, model: Optional[str] = None):
model = model or os.getenv("MODEL", "gpt-5.6-luna")
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_BASE_URL")
orkey = os.getenv("OPENROUTER_API_KEY")
# 通用 OpenRouter 兜底:无直连 key,或默认 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
if prefer_or or (not api_key and orkey):
api_key, base_url, model = orkey, OPENROUTER_BASE_URL, _map_to_openrouter_model(model)
if not api_key:
raise SystemExit("未找到 OPENAI_API_KEY(或 OPENROUTER_API_KEY 兜底),请在环境变量或 .env 中设置。")
# timeout / max_retries:让偶发的网络/SSL 抖动自动重试,不至于整轮崩溃
client_kwargs = {"api_key": api_key, "timeout": 60.0, "max_retries": 3}
if base_url:
client_kwargs["base_url"] = base_url
self.client = OpenAI(**client_kwargs)
self.model = model
def generate_parser_code(
self,
samples: List[str],
required_keys: List[str],
error_report: str,
feedback: Optional[str] = None,
) -> str:
"""调用 LLM 生成解析器代码,返回纯 Python 源码字符串。"""
user_prompt = _build_user_prompt(samples, required_keys, error_report, feedback)
# 推理模型(gpt-5 / o 系列等)不接受 temperature=0。
_reasoning = any(k in (self.model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
resp = self.client.chat.completions.create(
model=self.model,
temperature=1 if _reasoning else 0,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
)
return _extract_code(resp.choices[0].message.content or "")
# ---------------------------------------------------------------------------
# 离线(无 API)代码生成 Agent
# ---------------------------------------------------------------------------
# 与 CodeGenAgent 接口完全一致,但不调用 OpenAI,而是根据必需字段返回**预置**的
# 解析器源码。它的用途是:在没有 API Key 的环境里,仍能确定性地演示与验证整条
# 机制——失败检测 → (预置)生成代码 → 自动测试 → 热加载注册 → 持久化复用。
# 注意:这里的“生成”是查表返回预写好的代码,并非真正让 LLM 现写;只有换用
# CodeGenAgent 才是真正的代码生成。
_CANNED_PARSERS = {
# 竖线分隔格式:时间戳|级别|模块|step=N|消息
frozenset(["timestamp", "level", "module", "step", "message"]): '''import re
def parse(line: str) -> dict | None:
pattern = (
r"^(?P<timestamp>\\S+)\\|(?P<level>\\S+)\\|(?P<module>\\S+)"
r"\\|step=(?P<step>\\d+)\\|(?P<message>.+)$"
)
match = re.match(pattern, line.strip())
if match:
return match.groupdict()
return None
''',
# 嵌套括号格式:[时间] (级别) <tool=名字> {k=v k=v} :: 消息
frozenset(["timestamp", "level", "tool", "message"]): '''import re
def parse(line: str) -> dict | None:
pattern = (
r"\\[(?P<timestamp>.*?)\\] \\((?P<level>.*?)\\) <tool=(?P<tool>.*?)> "
r"\\{latency_ms=(?P<latency_ms>\\d+) status=(?P<status>\\w+)\\} :: (?P<message>.*)"
)
match = re.match(pattern, line.strip())
if match:
return match.groupdict()
return None
''',
}
class OfflineCodeGenAgent:
"""离线桩:查表返回预置解析器代码,接口与 CodeGenAgent 一致(无需 API Key)。"""
def __init__(self, model: Optional[str] = None):
self.model = model or "offline-canned"
def generate_parser_code(
self,
samples: List[str],
required_keys: List[str],
error_report: str,
feedback: Optional[str] = None,
) -> str:
key = frozenset(required_keys)
code = _CANNED_PARSERS.get(key)
if code is not None:
return code
# 未预置该格式:返回一个永远返回 None 的桩,让自动测试如实失败,
# 从而演示“测试未通过 → 放弃该格式”的分支(离线模式无法真正现写代码)。
return (
"def parse(line: str) -> dict | None:\n"
" # 离线模式未预置该格式的解析器\n"
" return None\n"
)
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""Live self-healing parser + browser/Vision campaign for Experiment 5-7."""
from __future__ import annotations
import argparse
import ast
import base64
import datetime as dt
import hashlib
import html
import io
import json
import os
import re
import shutil
import subprocess
import time
from pathlib import Path
from typing import Any
from openai import OpenAI
from PIL import Image
from playwright.sync_api import sync_playwright
from agent import SYSTEM_PROMPT, _build_user_prompt, _extract_code
from engine import LogParserEngine, ParseError, builtin_json_parser
from tester import run_tests
HERE = Path(__file__).resolve().parent
FORMATS = [
{
"name": "live_pipe_parser",
"required": ["timestamp", "level", "module", "step", "message"],
"script": """import logging, sys
formatter=logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|step=%(step)s|%(message)s', datefmt='%Y-%m-%dT%H:%M:%SZ')
handler=logging.StreamHandler(sys.stdout); handler.setFormatter(formatter)
logger=logging.getLogger('checkout.worker'); logger.handlers=[handler]; logger.setLevel(logging.INFO); logger.propagate=False
logger.info('accepted real request req-81', extra={'step': 1})
logger.warning('retrying payment authorization req-81', extra={'step': 2})
logger.error('authorization exhausted req-81', extra={'step': 3})
""",
},
{
"name": "live_bracket_parser",
"required": ["timestamp", "level", "tool", "latency_ms", "status", "message"],
"script": """import datetime, time
events=[('inventory_lookup',34,'ok','stock check completed'),('payment_api',181,'retry','upstream requested retry'),('payment_api',412,'timeout','deadline exceeded')]
for tool,latency,status,message in events:
started=time.perf_counter(); time.sleep(0.003); observed=max(latency,int((time.perf_counter()-started)*1000))
stamp=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='milliseconds')
level='ERROR' if status=='timeout' else ('WARNING' if status=='retry' else 'INFO')
print(f'[{stamp}] ({level}) <tool={tool}> {{latency_ms={observed} status={status}}} :: {message}', flush=True)
""",
},
]
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def atomic_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
temporary.replace(path)
def backend(provider: str, model: str | None) -> tuple[OpenAI, str, str]:
choices = {
"ark": (os.getenv("ARK_API_KEY"), "https://ark.cn-beijing.volces.com/api/v3", model or "doubao-seed-1-6-250615"),
"moonshot": (os.getenv("MOONSHOT_API_KEY") or os.getenv("KIMI_API_KEY"), "https://api.moonshot.cn/v1", model or "kimi-k3"),
"openrouter": (os.getenv("OPENROUTER_API_KEY"), "https://openrouter.ai/api/v1", model or "openai/gpt-5.6-luna"),
"openai": (os.getenv("OPENAI_API_KEY"), os.getenv("OPENAI_BASE_URL"), model or "gpt-5.6-luna"),
}
key, base_url, resolved = choices[provider]
if not key:
raise RuntimeError(f"provider={provider} has no configured credential")
kwargs: dict[str, Any] = {"api_key": key, "timeout": 180.0, "max_retries": 4}
if base_url:
kwargs["base_url"] = base_url
return OpenAI(**kwargs), resolved, base_url or "https://api.openai.com/v1"
def usage(response) -> dict[str, Any]:
value = response.usage
return {
"prompt_tokens": getattr(value, "prompt_tokens", None),
"completion_tokens": getattr(value, "completion_tokens", None),
"total_tokens": getattr(value, "total_tokens", None),
"cached_prompt_tokens": getattr(getattr(value, "prompt_tokens_details", None), "cached_tokens", None),
}
def assert_safe_parser(source: str) -> None:
tree = ast.parse(source)
allowed_imports = {"re", "json", "datetime"}
functions = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))]
if not any(node.name == "parse" for node in functions):
raise ValueError("generated code has no parse function")
for node in ast.walk(tree):
if isinstance(node, ast.Import):
if any(alias.name.split(".")[0] not in allowed_imports for alias in node.names):
raise ValueError("generated parser imports a disallowed module")
if isinstance(node, ast.ImportFrom) and (node.module or "").split(".")[0] not in allowed_imports:
raise ValueError("generated parser imports a disallowed module")
if isinstance(node, (ast.With, ast.AsyncWith, ast.ClassDef, ast.Global, ast.Nonlocal)):
raise ValueError(f"generated parser contains disallowed {type(node).__name__}")
def model_parser(
client: OpenAI,
model: str,
definition: dict[str, Any],
samples: list[str],
error: str,
parsers_dir: Path,
) -> tuple[Path, list[dict[str, Any]], dict[str, Any]]:
receipts = []
feedback = None
final_test = None
path = parsers_dir / f"{definition['name']}.py"
for attempt in range(1, 4):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": _build_user_prompt(samples, definition["required"], error, feedback)},
]
request = {
"model": model,
"messages": messages,
"temperature": 1 if any(x in model.casefold() for x in ("kimi-k3", "gpt-5", "o1", "o3", "o4")) else 0,
}
started = time.monotonic()
response = client.chat.completions.create(**request)
choice = response.choices[0]
receipt = {
"purpose": f"generate-{definition['name']}-attempt-{attempt}",
"called_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
"latency_s": round(time.monotonic() - started, 3),
"request": request,
"response": {"id": response.id, "model": response.model, "finish_reason": choice.finish_reason, "content": choice.message.content},
"usage": usage(response),
}
receipts.append(receipt)
if choice.finish_reason == "length":
feedback = "The provider truncated the previous program. Return a shorter complete parse function."
continue
source = _extract_code(choice.message.content or "")
try:
assert_safe_parser(source)
path.write_text(source + "\n", encoding="utf-8")
fn = LogParserEngine.load_parser_from_file(str(path))
final_test = run_tests(fn, samples, definition["required"])
if final_test["passed"]:
return path, receipts, final_test
feedback = final_test["report"]
except Exception as exc:
feedback = f"{type(exc).__name__}: {exc}"
raise RuntimeError(f"three real parser attempts failed for {definition['name']}: {feedback}")
def collect_live_logs(run_dir: Path) -> list[dict[str, Any]]:
collected = []
for index, definition in enumerate(FORMATS, 1):
script = run_dir / f"producer-{index}.py"
script.write_text(definition["script"], encoding="utf-8")
started = time.monotonic()
process = subprocess.run(["python", str(script)], capture_output=True, text=True, timeout=30)
if process.returncode != 0:
raise RuntimeError(f"live log producer failed: {process.stderr}")
lines = [line for line in process.stdout.splitlines() if line.strip()]
raw = run_dir / f"live-format-{index}.log"
raw.write_text("\n".join(lines) + "\n", encoding="utf-8")
collected.append({
**definition, "script_path": script, "raw_path": raw, "lines": lines,
"producer_latency_s": round(time.monotonic() - started, 4),
})
return collected
def visualize(run_dir: Path, parsed: list[dict[str, Any]]) -> tuple[dict[str, Any], Path]:
keys = sorted({key for row in parsed for key in row})
rows = "".join(
"<tr>" + "".join(f"<td>{html.escape(str(row.get(key, '')))}</td>" for key in keys) + "</tr>"
for row in parsed
)
document = f"""<!doctype html><meta charset=utf-8><title>Adaptive log parser</title>
<style>body{{font-family:system-ui;background:#0b1020;color:#e8eefc;padding:30px}}table{{border-collapse:collapse;width:100%;background:#121a30}}th,td{{border:1px solid #33415f;padding:9px;text-align:left}}th{{color:#79c0ff}}h1{{color:#a5d6ff}}</style>
<h1>Self-healed live log stream</h1><p>{len(parsed)} runtime records parsed after hot update.</p>
<table><thead><tr>{''.join(f'<th>{html.escape(key)}</th>' for key in keys)}</tr></thead><tbody>{rows}</tbody></table>"""
html_path = run_dir / "visualization.html"
screenshot = run_dir / "visualization.png"
html_path.write_text(document, encoding="utf-8")
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1800, "height": 1000})
page.set_content(document, wait_until="load")
page.screenshot(path=str(screenshot), full_page=True)
result = {"browser": "Chromium", "version": browser.version, "rows": len(parsed), "columns": keys}
browser.close()
return result, screenshot
def vision_review(client: OpenAI, model: str, image_path: Path) -> tuple[dict[str, Any], dict[str, Any]]:
image = Image.open(image_path).convert("RGB")
buffer = io.BytesIO(); image.save(buffer, format="JPEG", quality=85)
encoded = base64.b64encode(buffer.getvalue()).decode()
prompt = "Inspect this rendered adaptive-log table. Return strict JSON: {\"pass\": bool, \"readable\": bool, \"has_multiple_parsers\": bool, \"observed_columns\": [strings], \"reason\": string}. Pass only if the table is readable, contains multiple parsed rows, and visibly includes both parser identifiers and structured fields."
request = {
"model": model,
"messages": [{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{encoded}", "detail": "high"}},
]}],
"temperature": 0,
}
started = time.monotonic()
response = client.chat.completions.create(**request)
choice = response.choices[0]
text = choice.message.content or ""
match = re.search(r"\{.*\}", text, re.S)
if not match:
raise ValueError(f"Vision reviewer returned no JSON: {text}")
judgment = json.loads(match.group(0))
receipt = {
"purpose": "vision-review-rendered-parser-table",
"called_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
"latency_s": round(time.monotonic() - started, 3),
"request": {
"model": model,
"messages": [{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"sha256": sha256(image_path), "bytes": image_path.stat().st_size}},
]}], "temperature": 0,
},
"response": {"id": response.id, "model": response.model, "finish_reason": choice.finish_reason, "content": text},
"usage": usage(response),
}
return judgment, receipt
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--provider", choices=["ark", "moonshot", "openrouter", "openai"], default="ark")
parser.add_argument("--model", default=None)
parser.add_argument("--run-id", default=None)
args = parser.parse_args()
started = dt.datetime.now(dt.timezone.utc)
run_id = args.run_id or started.strftime("%Y%m%dT%H%M%SZ-5_7-live")
run_dir = HERE / "validation" / "runs" / run_id
if run_dir.exists():
raise FileExistsError(f"immutable run exists: {run_dir}")
parsers_dir = run_dir / "parsers"; parsers_dir.mkdir(parents=True)
live = collect_live_logs(run_dir)
client, model, endpoint = backend(args.provider, args.model)
engine = LogParserEngine(); engine.register("builtin_json", builtin_json_parser)
receipts = []; format_records = []
for definition in live:
failures = 0
for line in definition["lines"]:
try: engine.parse_line(line)
except ParseError: failures += 1
if failures != len(definition["lines"]):
raise RuntimeError("new format did not trigger the initial parser failure")
error = str(ParseError(definition["lines"][0]))
path, calls, test = model_parser(client, model, definition, definition["lines"], error, parsers_dir)
receipts.extend(calls)
fn = LogParserEngine.load_parser_from_file(str(path)); engine.register(definition["name"], fn)
after = [engine.parse_line(line) for line in definition["lines"]]
format_records.append({
"name": definition["name"], "raw_log": definition["raw_path"].name,
"raw_log_sha256": sha256(definition["raw_path"]), "samples": len(definition["lines"]),
"initial_failures": failures, "required_keys": definition["required"],
"parser": str(path.relative_to(run_dir)), "parser_sha256": sha256(path),
"test": test, "parsed_after_hot_update": after,
})
restarted = LogParserEngine(); restarted.register("builtin_json", builtin_json_parser)
loaded = restarted.load_persisted(str(parsers_dir))
all_lines = [line for definition in live for line in definition["lines"]]
restarted_rows = [restarted.parse_line(line) for line in all_lines]
browser, screenshot = visualize(run_dir, restarted_rows)
judgment, vision_receipt = vision_review(client, model, screenshot)
receipts.append(vision_receipt)
atomic_json(run_dir / "receipts.json", receipts)
atomic_json(run_dir / "evidence.json", {"formats": format_records, "loaded_after_restart": loaded, "rows_after_restart": restarted_rows, "browser": browser, "vision_judgment": judgment})
gates = {
"raw_logs_emitted_by_real_runtime_processes": all(item["producer_latency_s"] > 0 and item["raw_path"].is_file() for item in live),
"initial_system_detected_every_new_format_failure": all(row["initial_failures"] == row["samples"] for row in format_records),
"real_model_generated_both_parser_modules": len(format_records) == 2 and all(row["parser_sha256"] for row in format_records),
"generated_code_passed_automatic_tests": all(row["test"]["passed"] for row in format_records),
"hot_update_parsed_every_failed_sample": all(len(row["parsed_after_hot_update"]) == row["samples"] for row in format_records),
"persisted_parsers_loaded_after_fresh_engine_restart": set(loaded) == {row["name"] for row in format_records},
"fresh_engine_parsed_entire_mixed_stream": len(restarted_rows) == len(all_lines),
"real_chromium_rendered_visualization": bool(browser["version"] and screenshot.is_file()),
"real_vision_model_approved_rendered_pixels": judgment.get("pass") is True and judgment.get("readable") is True,
"raw_provider_receipts_complete": all(r["response"]["id"] and r["usage"]["total_tokens"] for r in receipts),
}
artifacts = {}
for path in sorted(run_dir.rglob("*")):
if path.is_file() and path.name != "manifest.json":
artifacts[str(path.relative_to(run_dir))] = {"path": str(path.relative_to(run_dir)), "sha256": sha256(path), "bytes": path.stat().st_size}
manifest = {
"schema_version": "1.0", "experiment": "5-7", "run_id": run_id,
"started_at_utc": started.isoformat(), "completed_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
"provider": args.provider, "endpoint": endpoint, "model": model,
"source": {"manuscript": "book/chapter5.md#实验-5-7", "campaign_sha256": sha256(Path(__file__))},
"formats": format_records, "browser": browser, "vision_judgment": judgment,
"usage": {"calls": len(receipts), "prompt_tokens": sum(r["usage"]["prompt_tokens"] or 0 for r in receipts), "completion_tokens": sum(r["usage"]["completion_tokens"] or 0 for r in receipts), "total_tokens": sum(r["usage"]["total_tokens"] or 0 for r in receipts), "latency_s": round(sum(r["latency_s"] for r in receipts), 3)},
"artifacts": artifacts, "acceptance_gates": gates, "official_complete": all(gates.values()),
}
atomic_json(run_dir / "manifest.json", manifest)
(HERE / "validation").mkdir(exist_ok=True)
if manifest["official_complete"]:
shutil.copyfile(run_dir / "manifest.json", HERE / "validation" / "latest.json")
print(json.dumps({"run_id": run_id, "official_complete": manifest["official_complete"], "gates": gates}, ensure_ascii=False, indent=2))
if not manifest["official_complete"]: raise SystemExit(2)
if __name__ == "__main__":
main()
+300
View File
@@ -0,0 +1,300 @@
"""
demo.py —— 自适应日志解析系统:自愈闭环演示
演示整条自愈流程(全流程自动化):
初始系统只认基础 JSON 日志 →
遇到没见过的新格式 → 解析【失败】被检测到 →
把失败样本 + 报错交给 Agent → Agent【生成解析代码】→
【自动测试】(数据结构断言)→ 通过后【热加载注册 + 持久化】→
系统【正确解析】了新格式。
运行:
python demo.py # 完整演示(两种新格式,两次 Agent 调用,需 API Key
python demo.py --offline # 离线演示:用预置解析器跑完整机制,无需 API Key
python demo.py --quick # 快速模式:只演示 1 种新格式,省一次 API 调用
python demo.py --help # 查看全部参数
命令行参数见文件底部的 build_arg_parser()。
"""
from __future__ import annotations
import argparse
import json
import os
import textwrap
from typing import List, Tuple
from engine import LogParserEngine, ParseError, builtin_json_parser
from agent import CodeGenAgent, OfflineCodeGenAgent
from tester import run_tests
HERE = os.path.dirname(os.path.abspath(__file__))
PARSERS_DIR = os.path.join(HERE, "parsers")
MAX_ATTEMPTS = 3 # Agent 生成→测试的最大迭代修复次数
# ---------------------------------------------------------------------------
# 演示用的三种递进日志格式
# ---------------------------------------------------------------------------
# 格式 1:基础 JSON 行 —— 初始系统就支持
JSON_LOGS = [
'{"timestamp": "2026-07-17T10:22:31Z", "level": "INFO", "message": "Agent started task planning"}',
'{"timestamp": "2026-07-17T10:22:33Z", "level": "DEBUG", "message": "Loaded 12 tools into context"}',
]
# 格式 2:自定义竖线分隔格式 —— Agent 没见过
# 时间戳|级别|模块|step=N|消息
PIPE_LOGS = [
"2026-07-17T10:23:01Z|INFO|agent.planner|step=3|Generated plan with 5 actions",
"2026-07-17T10:23:04Z|WARNING|agent.executor|step=4|Tool call retried once",
"2026-07-17T10:23:07Z|ERROR|agent.executor|step=5|Tool web_search returned empty result",
]
PIPE_REQUIRED = ["timestamp", "level", "module", "step", "message"]
# 格式 3:嵌套括号格式 —— Agent 也没见过
# [时间] (级别) <tool=名字> {k=v k=v} :: 消息
BRACKET_LOGS = [
"[2026-07-17 10:24:55] (ERROR) <tool=web_search> {latency_ms=812 status=timeout} :: upstream request failed",
"[2026-07-17 10:25:01] (INFO) <tool=code_run> {latency_ms=134 status=ok} :: executed snippet successfully",
"[2026-07-17 10:25:09] (WARN) <tool=file_read> {latency_ms=45 status=partial} :: file truncated at 1MB",
]
BRACKET_REQUIRED = ["timestamp", "level", "tool", "message"]
# ---------------------------------------------------------------------------
# 小工具
# ---------------------------------------------------------------------------
def hr(title: str = "") -> None:
print("\n" + "=" * 78)
if title:
print(title)
print("=" * 78)
def try_parse_all(
engine: LogParserEngine, logs: List[str]
) -> Tuple[bool, List[dict]]:
"""尝试解析一批日志,打印结果;返回 (是否全部成功, 成功解析出的结构化记录列表)。"""
all_ok = True
records: List[dict] = []
for line in logs:
try:
result = engine.parse_line(line)
records.append(result)
print(f" ✅ [{result['_parser']}] {result}")
except ParseError:
all_ok = False
print(f" ❌ 解析失败:{line}")
return all_ok, records
def read_log_file(path: str) -> List[str]:
"""从外部日志文件读取日志(每行一条,忽略空行)。"""
with open(path, "r", encoding="utf-8") as f:
return [line.rstrip("\n") for line in f if line.strip()]
def write_output(path: str, records: List[dict]) -> None:
"""把解析出的结构化记录写成 JSONL(每行一条 JSON)。"""
with open(path, "w", encoding="utf-8") as f:
for rec in records:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
# ---------------------------------------------------------------------------
# 自愈闭环:检测失败 → 生成 → 测试 → 热更新
# ---------------------------------------------------------------------------
def self_heal(
engine: LogParserEngine,
agent: "CodeGenAgent | OfflineCodeGenAgent",
parser_name: str,
samples: List[str],
required_keys: List[str],
) -> bool:
"""针对一种新格式跑完整的自愈闭环,成功注册返回 True。"""
# (a) 触发原因:拿一条样本让系统解析,确认确实失败
failing_line = samples[0]
try:
engine.parse_line(failing_line)
print(" (该格式已能解析,无需自愈)")
return True
except ParseError as exc:
error_report = str(exc)
print(f" 🔎 检测到无法解析的新格式,触发自愈。报错:{error_report}")
target_path = os.path.join(PARSERS_DIR, f"{parser_name}.py")
feedback = None
for attempt in range(1, MAX_ATTEMPTS + 1):
print(f"\n --- 第 {attempt}/{MAX_ATTEMPTS} 次:Agent 生成解析代码 ---")
code = agent.generate_parser_code(
samples=samples,
required_keys=required_keys,
error_report=error_report,
feedback=feedback,
)
print(textwrap.indent(code, " | "))
# 写入候选文件(parsers/),再热加载
with open(target_path, "w", encoding="utf-8") as f:
f.write(code)
# 热加载生成的 parse 函数
try:
fn = LogParserEngine.load_parser_from_file(target_path)
except Exception as exc:
feedback = f"代码无法导入/执行:{type(exc).__name__}: {exc}"
print(f" ⚠️ 热加载失败:{feedback}")
continue
# (b) 自动测试:数据结构断言
print(" 🧪 自动测试(数据结构断言):")
test = run_tests(fn, samples, required_keys)
print(textwrap.indent(test["report"], " "))
if test["passed"]:
# (c) 通过 → 热更新注册进引擎,文件已持久化到 parsers/
engine.register(parser_name, fn)
print(f" ✅ 自动测试通过,已热更新注册解析器 '{parser_name}' 并持久化到 parsers/{parser_name}.py")
return True
feedback = "自动测试未通过,失败详情如下:\n" + test["report"]
print(" ↻ 测试未通过,把失败报告反馈给 Agent 重试。")
# 全部尝试失败:删除无效文件
if os.path.exists(target_path):
os.remove(target_path)
print(f"{MAX_ATTEMPTS} 次尝试后仍未通过,放弃该格式。")
return False
# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
def main(args: argparse.Namespace) -> None:
hr("自适应日志解析系统 —— 自愈闭环演示(实验 5-7)")
print("初始系统只内置一个基础解析器:JSON 行解析器。")
if args.quick:
print("(--quick 快速模式:仅演示 1 种新格式,省一次 Agent/API 调用)")
if args.offline:
print("(--offline 离线模式:用预置解析器代替 OpenAI,无需 API Key,机制完全一致)")
os.makedirs(PARSERS_DIR, exist_ok=True) # 确保持久化目录存在(新克隆时可能只有 .gitkeep)
engine = LogParserEngine()
engine.register("builtin_json", builtin_json_parser)
print(f"当前已注册解析器:{engine.parser_names}")
# model=None 时回落到 MODEL 环境变量/默认 gpt-5.6-luna;离线模式不触碰 API
agent = OfflineCodeGenAgent(args.model) if args.offline else CodeGenAgent(model=args.model)
print(f"代码生成 Agent 使用模型:{agent.model}")
# 步骤 0:基础 JSON 格式,系统本来就能解析
hr("步骤 0:解析基础 JSON 日志(系统原生支持)")
try_parse_all(engine, JSON_LOGS)
# 步骤 1:自定义竖线分隔格式(Agent 没见过)
hr("步骤 1:遇到新格式 A —— 自定义竖线分隔格式")
print("原始日志样本:")
for l in PIPE_LOGS:
print(f" {l}")
print("\n(a) 先让系统解析,预期【失败】:")
try_parse_all(engine, PIPE_LOGS)
print("\n触发自愈闭环:")
ok1 = self_heal(engine, agent, "pipe_parser", PIPE_LOGS, PIPE_REQUIRED)
if ok1:
print("\n(c) 热更新后重新解析同样的日志,预期【成功】:")
try_parse_all(engine, PIPE_LOGS)
# 步骤 2:嵌套括号格式(Agent 也没见过)—— 快速模式下跳过,省一次 API 调用
ok2 = None
if args.quick:
hr("步骤 2:(--quick 模式已跳过新格式 B 的演示)")
else:
hr("步骤 2:遇到新格式 B —— 嵌套括号格式")
print("原始日志样本:")
for l in BRACKET_LOGS:
print(f" {l}")
print("\n(a) 先让系统解析,预期【失败】:")
try_parse_all(engine, BRACKET_LOGS)
print("\n触发自愈闭环:")
ok2 = self_heal(engine, agent, "bracket_parser", BRACKET_LOGS, BRACKET_REQUIRED)
if ok2:
print("\n(c) 热更新后重新解析同样的日志,预期【成功】:")
try_parse_all(engine, BRACKET_LOGS)
# 步骤 3:验证持久化复用 —— 新引擎直接加载 parsers/,无需再问 Agent
hr("步骤 3:验证持久化复用(重启系统,直接加载已学会的解析器)")
engine2 = LogParserEngine()
engine2.register("builtin_json", builtin_json_parser)
loaded = engine2.load_persisted(PARSERS_DIR)
print(f"新引擎从 parsers/ 热加载了:{loaded}")
if args.log_file:
print(f"用学到的解析系统解析外部日志文件(不再调用 Agent):{args.log_file}")
mixed = read_log_file(args.log_file)
else:
print("直接解析之前的新格式(不再调用 Agent):")
mixed = [JSON_LOGS[0], PIPE_LOGS[0]]
if not args.quick:
mixed.append(BRACKET_LOGS[0]) # 快速模式没生成 bracket_parser,混合样本里也不放它
all_ok, records = try_parse_all(engine2, mixed)
if args.output:
write_output(args.output, records)
print(f"已将 {len(records)} 条结构化解析结果写入(JSONL):{args.output}")
hr("演示结束")
print(f"新格式 A(竖线分隔)自愈结果:{'成功' if ok1 else '失败'}")
if ok2 is None:
print("新格式 B(嵌套括号):--quick 模式已跳过")
else:
print(f"新格式 B(嵌套括号)自愈结果:{'成功' if ok2 else '失败'}")
print(f"持久化复用(混合格式全部解析):{'成功' if all_ok else '失败'}")
print(f"已学会并持久化的解析器目录:{PARSERS_DIR}")
def build_arg_parser() -> argparse.ArgumentParser:
"""构造命令行参数解析器(提供 --help / --quick / --model)。"""
parser = argparse.ArgumentParser(
description="自适应日志解析系统:自愈闭环演示(检测失败 → Agent 生成解析代码 → "
"自动测试 → 热加载注册 → 持久化复用)。默认走 OpenAI,需 OPENAI_API_KEY"
"加 --offline 用预置解析器演示同一套机制,无需 API Key。",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--offline",
action="store_true",
help="离线模式:用预置(canned)解析器代码代替调用 OpenAI,无需 API Key"
"确定性地演示“失败检测→生成→测试→热重载→持久化”整条机制。",
)
parser.add_argument(
"--quick",
action="store_true",
help="快速模式:只演示 1 种新格式(竖线分隔),跳过嵌套括号格式,省一次 Agent/API 调用。",
)
parser.add_argument(
"--model",
default=None,
help="覆盖代码生成使用的模型;默认读取环境变量 MODEL,再回落到 gpt-5.6-luna。"
"(--offline 下此项仅作展示,不影响预置解析器。)",
)
parser.add_argument(
"--log-file",
default=None,
metavar="PATH",
help="外部日志文件路径(每行一条日志)。给定后,步骤 3 改用学到的解析系统解析"
"该文件,替代内置混合样本;用于验证学到的解析器可复用到真实日志流。",
)
parser.add_argument(
"--output",
default=None,
metavar="PATH",
help="把步骤 3 解析出的结构化结果以 JSONL(每行一条 JSON)写入该文件。",
)
return parser
if __name__ == "__main__":
main(build_arg_parser().parse_args())
+115
View File
@@ -0,0 +1,115 @@
"""
engine.py —— 自适应日志解析引擎(自愈闭环的“运行时”)
设计要点:
- 引擎维护一个**解析器注册表**(有序列表)。每来一行日志,依次尝试每个解析器,
谁能解析(返回非空 dict)就用谁的结果;全部失败则抛出 ParseError —— 这就是
“前端检测到无法解析的新格式”的信号,触发后续的自愈流程。
- 每个解析器就是一个纯函数 `parse(line: str) -> dict | None`
* 能解析 → 返回结构化字段(dict)
* 不认识这行 → 返回 None(把机会让给别的解析器,避免“抢答”)
- 生成的解析器可以持久化成 parsers/*.py 模块,下次启动直接热加载复用,无需再问 Agent。
注意:这里对“可视化”做了降级——书中用虚拟浏览器 + Vision LLM 验证渲染效果,
本项目改为对解析函数做**数据结构断言**(见 tester.py),核心自愈闭环是真实实现的。
"""
from __future__ import annotations
import importlib.util
import json
import os
from typing import Callable, Dict, List, Optional, Tuple
# 一个解析器 = (名字, 解析函数)
ParserFn = Callable[[str], Optional[Dict]]
class ParseError(Exception):
"""所有已注册解析器都无法解析该行时抛出,携带原始样本供 Agent 分析。"""
def __init__(self, line: str):
self.line = line
super().__init__(f"没有任何已注册解析器能解析该行:{line!r}")
def builtin_json_parser(line: str) -> Optional[Dict]:
"""内置的基础解析器:只认标准 JSON 行(JSON Lines)。
形如:{"timestamp": "...", "level": "INFO", "message": "..."}
不是 JSON,或不含基本字段,则返回 None(不是我的格式)。
"""
line = line.strip()
if not (line.startswith("{") and line.endswith("}")):
return None
try:
obj = json.loads(line)
except json.JSONDecodeError:
return None
if not isinstance(obj, dict):
return None
# 至少要有一个基本字段,才认为是“合法的 JSON 日志”
if not any(k in obj for k in ("timestamp", "level", "message")):
return None
return obj
class LogParserEngine:
"""日志解析系统:持有一组解析器,并支持热加载注册新解析器。"""
def __init__(self) -> None:
self._parsers: List[Tuple[str, ParserFn]] = []
# -- 注册 / 查询 --------------------------------------------------------
def register(self, name: str, fn: ParserFn) -> None:
"""注册(或替换同名)解析器。新解析器优先级更高,放到列表末尾后再尝试。"""
# 若同名已存在则先移除,实现“热更新替换”
self._parsers = [(n, f) for (n, f) in self._parsers if n != name]
self._parsers.append((name, fn))
@property
def parser_names(self) -> List[str]:
return [n for n, _ in self._parsers]
# -- 解析 ---------------------------------------------------------------
def parse_line(self, line: str) -> Dict:
"""尝试用每个解析器解析一行;成功则在结果里标注 _parser。全部失败抛 ParseError。"""
for name, fn in self._parsers:
try:
result = fn(line)
except Exception:
# 某个解析器对这行报错,不代表别的不行,继续尝试
continue
if result:
return {"_parser": name, **result}
raise ParseError(line)
# -- 热加载:从 .py 文件加载 parse 函数 ----------------------------------
@staticmethod
def load_parser_from_file(path: str) -> ParserFn:
"""把一个 parsers/*.py 模块动态导入,取出其中的 parse 函数。"""
module_name = "genparser_" + os.path.splitext(os.path.basename(path))[0]
spec = importlib.util.spec_from_file_location(module_name, path)
if spec is None or spec.loader is None:
raise ImportError(f"无法加载模块:{path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # 执行模块,定义 parse
fn = getattr(module, "parse", None)
if not callable(fn):
raise ImportError(f"{path} 中未找到可调用的 parse(line) 函数")
return fn
def load_persisted(self, parsers_dir: str) -> List[str]:
"""启动时把 parsers/ 目录下已持久化的解析器全部热加载注册(复用历史成果)。"""
loaded: List[str] = []
if not os.path.isdir(parsers_dir):
return loaded
for fname in sorted(os.listdir(parsers_dir)):
if not fname.endswith(".py") or fname.startswith("_"):
continue
path = os.path.join(parsers_dir, fname)
fn = self.load_parser_from_file(path)
name = os.path.splitext(fname)[0]
self.register(name, fn)
loaded.append(name)
return loaded
+13
View File
@@ -0,0 +1,13 @@
# 必填其一:OpenAI API Key(本实验读取此项,模型默认 gpt-5.6-luna
OPENAI_API_KEY=your_openai_api_key_here
# 通用兜底:未配置 OPENAI_API_KEY 时自动改走 OpenRouter
# 默认模型 gpt-5.6-lunagpt-5.x)直连 OpenAI 需组织实名认证,
# 故设置了本 key 时会优先走 OpenRouterroute openai/gpt-5.6-luna)。
# OPENROUTER_API_KEY=your_openrouter_api_key_here
# 可选:切换到兼容 OpenAI 协议的服务端点
# OPENAI_BASE_URL=https://api.openai.com/v1
# 可选:指定模型(默认 gpt-5.6-luna
# MODEL=gpt-5.6-luna
@@ -0,0 +1,4 @@
openai>=1.30.0
python-dotenv>=1.0
playwright>=1.45.0
Pillow>=10.0.0
+59
View File
@@ -0,0 +1,59 @@
"""
tester.py —— 自动测试(对生成的解析器做数据结构断言)
书中原方案:把生成的可视化代码放进虚拟浏览器渲染,再用 Vision LLM 检查图像。
本机没有 playwright/浏览器,因此**降级**为对解析函数做单元测试:
用一批样本日志喂给生成的 parse 函数,断言它能解析出预期的结构化字段。
这保证了“生成的代码确实能正确解析新格式”,是自愈闭环里真正的质量闸门。
"""
from __future__ import annotations
from typing import Callable, Dict, List, Optional
ParserFn = Callable[[str], Optional[Dict]]
def run_tests(
parse_fn: ParserFn,
samples: List[str],
required_keys: List[str],
) -> Dict:
"""对 parse_fn 跑一组断言,返回 {passed: bool, report: str, results: [...]}。
通过条件(对每一条样本都要满足):
1. parse_fn(line) 不抛异常;
2. 返回值是非空 dict;
3. required_keys 中的每个字段都存在,且值不为空(非 None、非空字符串)。
"""
lines: List[str] = []
results: List[Optional[Dict]] = []
all_passed = True
for i, sample in enumerate(samples, 1):
try:
out = parse_fn(sample)
except Exception as exc: # 生成的代码在样本上直接崩了
all_passed = False
results.append(None)
lines.append(f"[样本{i}] 解析抛出异常:{type(exc).__name__}: {exc}")
continue
if not isinstance(out, dict) or not out:
all_passed = False
results.append(out)
lines.append(f"[样本{i}] 未返回非空 dict,实际返回:{out!r}")
continue
missing = [k for k in required_keys if k not in out or out[k] in (None, "")]
if missing:
all_passed = False
lines.append(
f"[样本{i}] 缺少/为空的必需字段:{missing};实际解析出:{out}"
)
else:
lines.append(f"[样本{i}] 通过,解析出字段:{sorted(out.keys())}")
results.append(out)
report = "\n".join(lines)
return {"passed": all_passed, "report": report, "results": results}
@@ -0,0 +1,276 @@
{
"schema_version": "1.0",
"experiment": "5-7",
"run_id": "20260729T212342Z-5_7-live",
"started_at_utc": "2026-07-29T21:23:42.501795+00:00",
"completed_at_utc": "2026-07-29T21:25:15.353838+00:00",
"provider": "ark",
"endpoint": "https://ark.cn-beijing.volces.com/api/v3",
"model": "doubao-seed-1-6-250615",
"source": {
"manuscript": "book/chapter5.md#实验-5-7",
"campaign_sha256": "cafda97001032d4720a5cdd3e920e4c025102486e6158b017be9352b3935664a"
},
"formats": [
{
"name": "live_pipe_parser",
"raw_log": "live-format-1.log",
"raw_log_sha256": "0e6df83a5a373e7adb217708ccfa45df197f2ee84edd638b9db981cba85d5b80",
"samples": 3,
"initial_failures": 3,
"required_keys": [
"timestamp",
"level",
"module",
"step",
"message"
],
"parser": "parsers/live_pipe_parser.py",
"parser_sha256": "8c03fe6676bfe61f78900065f1a4caae5f2ee16ebe563cb57ed2c68bd4e8d4d5",
"test": {
"passed": true,
"report": "[样本1] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']\n[样本2] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']\n[样本3] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']",
"results": [
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "INFO",
"module": "checkout.worker",
"step": "1",
"message": "accepted real request req-81"
},
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "WARNING",
"module": "checkout.worker",
"step": "2",
"message": "retrying payment authorization req-81"
},
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "ERROR",
"module": "checkout.worker",
"step": "3",
"message": "authorization exhausted req-81"
}
]
},
"parsed_after_hot_update": [
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "INFO",
"module": "checkout.worker",
"step": "1",
"message": "accepted real request req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "WARNING",
"module": "checkout.worker",
"step": "2",
"message": "retrying payment authorization req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "ERROR",
"module": "checkout.worker",
"step": "3",
"message": "authorization exhausted req-81"
}
]
},
{
"name": "live_bracket_parser",
"raw_log": "live-format-2.log",
"raw_log_sha256": "b52aa925d76f1c10017d42f16efd4daacf5ed73c9184067bdee9a9e50a7878b6",
"samples": 3,
"initial_failures": 3,
"required_keys": [
"timestamp",
"level",
"tool",
"latency_ms",
"status",
"message"
],
"parser": "parsers/live_bracket_parser.py",
"parser_sha256": "4f1780d6d2e9886eafc1f5a0c3cce09c9873cb4113f9de5f34482d1efc35e07c",
"test": {
"passed": true,
"report": "[样本1] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']\n[样本2] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']\n[样本3] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']",
"results": [
{
"timestamp": "2026-07-29T21:23:42.606+00:00",
"level": "INFO",
"tool": "inventory_lookup",
"latency_ms": 34,
"status": "ok",
"message": "stock check completed"
},
{
"timestamp": "2026-07-29T21:23:42.609+00:00",
"level": "WARNING",
"tool": "payment_api",
"latency_ms": 181,
"status": "retry",
"message": "upstream requested retry"
},
{
"timestamp": "2026-07-29T21:23:42.612+00:00",
"level": "ERROR",
"tool": "payment_api",
"latency_ms": 412,
"status": "timeout",
"message": "deadline exceeded"
}
]
},
"parsed_after_hot_update": [
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.606+00:00",
"level": "INFO",
"tool": "inventory_lookup",
"latency_ms": 34,
"status": "ok",
"message": "stock check completed"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.609+00:00",
"level": "WARNING",
"tool": "payment_api",
"latency_ms": 181,
"status": "retry",
"message": "upstream requested retry"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.612+00:00",
"level": "ERROR",
"tool": "payment_api",
"latency_ms": 412,
"status": "timeout",
"message": "deadline exceeded"
}
]
}
],
"browser": {
"browser": "Chromium",
"version": "139.0.7258.5",
"rows": 6,
"columns": [
"_parser",
"latency_ms",
"level",
"message",
"module",
"status",
"step",
"timestamp",
"tool"
]
},
"vision_judgment": {
"pass": true,
"readable": true,
"has_multiple_parsers": true,
"observed_columns": [
"_parser",
"latency_ms",
"level",
"message",
"module",
"status",
"step",
"timestamp",
"tool"
],
"reason": "The table is readable with clear columns, contains 6 parsed rows, includes multiple parser identifiers (_parser: live_pipe_parser, live_bracket_parser), and structured fields (latency_ms, level, message, etc.)"
},
"usage": {
"calls": 3,
"prompt_tokens": 3696,
"completion_tokens": 4069,
"total_tokens": 7765,
"latency_s": 92.052
},
"artifacts": {
"evidence.json": {
"path": "evidence.json",
"sha256": "5bbdfd7a34e133c51ce96f6cb3139cc90a50462a9f1ddf3963850c7136851d75",
"bytes": 7273
},
"live-format-1.log": {
"path": "live-format-1.log",
"sha256": "0e6df83a5a373e7adb217708ccfa45df197f2ee84edd638b9db981cba85d5b80",
"bytes": 249
},
"live-format-2.log": {
"path": "live-format-2.log",
"sha256": "b52aa925d76f1c10017d42f16efd4daacf5ed73c9184067bdee9a9e50a7878b6",
"bytes": 345
},
"parsers/__pycache__/live_bracket_parser.cpython-311.pyc": {
"path": "parsers/__pycache__/live_bracket_parser.cpython-311.pyc",
"sha256": "096ea41a2b5b1a6cf683d18d05579810f499dbea75366e798c98f882eeab5a8a",
"bytes": 1232
},
"parsers/__pycache__/live_pipe_parser.cpython-311.pyc": {
"path": "parsers/__pycache__/live_pipe_parser.cpython-311.pyc",
"sha256": "fa59fd91273396f06f51866d8cc206cbad0b73a146a0de78f1d5752b6cc4be86",
"bytes": 1200
},
"parsers/live_bracket_parser.py": {
"path": "parsers/live_bracket_parser.py",
"sha256": "4f1780d6d2e9886eafc1f5a0c3cce09c9873cb4113f9de5f34482d1efc35e07c",
"bytes": 869
},
"parsers/live_pipe_parser.py": {
"path": "parsers/live_pipe_parser.py",
"sha256": "8c03fe6676bfe61f78900065f1a4caae5f2ee16ebe563cb57ed2c68bd4e8d4d5",
"bytes": 733
},
"producer-1.py": {
"path": "producer-1.py",
"sha256": "dda2a6a04f198f038d74f78375f8d03eb9e48aeadd672c0b39c13fb2900c69f2",
"bytes": 547
},
"producer-2.py": {
"path": "producer-2.py",
"sha256": "cb6999b88a3bd22a9187cc37efdeec0e02d5a0f70390f21620cab2218b53bb71",
"bytes": 638
},
"receipts.json": {
"path": "receipts.json",
"sha256": "31a3da57aa08f7ec90b591567721fb4a09698365fc7c7710cd9c958e2c647a45",
"bytes": 8546
},
"visualization.html": {
"path": "visualization.html",
"sha256": "3f236a29650312ca11579cd1918e98affc1082c2dac3aeb8dc90bcdae0aaf185",
"bytes": 1693
},
"visualization.png": {
"path": "visualization.png",
"sha256": "d8dfad31b33fc0fa5dcf60b99041e61ef531627e5bde64e92f1cb345ab2cec52",
"bytes": 96289
}
},
"acceptance_gates": {
"raw_logs_emitted_by_real_runtime_processes": true,
"initial_system_detected_every_new_format_failure": true,
"real_model_generated_both_parser_modules": true,
"generated_code_passed_automatic_tests": true,
"hot_update_parsed_every_failed_sample": true,
"persisted_parsers_loaded_after_fresh_engine_restart": true,
"fresh_engine_parsed_entire_mixed_stream": true,
"real_chromium_rendered_visualization": true,
"real_vision_model_approved_rendered_pixels": true,
"raw_provider_receipts_complete": true
},
"official_complete": true
}
@@ -0,0 +1,239 @@
{
"formats": [
{
"name": "live_pipe_parser",
"raw_log": "live-format-1.log",
"raw_log_sha256": "0e6df83a5a373e7adb217708ccfa45df197f2ee84edd638b9db981cba85d5b80",
"samples": 3,
"initial_failures": 3,
"required_keys": [
"timestamp",
"level",
"module",
"step",
"message"
],
"parser": "parsers/live_pipe_parser.py",
"parser_sha256": "8c03fe6676bfe61f78900065f1a4caae5f2ee16ebe563cb57ed2c68bd4e8d4d5",
"test": {
"passed": true,
"report": "[样本1] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']\n[样本2] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']\n[样本3] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']",
"results": [
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "INFO",
"module": "checkout.worker",
"step": "1",
"message": "accepted real request req-81"
},
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "WARNING",
"module": "checkout.worker",
"step": "2",
"message": "retrying payment authorization req-81"
},
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "ERROR",
"module": "checkout.worker",
"step": "3",
"message": "authorization exhausted req-81"
}
]
},
"parsed_after_hot_update": [
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "INFO",
"module": "checkout.worker",
"step": "1",
"message": "accepted real request req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "WARNING",
"module": "checkout.worker",
"step": "2",
"message": "retrying payment authorization req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "ERROR",
"module": "checkout.worker",
"step": "3",
"message": "authorization exhausted req-81"
}
]
},
{
"name": "live_bracket_parser",
"raw_log": "live-format-2.log",
"raw_log_sha256": "b52aa925d76f1c10017d42f16efd4daacf5ed73c9184067bdee9a9e50a7878b6",
"samples": 3,
"initial_failures": 3,
"required_keys": [
"timestamp",
"level",
"tool",
"latency_ms",
"status",
"message"
],
"parser": "parsers/live_bracket_parser.py",
"parser_sha256": "4f1780d6d2e9886eafc1f5a0c3cce09c9873cb4113f9de5f34482d1efc35e07c",
"test": {
"passed": true,
"report": "[样本1] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']\n[样本2] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']\n[样本3] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']",
"results": [
{
"timestamp": "2026-07-29T21:23:42.606+00:00",
"level": "INFO",
"tool": "inventory_lookup",
"latency_ms": 34,
"status": "ok",
"message": "stock check completed"
},
{
"timestamp": "2026-07-29T21:23:42.609+00:00",
"level": "WARNING",
"tool": "payment_api",
"latency_ms": 181,
"status": "retry",
"message": "upstream requested retry"
},
{
"timestamp": "2026-07-29T21:23:42.612+00:00",
"level": "ERROR",
"tool": "payment_api",
"latency_ms": 412,
"status": "timeout",
"message": "deadline exceeded"
}
]
},
"parsed_after_hot_update": [
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.606+00:00",
"level": "INFO",
"tool": "inventory_lookup",
"latency_ms": 34,
"status": "ok",
"message": "stock check completed"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.609+00:00",
"level": "WARNING",
"tool": "payment_api",
"latency_ms": 181,
"status": "retry",
"message": "upstream requested retry"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.612+00:00",
"level": "ERROR",
"tool": "payment_api",
"latency_ms": 412,
"status": "timeout",
"message": "deadline exceeded"
}
]
}
],
"loaded_after_restart": [
"live_bracket_parser",
"live_pipe_parser"
],
"rows_after_restart": [
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "INFO",
"module": "checkout.worker",
"step": "1",
"message": "accepted real request req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "WARNING",
"module": "checkout.worker",
"step": "2",
"message": "retrying payment authorization req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "ERROR",
"module": "checkout.worker",
"step": "3",
"message": "authorization exhausted req-81"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.606+00:00",
"level": "INFO",
"tool": "inventory_lookup",
"latency_ms": 34,
"status": "ok",
"message": "stock check completed"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.609+00:00",
"level": "WARNING",
"tool": "payment_api",
"latency_ms": 181,
"status": "retry",
"message": "upstream requested retry"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.612+00:00",
"level": "ERROR",
"tool": "payment_api",
"latency_ms": 412,
"status": "timeout",
"message": "deadline exceeded"
}
],
"browser": {
"browser": "Chromium",
"version": "139.0.7258.5",
"rows": 6,
"columns": [
"_parser",
"latency_ms",
"level",
"message",
"module",
"status",
"step",
"timestamp",
"tool"
]
},
"vision_judgment": {
"pass": true,
"readable": true,
"has_multiple_parsers": true,
"observed_columns": [
"_parser",
"latency_ms",
"level",
"message",
"module",
"status",
"step",
"timestamp",
"tool"
],
"reason": "The table is readable with clear columns, contains 6 parsed rows, includes multiple parser identifiers (_parser: live_pipe_parser, live_bracket_parser), and structured fields (latency_ms, level, message, etc.)"
}
}
@@ -0,0 +1,276 @@
{
"schema_version": "1.0",
"experiment": "5-7",
"run_id": "20260729T212342Z-5_7-live",
"started_at_utc": "2026-07-29T21:23:42.501795+00:00",
"completed_at_utc": "2026-07-29T21:25:15.353838+00:00",
"provider": "ark",
"endpoint": "https://ark.cn-beijing.volces.com/api/v3",
"model": "doubao-seed-1-6-250615",
"source": {
"manuscript": "book/chapter5.md#实验-5-7",
"campaign_sha256": "cafda97001032d4720a5cdd3e920e4c025102486e6158b017be9352b3935664a"
},
"formats": [
{
"name": "live_pipe_parser",
"raw_log": "live-format-1.log",
"raw_log_sha256": "0e6df83a5a373e7adb217708ccfa45df197f2ee84edd638b9db981cba85d5b80",
"samples": 3,
"initial_failures": 3,
"required_keys": [
"timestamp",
"level",
"module",
"step",
"message"
],
"parser": "parsers/live_pipe_parser.py",
"parser_sha256": "8c03fe6676bfe61f78900065f1a4caae5f2ee16ebe563cb57ed2c68bd4e8d4d5",
"test": {
"passed": true,
"report": "[样本1] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']\n[样本2] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']\n[样本3] 通过,解析出字段:['level', 'message', 'module', 'step', 'timestamp']",
"results": [
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "INFO",
"module": "checkout.worker",
"step": "1",
"message": "accepted real request req-81"
},
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "WARNING",
"module": "checkout.worker",
"step": "2",
"message": "retrying payment authorization req-81"
},
{
"timestamp": "2026-07-30T05:23:42Z",
"level": "ERROR",
"module": "checkout.worker",
"step": "3",
"message": "authorization exhausted req-81"
}
]
},
"parsed_after_hot_update": [
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "INFO",
"module": "checkout.worker",
"step": "1",
"message": "accepted real request req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "WARNING",
"module": "checkout.worker",
"step": "2",
"message": "retrying payment authorization req-81"
},
{
"_parser": "live_pipe_parser",
"timestamp": "2026-07-30T05:23:42Z",
"level": "ERROR",
"module": "checkout.worker",
"step": "3",
"message": "authorization exhausted req-81"
}
]
},
{
"name": "live_bracket_parser",
"raw_log": "live-format-2.log",
"raw_log_sha256": "b52aa925d76f1c10017d42f16efd4daacf5ed73c9184067bdee9a9e50a7878b6",
"samples": 3,
"initial_failures": 3,
"required_keys": [
"timestamp",
"level",
"tool",
"latency_ms",
"status",
"message"
],
"parser": "parsers/live_bracket_parser.py",
"parser_sha256": "4f1780d6d2e9886eafc1f5a0c3cce09c9873cb4113f9de5f34482d1efc35e07c",
"test": {
"passed": true,
"report": "[样本1] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']\n[样本2] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']\n[样本3] 通过,解析出字段:['latency_ms', 'level', 'message', 'status', 'timestamp', 'tool']",
"results": [
{
"timestamp": "2026-07-29T21:23:42.606+00:00",
"level": "INFO",
"tool": "inventory_lookup",
"latency_ms": 34,
"status": "ok",
"message": "stock check completed"
},
{
"timestamp": "2026-07-29T21:23:42.609+00:00",
"level": "WARNING",
"tool": "payment_api",
"latency_ms": 181,
"status": "retry",
"message": "upstream requested retry"
},
{
"timestamp": "2026-07-29T21:23:42.612+00:00",
"level": "ERROR",
"tool": "payment_api",
"latency_ms": 412,
"status": "timeout",
"message": "deadline exceeded"
}
]
},
"parsed_after_hot_update": [
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.606+00:00",
"level": "INFO",
"tool": "inventory_lookup",
"latency_ms": 34,
"status": "ok",
"message": "stock check completed"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.609+00:00",
"level": "WARNING",
"tool": "payment_api",
"latency_ms": 181,
"status": "retry",
"message": "upstream requested retry"
},
{
"_parser": "live_bracket_parser",
"timestamp": "2026-07-29T21:23:42.612+00:00",
"level": "ERROR",
"tool": "payment_api",
"latency_ms": 412,
"status": "timeout",
"message": "deadline exceeded"
}
]
}
],
"browser": {
"browser": "Chromium",
"version": "139.0.7258.5",
"rows": 6,
"columns": [
"_parser",
"latency_ms",
"level",
"message",
"module",
"status",
"step",
"timestamp",
"tool"
]
},
"vision_judgment": {
"pass": true,
"readable": true,
"has_multiple_parsers": true,
"observed_columns": [
"_parser",
"latency_ms",
"level",
"message",
"module",
"status",
"step",
"timestamp",
"tool"
],
"reason": "The table is readable with clear columns, contains 6 parsed rows, includes multiple parser identifiers (_parser: live_pipe_parser, live_bracket_parser), and structured fields (latency_ms, level, message, etc.)"
},
"usage": {
"calls": 3,
"prompt_tokens": 3696,
"completion_tokens": 4069,
"total_tokens": 7765,
"latency_s": 92.052
},
"artifacts": {
"evidence.json": {
"path": "evidence.json",
"sha256": "5bbdfd7a34e133c51ce96f6cb3139cc90a50462a9f1ddf3963850c7136851d75",
"bytes": 7273
},
"live-format-1.log": {
"path": "live-format-1.log",
"sha256": "0e6df83a5a373e7adb217708ccfa45df197f2ee84edd638b9db981cba85d5b80",
"bytes": 249
},
"live-format-2.log": {
"path": "live-format-2.log",
"sha256": "b52aa925d76f1c10017d42f16efd4daacf5ed73c9184067bdee9a9e50a7878b6",
"bytes": 345
},
"parsers/__pycache__/live_bracket_parser.cpython-311.pyc": {
"path": "parsers/__pycache__/live_bracket_parser.cpython-311.pyc",
"sha256": "096ea41a2b5b1a6cf683d18d05579810f499dbea75366e798c98f882eeab5a8a",
"bytes": 1232
},
"parsers/__pycache__/live_pipe_parser.cpython-311.pyc": {
"path": "parsers/__pycache__/live_pipe_parser.cpython-311.pyc",
"sha256": "fa59fd91273396f06f51866d8cc206cbad0b73a146a0de78f1d5752b6cc4be86",
"bytes": 1200
},
"parsers/live_bracket_parser.py": {
"path": "parsers/live_bracket_parser.py",
"sha256": "4f1780d6d2e9886eafc1f5a0c3cce09c9873cb4113f9de5f34482d1efc35e07c",
"bytes": 869
},
"parsers/live_pipe_parser.py": {
"path": "parsers/live_pipe_parser.py",
"sha256": "8c03fe6676bfe61f78900065f1a4caae5f2ee16ebe563cb57ed2c68bd4e8d4d5",
"bytes": 733
},
"producer-1.py": {
"path": "producer-1.py",
"sha256": "dda2a6a04f198f038d74f78375f8d03eb9e48aeadd672c0b39c13fb2900c69f2",
"bytes": 547
},
"producer-2.py": {
"path": "producer-2.py",
"sha256": "cb6999b88a3bd22a9187cc37efdeec0e02d5a0f70390f21620cab2218b53bb71",
"bytes": 638
},
"receipts.json": {
"path": "receipts.json",
"sha256": "31a3da57aa08f7ec90b591567721fb4a09698365fc7c7710cd9c958e2c647a45",
"bytes": 8546
},
"visualization.html": {
"path": "visualization.html",
"sha256": "3f236a29650312ca11579cd1918e98affc1082c2dac3aeb8dc90bcdae0aaf185",
"bytes": 1693
},
"visualization.png": {
"path": "visualization.png",
"sha256": "d8dfad31b33fc0fa5dcf60b99041e61ef531627e5bde64e92f1cb345ab2cec52",
"bytes": 96289
}
},
"acceptance_gates": {
"raw_logs_emitted_by_real_runtime_processes": true,
"initial_system_detected_every_new_format_failure": true,
"real_model_generated_both_parser_modules": true,
"generated_code_passed_automatic_tests": true,
"hot_update_parsed_every_failed_sample": true,
"persisted_parsers_loaded_after_fresh_engine_restart": true,
"fresh_engine_parsed_entire_mixed_stream": true,
"real_chromium_rendered_visualization": true,
"real_vision_model_approved_rendered_pixels": true,
"raw_provider_receipts_complete": true
},
"official_complete": true
}
@@ -0,0 +1,27 @@
import re
def parse(line: str) -> dict | None:
# 定义日志格式的正则表达式模式
pattern = r'^\[(?P<timestamp>[^\]]+)\]\s*\((?P<level>[^)]+)\)\s*<tool=(?P<tool>[^>]+)>\s*\{latency_ms=(?P<latency_ms>\d+)\s+status=(?P<status>\w+)\}\s*::\s*(?P<message>.*)$'
# 尝试匹配日志行
match = re.match(pattern, line.strip())
if not match:
return None
# 提取匹配的组
groups = match.groupdict()
# 检查所有必需字段是否存在且不为空
required_keys = ['timestamp', 'level', 'tool', 'latency_ms', 'status', 'message']
for key in required_keys:
if key not in groups or not groups[key]:
return None
# 转换latency_ms为整数
try:
groups['latency_ms'] = int(groups['latency_ms'])
except ValueError:
return None
return groups
@@ -0,0 +1,28 @@
import re
def parse(line: str) -> dict | None:
# 定义匹配日志格式的正则表达式
pattern = r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\|([A-Z]+)\|([\w.]+)\|step=(\d+)\|(.*)$'
match = re.match(pattern, line.strip())
if not match:
return None
# 提取各个字段
timestamp = match.group(1)
level = match.group(2)
module = match.group(3)
step = match.group(4)
message = match.group(5)
# 确保所有必需字段都不为空
if not all([timestamp, level, module, step, message]):
return None
return {
'timestamp': timestamp,
'level': level,
'module': module,
'step': step,
'message': message
}
@@ -0,0 +1,7 @@
import logging, sys
formatter=logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|step=%(step)s|%(message)s', datefmt='%Y-%m-%dT%H:%M:%SZ')
handler=logging.StreamHandler(sys.stdout); handler.setFormatter(formatter)
logger=logging.getLogger('checkout.worker'); logger.handlers=[handler]; logger.setLevel(logging.INFO); logger.propagate=False
logger.info('accepted real request req-81', extra={'step': 1})
logger.warning('retrying payment authorization req-81', extra={'step': 2})
logger.error('authorization exhausted req-81', extra={'step': 3})
@@ -0,0 +1,7 @@
import datetime, time
events=[('inventory_lookup',34,'ok','stock check completed'),('payment_api',181,'retry','upstream requested retry'),('payment_api',412,'timeout','deadline exceeded')]
for tool,latency,status,message in events:
started=time.perf_counter(); time.sleep(0.003); observed=max(latency,int((time.perf_counter()-started)*1000))
stamp=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='milliseconds')
level='ERROR' if status=='timeout' else ('WARNING' if status=='retry' else 'INFO')
print(f'[{stamp}] ({level}) <tool={tool}> {{latency_ms={observed} status={status}}} :: {message}', flush=True)
@@ -0,0 +1,103 @@
[
{
"purpose": "generate-live_pipe_parser-attempt-1",
"called_at_utc": "2026-07-29T21:24:10.369834+00:00",
"latency_s": 27.68,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "你是一个\"日志解析器代码生成器\"。用户会给你一批**同一种未知格式**的日志样本,\n以及现有系统解析失败的报错。你的任务:编写一个 Python 函数,把这种格式的每一行解析成结构化字段。\n\n严格要求:\n1. 只输出一个 Python 代码块(```python ... ```),不要任何解释文字。\n2. 代码块里必须定义一个函数:def parse(line: str) -> dict | None\n - 输入是一行日志(字符串)。\n - 如果这行符合你要解析的格式,返回一个 dict,键为字段名(英文小写下划线),值为解析出的内容。\n - 如果这行**不符合**这种格式,必须返回 None(不要抛异常,把机会让给其它解析器)。\n3. 只能使用 Python 标准库(re、json、datetime 等),不要 import 第三方库。\n4. 不要有任何 print、input、文件读写、网络访问等副作用。\n5. 必须解析出用户指定的**所有必需字段**(required_keys),字段值不能为空。\n6. 尽量健壮:用正则/分隔符解析,容忍字段顺序内的空格。\n"
},
{
"role": "user",
"content": "现有系统无法解析下面这种格式的日志,请生成解析函数。\n\n【失败样本(同一种新格式)】\n2026-07-30T05:23:42Z|INFO|checkout.worker|step=1|accepted real request req-81\n2026-07-30T05:23:42Z|WARNING|checkout.worker|step=2|retrying payment authorization req-81\n2026-07-30T05:23:42Z|ERROR|checkout.worker|step=3|authorization exhausted req-81\n\n【系统报错】\n没有任何已注册解析器能解析该行:'2026-07-30T05:23:42Z|INFO|checkout.worker|step=1|accepted real request req-81'\n\n【必需解析出的字段 required_keys】\n['timestamp', 'level', 'module', 'step', 'message']"
}
],
"temperature": 0
},
"response": {
"id": "0217853602230262b35cfdb86d4e4b324c5d7f5a75557a23d3680",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "```python\nimport re\n\ndef parse(line: str) -> dict | None:\n # 定义匹配日志格式的正则表达式\n pattern = r'^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)\\|([A-Z]+)\\|([\\w.]+)\\|step=(\\d+)\\|(.*)$'\n match = re.match(pattern, line.strip())\n \n if not match:\n return None\n \n # 提取各个字段\n timestamp = match.group(1)\n level = match.group(2)\n module = match.group(3)\n step = match.group(4)\n message = match.group(5)\n \n # 确保所有必需字段都不为空\n if not all([timestamp, level, module, step, message]):\n return None\n \n return {\n 'timestamp': timestamp,\n 'level': level,\n 'module': module,\n 'step': step,\n 'message': message\n }\n```"
},
"usage": {
"prompt_tokens": 588,
"completion_tokens": 1136,
"total_tokens": 1724,
"cached_prompt_tokens": 0
}
},
{
"purpose": "generate-live_bracket_parser-attempt-1",
"called_at_utc": "2026-07-29T21:25:00.326777+00:00",
"latency_s": 49.954,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "system",
"content": "你是一个\"日志解析器代码生成器\"。用户会给你一批**同一种未知格式**的日志样本,\n以及现有系统解析失败的报错。你的任务:编写一个 Python 函数,把这种格式的每一行解析成结构化字段。\n\n严格要求:\n1. 只输出一个 Python 代码块(```python ... ```),不要任何解释文字。\n2. 代码块里必须定义一个函数:def parse(line: str) -> dict | None\n - 输入是一行日志(字符串)。\n - 如果这行符合你要解析的格式,返回一个 dict,键为字段名(英文小写下划线),值为解析出的内容。\n - 如果这行**不符合**这种格式,必须返回 None(不要抛异常,把机会让给其它解析器)。\n3. 只能使用 Python 标准库(re、json、datetime 等),不要 import 第三方库。\n4. 不要有任何 print、input、文件读写、网络访问等副作用。\n5. 必须解析出用户指定的**所有必需字段**(required_keys),字段值不能为空。\n6. 尽量健壮:用正则/分隔符解析,容忍字段顺序内的空格。\n"
},
{
"role": "user",
"content": "现有系统无法解析下面这种格式的日志,请生成解析函数。\n\n【失败样本(同一种新格式)】\n[2026-07-29T21:23:42.606+00:00] (INFO) <tool=inventory_lookup> {latency_ms=34 status=ok} :: stock check completed\n[2026-07-29T21:23:42.609+00:00] (WARNING) <tool=payment_api> {latency_ms=181 status=retry} :: upstream requested retry\n[2026-07-29T21:23:42.612+00:00] (ERROR) <tool=payment_api> {latency_ms=412 status=timeout} :: deadline exceeded\n\n【系统报错】\n没有任何已注册解析器能解析该行:'[2026-07-29T21:23:42.606+00:00] (INFO) <tool=inventory_lookup> {latency_ms=34 status=ok} :: stock check completed'\n\n【必需解析出的字段 required_keys】\n['timestamp', 'level', 'tool', 'latency_ms', 'status', 'message']"
}
],
"temperature": 0
},
"response": {
"id": "0217853602505592b35cfdb86d4e4b324c5d7f5a75557a2d2f0d6",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "```python\nimport re\n\ndef parse(line: str) -> dict | None:\n # 定义日志格式的正则表达式模式\n pattern = r'^\\[(?P<timestamp>[^\\]]+)\\]\\s*\\((?P<level>[^)]+)\\)\\s*<tool=(?P<tool>[^>]+)>\\s*\\{latency_ms=(?P<latency_ms>\\d+)\\s+status=(?P<status>\\w+)\\}\\s*::\\s*(?P<message>.*)$'\n \n # 尝试匹配日志行\n match = re.match(pattern, line.strip())\n if not match:\n return None\n \n # 提取匹配的组\n groups = match.groupdict()\n \n # 检查所有必需字段是否存在且不为空\n required_keys = ['timestamp', 'level', 'tool', 'latency_ms', 'status', 'message']\n for key in required_keys:\n if key not in groups or not groups[key]:\n return None\n \n # 转换latency_ms为整数\n try:\n groups['latency_ms'] = int(groups['latency_ms'])\n except ValueError:\n return None\n \n return groups\n```"
},
"usage": {
"prompt_tokens": 661,
"completion_tokens": 2452,
"total_tokens": 3113,
"cached_prompt_tokens": 0
}
},
{
"purpose": "vision-review-rendered-parser-table",
"called_at_utc": "2026-07-29T21:25:15.349082+00:00",
"latency_s": 14.418,
"request": {
"model": "doubao-seed-1-6-250615",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Inspect this rendered adaptive-log table. Return strict JSON: {\"pass\": bool, \"readable\": bool, \"has_multiple_parsers\": bool, \"observed_columns\": [strings], \"reason\": string}. Pass only if the table is readable, contains multiple parsed rows, and visibly includes both parser identifiers and structured fields."
},
{
"type": "image_url",
"image_url": {
"sha256": "d8dfad31b33fc0fa5dcf60b99041e61ef531627e5bde64e92f1cb345ab2cec52",
"bytes": 96289
}
}
]
}
],
"temperature": 0
},
"response": {
"id": "0217853603011172b35cfdb86d4e4b324c5d7f5a75557a2e2712e",
"model": "doubao-seed-1-6-250615",
"finish_reason": "stop",
"content": "{\"pass\": true, \"readable\": true, \"has_multiple_parsers\": true, \"observed_columns\": [\"_parser\", \"latency_ms\", \"level\", \"message\", \"module\", \"status\", \"step\", \"timestamp\", \"tool\"], \"reason\": \"The table is readable with clear columns, contains 6 parsed rows, includes multiple parser identifiers (_parser: live_pipe_parser, live_bracket_parser), and structured fields (latency_ms, level, message, etc.)\"}"
},
"usage": {
"prompt_tokens": 2447,
"completion_tokens": 481,
"total_tokens": 2928,
"cached_prompt_tokens": 0
}
}
]
@@ -0,0 +1,4 @@
<!doctype html><meta charset=utf-8><title>Adaptive log parser</title>
<style>body{font-family:system-ui;background:#0b1020;color:#e8eefc;padding:30px}table{border-collapse:collapse;width:100%;background:#121a30}th,td{border:1px solid #33415f;padding:9px;text-align:left}th{color:#79c0ff}h1{color:#a5d6ff}</style>
<h1>Self-healed live log stream</h1><p>6 runtime records parsed after hot update.</p>
<table><thead><tr><th>_parser</th><th>latency_ms</th><th>level</th><th>message</th><th>module</th><th>status</th><th>step</th><th>timestamp</th><th>tool</th></tr></thead><tbody><tr><td>live_pipe_parser</td><td></td><td>INFO</td><td>accepted real request req-81</td><td>checkout.worker</td><td></td><td>1</td><td>2026-07-30T05:23:42Z</td><td></td></tr><tr><td>live_pipe_parser</td><td></td><td>WARNING</td><td>retrying payment authorization req-81</td><td>checkout.worker</td><td></td><td>2</td><td>2026-07-30T05:23:42Z</td><td></td></tr><tr><td>live_pipe_parser</td><td></td><td>ERROR</td><td>authorization exhausted req-81</td><td>checkout.worker</td><td></td><td>3</td><td>2026-07-30T05:23:42Z</td><td></td></tr><tr><td>live_bracket_parser</td><td>34</td><td>INFO</td><td>stock check completed</td><td></td><td>ok</td><td></td><td>2026-07-29T21:23:42.606+00:00</td><td>inventory_lookup</td></tr><tr><td>live_bracket_parser</td><td>181</td><td>WARNING</td><td>upstream requested retry</td><td></td><td>retry</td><td></td><td>2026-07-29T21:23:42.609+00:00</td><td>payment_api</td></tr><tr><td>live_bracket_parser</td><td>412</td><td>ERROR</td><td>deadline exceeded</td><td></td><td>timeout</td><td></td><td>2026-07-29T21:23:42.612+00:00</td><td>payment_api</td></tr></tbody></table>
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB