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
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:
@@ -0,0 +1,7 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
|
||||
# Agent 自动生成并持久化的解析器(运行时产物),保留目录但忽略生成的 .py
|
||||
parsers/*.py
|
||||
!parsers/.gitkeep
|
||||
@@ -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. / 命令、代码、路径与环境变量在中英文两侧保持一致。
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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-luna(gpt-5.x)直连 OpenAI 需组织实名认证,
|
||||
# 故设置了本 key 时会优先走 OpenRouter(route 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
|
||||
@@ -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
|
||||
}
|
||||
+27
@@ -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
|
||||
+28
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
+4
@@ -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>
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Reference in New Issue
Block a user