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,6 @@
|
||||
.env
|
||||
artifacts/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
@@ -0,0 +1,92 @@
|
||||
# Experiment 10-4 · Parallel research with real browser sessions
|
||||
|
||||
This implementation uses no simulated sources, canned content, or artificial source latency. The Manager dynamically launches one homogeneous worker per real university URL. Every worker owns an isolated Playwright Chromium browser context, navigates the live page, reads rendered text, and uses a real configured LLM endpoint for evidence-constrained profile extraction.
|
||||
|
||||
Implemented requirements:
|
||||
|
||||
- Dynamic N-way launch with target URL, teacher name, and routed task ID.
|
||||
- Push status updates over a timestamped asynchronous message bus.
|
||||
- Per-site timeout/error isolation; an inaccessible or structurally different site does not stop peers.
|
||||
- First `target_found` is settled under an `asyncio.Lock`; exactly one terminate broadcast is allowed and late hits are recorded.
|
||||
- Navigation and LLM extraction race against the terminate event. Losing workers cancel at a safe point, acknowledge, and close their browser context.
|
||||
- Context creation/closure counters make leaked browser sessions an explicit failing audit.
|
||||
- Serial and parallel paths visit the same live sites and use the same extraction function; wall-clock time and speedup are measured, not estimated.
|
||||
|
||||
## Code map
|
||||
|
||||
- **Run first:** python demo.py --target "Professor Name" --sites-json sites.example.json --agents 3.
|
||||
- **Start here:** agents.py::search_one and the Manager run path in run_official_experiment.py.
|
||||
- **Core behavior:** worker navigation/extraction, async message bus, first-target settlement and cancellation.
|
||||
- **State / protocol:** task IDs, status/result/terminate events, worker registry and manifest.
|
||||
- **Verifier:** evidence-constrained extraction, acceptance gates, lock-protected single winner, acknowledgement count and browser-context closure.
|
||||
- **Experiment variable:** site count, serial versus parallel scheduling and cascade timing.
|
||||
- **Skip on first pass:** provider request serialization, HTML fixtures and report formatting.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# From the repository root: use the shared Chapter 10 environment
|
||||
uv sync --locked --python 3.12 --extra ch10
|
||||
|
||||
# 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 ".[ch10]"
|
||||
|
||||
cd chapter10/parallel-web-research
|
||||
|
||||
# Single-project compatibility path, still supported during migration:
|
||||
# python -m pip install -r requirements.txt
|
||||
|
||||
playwright install chromium
|
||||
cp env.example .env # configure one real text-model endpoint
|
||||
python demo.py # 10 Stanford pages + real serial comparison
|
||||
```
|
||||
|
||||
For the provenance-complete acceptance campaign (the default comparison plus
|
||||
the four-worker live cascade in one run):
|
||||
|
||||
```bash
|
||||
python run_official_experiment.py --run-id exp10-4-real-receipts-YYYYMMDD-vN
|
||||
```
|
||||
|
||||
This runner stores full rendered browser observations, credential-free raw SDK
|
||||
request/response bodies with provider response IDs and usage, the message-bus
|
||||
event stream, exact runtime source hashes, artifact hashes, and acceptance gates.
|
||||
|
||||
Use your own university school/directory list:
|
||||
|
||||
```bash
|
||||
python demo.py --target 'Professor Name' --sites-json sites.example.json --agents 3
|
||||
```
|
||||
|
||||
`cascade-stress.example.json` repeats a real target-bearing Stanford profile under distinct query URLs solely to make near-simultaneous live hits and cancellation observable. It is a real-browser stress supplement, not the multi-school research dataset.
|
||||
|
||||
## Recorded real integration evidence
|
||||
|
||||
On 2026-07-29, the default ten-page Stanford run found Andrew Ng on the live Stanford HAI page using ARK extraction. Parallel wall time was 18.542 s; serial time was 58.264 s, a measured 3.142× speedup. All 10 parallel and 10 serial browser contexts closed. The live cascade stress run produced one winner, one terminate broadcast, three losing-worker acknowledgements, and 4/4 closed contexts.
|
||||
|
||||
The current provenance-complete campaign is
|
||||
[`validation/runs/exp10-4-real-receipts-20260730-v2/manifest.json`](validation/runs/exp10-4-real-receipts-20260730-v2/manifest.json).
|
||||
All 12 acceptance gates passed: the ten-site parallel and serial paths both
|
||||
found the target and closed all 20 contexts; the measured speedup was 1.872×;
|
||||
the cascade produced one broadcast, three loser acknowledgements, and 4/4
|
||||
closed contexts. The run retains 24 full browser observations, three raw ARK
|
||||
responses with unique response IDs and usage, and 114 bus events. Seven runtime
|
||||
source/input hashes and all four artifact hashes recompute exactly, and the
|
||||
credential scan found zero hits.
|
||||
|
||||
The earlier sanitized summary-only records remain at
|
||||
[`validation/real_parallel_serial_2026-07-29.json`](validation/real_parallel_serial_2026-07-29.json)
|
||||
and [`validation/real_cascade_2026-07-29.json`](validation/real_cascade_2026-07-29.json)
|
||||
for historical comparison; they are not the current provenance anchor.
|
||||
|
||||
---
|
||||
|
||||
## 中文说明
|
||||
|
||||
本实现不再使用“可控字符串 + 模拟延迟”。每个同构子 Agent 都拥有独立 Playwright Chromium context,访问真实大学网站、读取实际渲染内容,再由真实 LLM 做证据约束抽取。Manager 维护状态表、错误隔离、超时、加锁单次结算、级联终止、ack 与资源关闭审计;默认还会在同一批网站上实跑串行基线。
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Real-browser workers and central coordinator for Experiment 10-4."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from llm import extract_profile
|
||||
from message_bus import BROADCAST, MessageBus
|
||||
from sources import Website
|
||||
|
||||
|
||||
class TaskState(str, Enum):
|
||||
SUBMITTED = "已提交"
|
||||
RUNNING = "执行中"
|
||||
SUCCEEDED = "已完成"
|
||||
FAILED = "失败"
|
||||
TERMINATED = "已终止"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskRecord:
|
||||
worker_id: str
|
||||
source_name: str
|
||||
state: TaskState = TaskState.SUBMITTED
|
||||
note: str = ""
|
||||
updated: float = field(default_factory=time.monotonic)
|
||||
|
||||
|
||||
class BrowserPool:
|
||||
"""One Chromium process, one fully isolated browser context per worker."""
|
||||
|
||||
def __init__(self, headless: bool = True):
|
||||
self.headless = headless
|
||||
self._pw = None
|
||||
self.browser = None
|
||||
self.contexts_created = 0
|
||||
self.contexts_closed = 0
|
||||
|
||||
async def start(self):
|
||||
from playwright.async_api import async_playwright
|
||||
self._pw = await async_playwright().start()
|
||||
self.browser = await self._pw.chromium.launch(headless=self.headless)
|
||||
|
||||
async def new_context(self):
|
||||
if not self.browser:
|
||||
raise RuntimeError("BrowserPool not started")
|
||||
context = await self.browser.new_context()
|
||||
self.contexts_created += 1
|
||||
return context
|
||||
|
||||
async def mark_closed(self):
|
||||
self.contexts_closed += 1
|
||||
|
||||
async def close(self):
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
if self._pw:
|
||||
await self._pw.stop()
|
||||
|
||||
|
||||
class WorkerAgent:
|
||||
def __init__(self, worker_id: str, site: Website, bus: MessageBus, target: str,
|
||||
browsers: BrowserPool, timeout: float = 120,
|
||||
browser_receipt_sink: Optional[Callable[[dict], None]] = None,
|
||||
llm_receipt_sink: Optional[Callable[[dict], None]] = None,
|
||||
run_phase: str = "parallel"):
|
||||
self.id, self.site, self.bus, self.target = worker_id, site, bus, target
|
||||
self.browsers, self.timeout = browsers, timeout
|
||||
self.sub = bus.subscribe(worker_id, types=["task_assigned", "terminate"])
|
||||
self.terminate = asyncio.Event()
|
||||
self._termination_reason = ""
|
||||
self.context = None
|
||||
self.browser_receipt_sink = browser_receipt_sink
|
||||
self.llm_receipt_sink = llm_receipt_sink
|
||||
self.run_phase = run_phase
|
||||
|
||||
async def report(self, state: TaskState, note: str):
|
||||
await self.bus.send(self.id, "coordinator", "status_update", {
|
||||
"state": state.value, "note": note, "source": self.site.name,
|
||||
})
|
||||
|
||||
async def _signals(self):
|
||||
while True:
|
||||
message = await self.sub.get()
|
||||
if message.type == "terminate":
|
||||
self._termination_reason = message.payload.get("reason", "cascade")
|
||||
self.terminate.set()
|
||||
return
|
||||
|
||||
async def _await_interruptibly(self, awaitable):
|
||||
operation = asyncio.create_task(awaitable)
|
||||
stopping = asyncio.create_task(self.terminate.wait())
|
||||
try:
|
||||
done, _ = await asyncio.wait(
|
||||
{operation, stopping}, return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
except BaseException:
|
||||
operation.cancel()
|
||||
stopping.cancel()
|
||||
await asyncio.gather(operation, stopping, return_exceptions=True)
|
||||
raise
|
||||
if stopping in done and self.terminate.is_set():
|
||||
operation.cancel()
|
||||
await asyncio.gather(operation, return_exceptions=True)
|
||||
raise asyncio.CancelledError
|
||||
stopping.cancel()
|
||||
await asyncio.gather(stopping, return_exceptions=True)
|
||||
return await operation
|
||||
|
||||
async def _navigate_interruptibly(self, page):
|
||||
return await self._await_interruptibly(page.goto(
|
||||
self.site.url, wait_until="domcontentloaded", timeout=int(self.timeout * 1000)
|
||||
))
|
||||
|
||||
async def run(self):
|
||||
assigned = await self.sub.get()
|
||||
while assigned.type != "task_assigned":
|
||||
if assigned.type == "terminate":
|
||||
self._termination_reason = assigned.payload.get("reason", "cascade")
|
||||
self.terminate.set()
|
||||
await self.report(TaskState.TERMINATED, f"安全点响应终止:{self._termination_reason}")
|
||||
await self.bus.send(self.id, "coordinator", "ack", {
|
||||
"acked": "terminate", "source": self.site.name,
|
||||
})
|
||||
return
|
||||
assigned = await self.sub.get()
|
||||
signal_task = asyncio.create_task(self._signals())
|
||||
try:
|
||||
await self.report(TaskState.RUNNING, "创建独立 Chromium context")
|
||||
self.context = await self.browsers.new_context()
|
||||
page = await self.context.new_page()
|
||||
await self.report(TaskState.RUNNING, f"正在加载 {self.site.url}")
|
||||
navigation = await self._navigate_interruptibly(page)
|
||||
if self.terminate.is_set():
|
||||
raise asyncio.CancelledError
|
||||
await self.report(TaskState.RUNNING, "正在读取渲染后的教师页面")
|
||||
text = await self._await_interruptibly(
|
||||
page.locator("body").inner_text(timeout=20_000)
|
||||
)
|
||||
if self.browser_receipt_sink:
|
||||
self.browser_receipt_sink({
|
||||
"kind": "rendered_browser_observation",
|
||||
"phase": self.run_phase,
|
||||
"worker_id": self.id,
|
||||
"site": self.site.name,
|
||||
"college": self.site.college,
|
||||
"requested_url": self.site.url,
|
||||
"final_url": page.url,
|
||||
"http_status": navigation.status if navigation else None,
|
||||
"rendered_body_text": text,
|
||||
})
|
||||
if self.terminate.is_set():
|
||||
raise asyncio.CancelledError
|
||||
await self.report(TaskState.RUNNING, "正在做证据约束的教师信息抽取")
|
||||
profile = await self._await_interruptibly(
|
||||
extract_profile(
|
||||
self.target,
|
||||
self.site.college,
|
||||
self.site.url,
|
||||
text,
|
||||
receipt_sink=self.llm_receipt_sink,
|
||||
call_context={
|
||||
"phase": self.run_phase,
|
||||
"worker_id": self.id,
|
||||
"site": self.site.name,
|
||||
},
|
||||
)
|
||||
)
|
||||
if profile.get("found"):
|
||||
await self.bus.send(self.id, "coordinator", "target_found", {
|
||||
"data": profile, "source": self.site.name,
|
||||
})
|
||||
await self.report(TaskState.SUCCEEDED, "找到目标教师")
|
||||
else:
|
||||
await self.bus.send(self.id, "coordinator", "not_found", {
|
||||
"reason": profile.get("reason", "not found"), "source": self.site.name,
|
||||
})
|
||||
await self.report(TaskState.SUCCEEDED, "页面中未找到目标")
|
||||
except asyncio.CancelledError:
|
||||
if self.terminate.is_set():
|
||||
await self.report(TaskState.TERMINATED, f"安全点响应终止:{self._termination_reason}")
|
||||
await self.bus.send(self.id, "coordinator", "ack", {
|
||||
"acked": "terminate", "source": self.site.name,
|
||||
})
|
||||
else:
|
||||
await self.bus.send(self.id, "coordinator", "worker_error", {
|
||||
"error": f"TimeoutError: exceeded {self.timeout + 15:.0f}s worker deadline",
|
||||
"source": self.site.name,
|
||||
})
|
||||
await self.report(TaskState.FAILED, "任务超时,已关闭独立浏览器会话")
|
||||
except Exception as exc:
|
||||
await self.bus.send(self.id, "coordinator", "worker_error", {
|
||||
"error": f"{type(exc).__name__}: {exc}", "source": self.site.name,
|
||||
})
|
||||
await self.report(TaskState.FAILED, f"{type(exc).__name__}: {exc}")
|
||||
finally:
|
||||
signal_task.cancel()
|
||||
await asyncio.gather(signal_task, return_exceptions=True)
|
||||
context_closed = self.context is None
|
||||
if self.context:
|
||||
try:
|
||||
await self.context.close()
|
||||
await self.browsers.mark_closed()
|
||||
context_closed = True
|
||||
except Exception as exc:
|
||||
await self.bus.send(self.id, "coordinator", "worker_error", {
|
||||
"error": f"ContextCloseError: {exc}", "source": self.site.name,
|
||||
})
|
||||
await self.bus.send(self.id, "coordinator", "resource_closed", {
|
||||
"browser_context_closed": context_closed, "source": self.site.name,
|
||||
})
|
||||
|
||||
|
||||
class Coordinator:
|
||||
def __init__(self, bus: MessageBus, target: str):
|
||||
self.bus, self.target = bus, target
|
||||
self.sub = bus.subscribe("coordinator", types=None)
|
||||
self.workers: List[WorkerAgent] = []
|
||||
self.table: Dict[str, TaskRecord] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._settled = False
|
||||
self.winner: Optional[str] = None
|
||||
self.profile: Optional[dict] = None
|
||||
self.expected_loser_acks: Optional[set[str]] = None
|
||||
self.duplicate_hits: List[str] = []
|
||||
self.acks: set[str] = set()
|
||||
self.errors: Dict[str, str] = {}
|
||||
self.not_found: Dict[str, str] = {}
|
||||
self.closed: set[str] = set()
|
||||
self.resource_failures: Dict[str, str] = {}
|
||||
|
||||
def add_worker(self, worker: WorkerAgent):
|
||||
self.workers.append(worker)
|
||||
self.table[worker.id] = TaskRecord(worker.id, worker.site.name)
|
||||
|
||||
async def _settle(self, worker_id: str, profile: dict):
|
||||
async with self._lock:
|
||||
if self._settled:
|
||||
self.duplicate_hits.append(worker_id)
|
||||
return
|
||||
self._settled, self.winner, self.profile = True, worker_id, profile
|
||||
# Only workers still running when the winner settles receive the
|
||||
# terminate broadcast and therefore owe an acknowledgement.
|
||||
self.expected_loser_acks = {
|
||||
worker.id
|
||||
for worker in self.workers
|
||||
if worker.id != worker_id
|
||||
and worker.id not in self.not_found
|
||||
and worker.id not in self.errors
|
||||
}
|
||||
await self.bus.send("coordinator", BROADCAST, "terminate", {
|
||||
"reason": f"target_found_by_{worker_id}", "winner": worker_id,
|
||||
})
|
||||
|
||||
async def run(self) -> dict:
|
||||
started = time.monotonic()
|
||||
for w in self.workers:
|
||||
await self.bus.send("coordinator", w.id, "task_assigned", {
|
||||
"target": self.target, "url": w.site.url, "task_id": w.id,
|
||||
})
|
||||
tasks = [asyncio.create_task(asyncio.wait_for(w.run(), timeout=w.timeout + 15)) for w in self.workers]
|
||||
while len(self.closed) < len(self.workers):
|
||||
try:
|
||||
env = await asyncio.wait_for(self.sub.get(), timeout=0.5)
|
||||
except asyncio.TimeoutError:
|
||||
if all(t.done() for t in tasks):
|
||||
break
|
||||
continue
|
||||
rec = self.table.get(env.sender_id)
|
||||
if env.type == "status_update" and rec:
|
||||
rec.state = TaskState(env.payload["state"])
|
||||
rec.note = env.payload.get("note", "")
|
||||
rec.updated = time.monotonic()
|
||||
elif env.type == "target_found":
|
||||
await self._settle(env.sender_id, env.payload["data"])
|
||||
elif env.type == "ack":
|
||||
self.acks.add(env.sender_id)
|
||||
elif env.type == "worker_error":
|
||||
self.errors[env.sender_id] = env.payload["error"]
|
||||
elif env.type == "not_found":
|
||||
self.not_found[env.sender_id] = env.payload.get("reason", "not found")
|
||||
elif env.type == "resource_closed":
|
||||
self.closed.add(env.sender_id)
|
||||
if not env.payload.get("browser_context_closed", False):
|
||||
self.resource_failures[env.sender_id] = "browser context did not close"
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
failure_types: Dict[str, int] = {}
|
||||
for error in self.errors.values():
|
||||
kind = error.split(":", 1)[0]
|
||||
failure_types[kind] = failure_types.get(kind, 0) + 1
|
||||
expected_acks = self.expected_loser_acks or set()
|
||||
missing_acks = expected_acks - self.acks
|
||||
return {
|
||||
"outcome": "found" if self.winner else "not_found",
|
||||
"winner": self.winner,
|
||||
"profile": self.profile,
|
||||
"duplicate_hits": self.duplicate_hits,
|
||||
"acks": sorted(self.acks),
|
||||
"expected_loser_acks": sorted(expected_acks),
|
||||
"missing_loser_acks": sorted(missing_acks),
|
||||
"errors": self.errors,
|
||||
"failure_summary": {
|
||||
"count": len(self.errors),
|
||||
"by_type": failure_types,
|
||||
},
|
||||
"not_found_reasons": self.not_found,
|
||||
"status_table": {
|
||||
worker_id: {
|
||||
"source": record.source_name,
|
||||
"state": record.state.value,
|
||||
"note": record.note,
|
||||
}
|
||||
for worker_id, record in self.table.items()
|
||||
},
|
||||
"terminate_broadcasts": sum(1 for e in self.bus.history if e.type == "terminate"),
|
||||
"parallel_seconds": round(time.monotonic() - started, 3),
|
||||
"contexts_closed": len(self.closed),
|
||||
"resource_failures": self.resource_failures,
|
||||
}
|
||||
|
||||
|
||||
async def search_one(
|
||||
site: Website,
|
||||
target: str,
|
||||
browsers: BrowserPool,
|
||||
timeout: float,
|
||||
browser_receipt_sink: Optional[Callable[[dict], None]] = None,
|
||||
llm_receipt_sink: Optional[Callable[[dict], None]] = None,
|
||||
worker_id: str = "serial",
|
||||
run_phase: str = "serial",
|
||||
) -> dict:
|
||||
context = await browsers.new_context()
|
||||
started = time.monotonic()
|
||||
try:
|
||||
page = await context.new_page()
|
||||
navigation = await page.goto(site.url, wait_until="domcontentloaded", timeout=int(timeout * 1000))
|
||||
text = await page.locator("body").inner_text(timeout=20_000)
|
||||
if browser_receipt_sink:
|
||||
browser_receipt_sink({
|
||||
"kind": "rendered_browser_observation",
|
||||
"phase": run_phase,
|
||||
"worker_id": worker_id,
|
||||
"site": site.name,
|
||||
"college": site.college,
|
||||
"requested_url": site.url,
|
||||
"final_url": page.url,
|
||||
"http_status": navigation.status if navigation else None,
|
||||
"rendered_body_text": text,
|
||||
})
|
||||
profile = await extract_profile(
|
||||
target,
|
||||
site.college,
|
||||
site.url,
|
||||
text,
|
||||
receipt_sink=llm_receipt_sink,
|
||||
call_context={"phase": run_phase, "worker_id": worker_id, "site": site.name},
|
||||
)
|
||||
return {"site": site.name, "profile": profile, "seconds": time.monotonic() - started}
|
||||
finally:
|
||||
await context.close()
|
||||
await browsers.mark_closed()
|
||||
|
||||
|
||||
async def run_sequential(
|
||||
sites: List[Website],
|
||||
target: str,
|
||||
browsers: BrowserPool,
|
||||
timeout: float,
|
||||
browser_receipt_sink: Optional[Callable[[dict], None]] = None,
|
||||
llm_receipt_sink: Optional[Callable[[dict], None]] = None,
|
||||
run_phase: str = "serial",
|
||||
) -> dict:
|
||||
started = time.monotonic()
|
||||
results = []
|
||||
for site in sites:
|
||||
try:
|
||||
item = await search_one(
|
||||
site,
|
||||
target,
|
||||
browsers,
|
||||
timeout,
|
||||
browser_receipt_sink=browser_receipt_sink,
|
||||
llm_receipt_sink=llm_receipt_sink,
|
||||
worker_id=f"serial-{len(results):02d}",
|
||||
run_phase=run_phase,
|
||||
)
|
||||
results.append(item)
|
||||
if item["profile"].get("found"):
|
||||
break
|
||||
except Exception as exc:
|
||||
results.append({"site": site.name, "error": f"{type(exc).__name__}: {exc}"})
|
||||
return {"seconds": round(time.monotonic() - started, 3), "visited": len(results), "results": results}
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"name": "hai-a", "college": "Stanford HAI", "url": "https://hai.stanford.edu/people/andrew-ng?worker=a"},
|
||||
{"name": "hai-b", "college": "Stanford HAI", "url": "https://hai.stanford.edu/people/andrew-ng?worker=b"},
|
||||
{"name": "hai-c", "college": "Stanford HAI", "url": "https://hai.stanford.edu/people/andrew-ng?worker=c"},
|
||||
{"name": "hai-d", "college": "Stanford HAI", "url": "https://hai.stanford.edu/people/andrew-ng?worker=d"}
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Experiment 10-4: parallel real-browser faculty search."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from agents import BrowserPool, Coordinator, WorkerAgent, run_sequential
|
||||
from message_bus import MessageBus
|
||||
from sources import DEFAULT_SITES, TARGET, load_sites
|
||||
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser(description="实验 10-4:N 个独立真实浏览器会话并行搜索教师")
|
||||
p.add_argument("--target", default=TARGET, help="要查找的教师姓名")
|
||||
p.add_argument("--sites-json", help="网站数组 JSON,每项包含 name/college/url")
|
||||
p.add_argument("--agents", type=int, default=len(DEFAULT_SITES), help="使用前 N 个网站/Agent")
|
||||
p.add_argument("--timeout", type=float, default=120, help="每网站超时秒数")
|
||||
p.add_argument("--headed", action="store_true", help="显示每个真实浏览器页面")
|
||||
p.add_argument("--quiet", action="store_true", help="不打印逐条总线消息")
|
||||
p.add_argument("--no-compare", action="store_true", help="跳过串行基线(默认真实运行并对比)")
|
||||
p.add_argument("--output", default="artifacts/latest.json", help="保存实测结果 JSON")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
async def main(args) -> int:
|
||||
sites = load_sites(args.sites_json, args.agents)
|
||||
print(f"真实目标:{args.target}; 真实网站/独立 browser context 数:{len(sites)}")
|
||||
|
||||
parallel_pool = BrowserPool(headless=not args.headed)
|
||||
await parallel_pool.start()
|
||||
try:
|
||||
bus = MessageBus(verbose=not args.quiet)
|
||||
coordinator = Coordinator(bus, args.target)
|
||||
for i, site in enumerate(sites):
|
||||
coordinator.add_worker(WorkerAgent(
|
||||
f"agent-{i:02d}", site, bus, args.target, parallel_pool, args.timeout
|
||||
))
|
||||
parallel = await coordinator.run()
|
||||
finally:
|
||||
await parallel_pool.close()
|
||||
|
||||
serial = None
|
||||
serial_pool = None
|
||||
if not args.no_compare:
|
||||
serial_pool = BrowserPool(headless=not args.headed)
|
||||
await serial_pool.start()
|
||||
try:
|
||||
serial = await run_sequential(sites, args.target, serial_pool, args.timeout)
|
||||
finally:
|
||||
await serial_pool.close()
|
||||
|
||||
evidence = {
|
||||
"target": args.target,
|
||||
"sites": [site.__dict__ for site in sites],
|
||||
"parallel": parallel,
|
||||
"serial": serial,
|
||||
"resource_audit": {
|
||||
"parallel_contexts_created": parallel_pool.contexts_created,
|
||||
"parallel_contexts_closed": parallel_pool.contexts_closed,
|
||||
"serial_contexts_created": serial_pool.contexts_created if serial_pool else 0,
|
||||
"serial_contexts_closed": serial_pool.contexts_closed if serial_pool else 0,
|
||||
},
|
||||
"measured_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
}
|
||||
if serial and parallel["parallel_seconds"]:
|
||||
evidence["measured_speedup"] = round(serial["seconds"] / parallel["parallel_seconds"], 3)
|
||||
output = Path(args.output)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(evidence, ensure_ascii=False, indent=2))
|
||||
|
||||
resources_ok = parallel_pool.contexts_created == parallel_pool.contexts_closed
|
||||
single_cascade = parallel["winner"] is None or parallel["terminate_broadcasts"] == 1
|
||||
acknowledgements_ok = not parallel["missing_loser_acks"]
|
||||
print(f"资源清理:{'PASS' if resources_ok else 'FAIL'}; 单次级联广播:{'PASS' if single_cascade else 'FAIL'}")
|
||||
if parallel["winner"] is None:
|
||||
print(f"搜索完成:未找到目标教师;失败统计={parallel['failure_summary']}")
|
||||
return 0 if resources_ok and single_cascade and acknowledgements_ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(asyncio.run(main(parse_args())))
|
||||
@@ -0,0 +1,10 @@
|
||||
# Set at least one real text-model provider. The first successful endpoint performs
|
||||
# evidence-constrained profile extraction from text rendered by Chromium.
|
||||
# ARK_API_KEY=
|
||||
# ARK_MODEL=doubao-seed-1-6-250615
|
||||
# MOONSHOT_API_KEY=
|
||||
# MOONSHOT_MODEL=kimi-k3
|
||||
# OPENAI_API_KEY=
|
||||
# OPENAI_MODEL=gpt-4.1-mini
|
||||
# OPENAI_BASE_URL=
|
||||
# OPENROUTER_API_KEY=
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Evidence-grounded profile extraction with real configured LLM APIs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
|
||||
ReceiptSink = Optional[Callable[[Dict[str, object]], None]]
|
||||
|
||||
|
||||
def _backends():
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
out = []
|
||||
if os.getenv("ARK_API_KEY"):
|
||||
out.append((AsyncOpenAI(api_key=os.environ["ARK_API_KEY"], base_url="https://ark.cn-beijing.volces.com/api/v3"), os.getenv("ARK_MODEL", "doubao-seed-1-6-250615"), "ark"))
|
||||
if os.getenv("MOONSHOT_API_KEY"):
|
||||
out.append((AsyncOpenAI(api_key=os.environ["MOONSHOT_API_KEY"], base_url="https://api.moonshot.cn/v1"), os.getenv("MOONSHOT_MODEL", "kimi-k3"), "moonshot"))
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
out.append((AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"], base_url=os.getenv("OPENAI_BASE_URL") or None), os.getenv("OPENAI_MODEL", "gpt-4.1-mini"), "openai"))
|
||||
if os.getenv("OPENROUTER_API_KEY"):
|
||||
raw = os.getenv("OPENAI_MODEL", "gpt-4.1-mini")
|
||||
out.append((AsyncOpenAI(api_key=os.environ["OPENROUTER_API_KEY"], base_url="https://openrouter.ai/api/v1"), raw if "/" in raw else f"openai/{raw}", "openrouter"))
|
||||
if not out:
|
||||
raise RuntimeError("真实网页内容抽取需要 ARK/MOONSHOT/OPENAI/OPENROUTER 任一 API Key")
|
||||
return out
|
||||
|
||||
|
||||
async def extract_profile(
|
||||
target: str,
|
||||
college: str,
|
||||
url: str,
|
||||
text: str,
|
||||
receipt_sink: ReceiptSink = None,
|
||||
call_context: Optional[Dict[str, object]] = None,
|
||||
) -> Dict[str, object]:
|
||||
"""Extract only facts visible in the browser observation.
|
||||
|
||||
The deterministic name-presence gate prevents a model from supplying a profile
|
||||
from parametric memory when the page did not actually contain the target.
|
||||
"""
|
||||
if target.casefold() not in text.casefold():
|
||||
return {"found": False, "reason": "target name absent from rendered page"}
|
||||
clipped = text[:45_000]
|
||||
prompt = {
|
||||
"target": target,
|
||||
"site_college": college,
|
||||
"url": url,
|
||||
"rendered_page_text": clipped,
|
||||
"instruction": (
|
||||
"Use only rendered_page_text. Decide whether it contains this exact person's faculty profile. "
|
||||
"Return JSON keys found, name, college, position, research, evidence. If the name is only a link/listing, "
|
||||
"found may be true but leave unsupported fields empty. evidence must be a short verbatim excerpt."
|
||||
),
|
||||
}
|
||||
last = None
|
||||
for client, model, provider in _backends():
|
||||
started = time.monotonic()
|
||||
try:
|
||||
kwargs = dict(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": json.dumps(prompt, ensure_ascii=False)}],
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
if "kimi-k3" in model:
|
||||
kwargs.update(temperature=1, max_tokens=2048)
|
||||
response = await client.chat.completions.create(**kwargs)
|
||||
raw_response = response.model_dump(mode="json")
|
||||
if receipt_sink:
|
||||
receipt_sink({
|
||||
"kind": "llm_chat_completion",
|
||||
"context": dict(call_context or {}),
|
||||
"provider": provider,
|
||||
"request": kwargs,
|
||||
"response": raw_response,
|
||||
"response_id": response.id,
|
||||
"response_model": response.model,
|
||||
"usage": response.usage.model_dump(mode="json") if response.usage else None,
|
||||
"duration_seconds": round(time.monotonic() - started, 3),
|
||||
})
|
||||
content = response.choices[0].message.content or ""
|
||||
if not content.strip():
|
||||
raise ValueError("empty model response")
|
||||
result = json.loads(content)
|
||||
result["provider"] = provider
|
||||
result["url"] = url
|
||||
return result
|
||||
except Exception as exc:
|
||||
last = exc
|
||||
if receipt_sink:
|
||||
receipt_sink({
|
||||
"kind": "llm_chat_completion_error",
|
||||
"context": dict(call_context or {}),
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"error_type": type(exc).__name__,
|
||||
"duration_seconds": round(time.monotonic() - started, 3),
|
||||
})
|
||||
print(f" [extract] {provider} failed: {type(exc).__name__}; trying next endpoint")
|
||||
raise RuntimeError("all configured LLM extraction endpoints failed") from last
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
进程内异步消息总线(Message Bus)
|
||||
================================
|
||||
|
||||
模仿 Redis Pub/Sub 的语义,但完全跑在单进程的 asyncio 事件循环里,
|
||||
无需真正部署 Redis。它承担实验 10-4 里"中心协调"的通信底座:
|
||||
|
||||
- 每条消息都封装在 ``Envelope`` 信封里,带上 sender_id / target / type / payload;
|
||||
- Agent 通过 ``subscribe()`` 拿到一个订阅句柄,按消息类型接收;
|
||||
- Agent 通过 ``publish()`` 把消息投递给指定目标或广播给所有人;
|
||||
- 总线本身不做任何业务判断,只负责"可靠地把信封送达订阅者"。
|
||||
|
||||
设计要点:
|
||||
- 使用 ``asyncio.Queue`` 做每个订阅者的收件箱,天然线程/协程安全;
|
||||
- ``target`` 为 ``BROADCAST`` 时投递给所有订阅了该类型的人(发送者除外);
|
||||
- 打印带时间戳的事件日志,方便在演示里"看见"发布/订阅的消息流。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import itertools
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
# 广播目标常量:发给所有订阅者
|
||||
BROADCAST = "*"
|
||||
|
||||
# 全局单调递增的消息序号,方便在日志里追踪顺序
|
||||
_seq_counter = itertools.count(1)
|
||||
|
||||
# 演示启动时间,用于打印相对时间戳(更易读)
|
||||
_START_TIME = time.monotonic()
|
||||
|
||||
|
||||
def _now() -> float:
|
||||
"""返回自演示启动以来的秒数(相对时间戳)。"""
|
||||
return time.monotonic() - _START_TIME
|
||||
|
||||
|
||||
@dataclass
|
||||
class Envelope:
|
||||
"""消息信封:总线里流动的最小单元。"""
|
||||
|
||||
sender_id: str # 发送者 ID
|
||||
target: str # 目标 Agent ID,或 BROADCAST 表示广播
|
||||
type: str # 消息类型:task_assigned / status_update / result / terminate / ack ...
|
||||
payload: Dict[str, Any] = field(default_factory=dict) # JSON 负载
|
||||
seq: int = field(default_factory=lambda: next(_seq_counter)) # 全局序号
|
||||
ts: float = field(default_factory=_now) # 相对时间戳
|
||||
|
||||
def short(self) -> str:
|
||||
"""给日志用的紧凑单行表示。"""
|
||||
tgt = "ALL" if self.target == BROADCAST else self.target
|
||||
try:
|
||||
body = json.dumps(self.payload, ensure_ascii=False, default=str)
|
||||
except Exception:
|
||||
body = str(self.payload)
|
||||
if len(body) > 80:
|
||||
body = body[:77] + "..."
|
||||
return (
|
||||
f"[t={self.ts:6.2f}s #{self.seq:<3}] "
|
||||
f"{self.sender_id:>11} -> {tgt:<11} | {self.type:<14} | {body}"
|
||||
)
|
||||
|
||||
|
||||
class Subscription:
|
||||
"""订阅句柄:内部就是一个收件箱队列 + 关心的消息类型集合。"""
|
||||
|
||||
def __init__(self, owner_id: str, types: Optional[List[str]]):
|
||||
self.owner_id = owner_id
|
||||
# types 为 None 表示订阅所有类型
|
||||
self.types = set(types) if types is not None else None
|
||||
self.inbox: "asyncio.Queue[Envelope]" = asyncio.Queue()
|
||||
|
||||
def accepts(self, env: Envelope) -> bool:
|
||||
return self.types is None or env.type in self.types
|
||||
|
||||
async def get(self) -> Envelope:
|
||||
return await self.inbox.get()
|
||||
|
||||
async def get_nowait_or_wait(self, timeout: float) -> Optional[Envelope]:
|
||||
"""带超时地取一条消息;超时返回 None(便于子 Agent 在循环里轮询终止信号)。"""
|
||||
try:
|
||||
return await asyncio.wait_for(self.inbox.get(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
return None
|
||||
|
||||
|
||||
class MessageBus:
|
||||
"""异步消息总线:注册订阅者、投递信封、打印消息流日志。"""
|
||||
|
||||
def __init__(self, verbose: bool = True):
|
||||
# owner_id -> 该 owner 的订阅列表
|
||||
self._subs: Dict[str, List[Subscription]] = {}
|
||||
self.verbose = verbose
|
||||
# 记录全部流过总线的信封,便于事后统计/断言
|
||||
self.history: List[Envelope] = []
|
||||
|
||||
def subscribe(self, owner_id: str, types: Optional[List[str]] = None) -> Subscription:
|
||||
"""注册一个订阅者,返回订阅句柄。types=None 表示接收所有类型。"""
|
||||
sub = Subscription(owner_id, types)
|
||||
self._subs.setdefault(owner_id, []).append(sub)
|
||||
return sub
|
||||
|
||||
async def publish(self, env: Envelope) -> None:
|
||||
"""把信封投递到总线:广播或点对点。"""
|
||||
self.history.append(env)
|
||||
if self.verbose:
|
||||
print(" BUS " + env.short())
|
||||
|
||||
delivered = 0
|
||||
for owner_id, sub_list in self._subs.items():
|
||||
# 点对点:只投递给指定目标
|
||||
if env.target != BROADCAST and owner_id != env.target:
|
||||
continue
|
||||
# 广播时不回投给发送者自己
|
||||
if env.target == BROADCAST and owner_id == env.sender_id:
|
||||
continue
|
||||
for sub in sub_list:
|
||||
if sub.accepts(env):
|
||||
await sub.inbox.put(env)
|
||||
delivered += 1
|
||||
|
||||
# 让出事件循环,保证消息尽快被对端取走(更接近真实推送时序)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# —— 便捷构造并发布 ——
|
||||
async def send(
|
||||
self,
|
||||
sender_id: str,
|
||||
target: str,
|
||||
type: str,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
) -> Envelope:
|
||||
env = Envelope(sender_id=sender_id, target=target, type=type, payload=payload or {})
|
||||
await self.publish(env)
|
||||
return env
|
||||
@@ -0,0 +1,5 @@
|
||||
openai>=1.30.0
|
||||
playwright>=1.44.0
|
||||
python-dotenv>=1.0.0
|
||||
pytest>=8.0.0
|
||||
pytest-asyncio>=0.23.0
|
||||
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run Experiment 10-4 with provenance-complete real-provider receipts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from agents import BrowserPool, Coordinator, WorkerAgent, run_sequential
|
||||
from message_bus import MessageBus
|
||||
from sources import TARGET, Website, load_sites
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
SOURCE_FILES = [
|
||||
"run_official_experiment.py",
|
||||
"demo.py",
|
||||
"agents.py",
|
||||
"llm.py",
|
||||
"message_bus.py",
|
||||
"sources.py",
|
||||
"cascade-stress.example.json",
|
||||
]
|
||||
SECRET_ENV_NAMES = (
|
||||
"ARK_API_KEY",
|
||||
"MOONSHOT_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
)
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def canonical_bytes(value: Any) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
return sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def git_commit() -> str | None:
|
||||
try:
|
||||
return subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return None
|
||||
|
||||
|
||||
class ReceiptRecorder:
|
||||
def __init__(self) -> None:
|
||||
self.browser: List[Dict[str, Any]] = []
|
||||
self.llm: List[Dict[str, Any]] = []
|
||||
self.bus: List[Dict[str, Any]] = []
|
||||
|
||||
def record_browser(self, receipt: dict) -> None:
|
||||
item = dict(receipt)
|
||||
body = item.get("rendered_body_text", "")
|
||||
item["rendered_body_bytes"] = len(body.encode("utf-8"))
|
||||
item["rendered_body_sha256"] = sha256_bytes(body.encode("utf-8"))
|
||||
item["captured_at"] = utc_now()
|
||||
self.browser.append(item)
|
||||
|
||||
def record_llm(self, receipt: dict) -> None:
|
||||
item = dict(receipt)
|
||||
request = item.get("request")
|
||||
response = item.get("response")
|
||||
if request is not None:
|
||||
item["request_sha256"] = sha256_bytes(canonical_bytes(request))
|
||||
if response is not None:
|
||||
item["response_sha256"] = sha256_bytes(canonical_bytes(response))
|
||||
item["captured_at"] = utc_now()
|
||||
self.llm.append(item)
|
||||
|
||||
def record_bus(self, phase: str, bus: MessageBus) -> None:
|
||||
for env in bus.history:
|
||||
self.bus.append({
|
||||
"phase": phase,
|
||||
"sender_id": env.sender_id,
|
||||
"target": env.target,
|
||||
"type": env.type,
|
||||
"payload": env.payload,
|
||||
"sequence": env.seq,
|
||||
"relative_seconds": round(env.ts, 6),
|
||||
})
|
||||
|
||||
|
||||
async def run_parallel_phase(
|
||||
sites: List[Website],
|
||||
target: str,
|
||||
timeout: float,
|
||||
phase: str,
|
||||
recorder: ReceiptRecorder,
|
||||
) -> Dict[str, Any]:
|
||||
pool = BrowserPool(headless=True)
|
||||
await pool.start()
|
||||
browser_version = pool.browser.version if pool.browser else None
|
||||
try:
|
||||
bus = MessageBus(verbose=False)
|
||||
coordinator = Coordinator(bus, target)
|
||||
for index, site in enumerate(sites):
|
||||
coordinator.add_worker(WorkerAgent(
|
||||
f"agent-{index:02d}",
|
||||
site,
|
||||
bus,
|
||||
target,
|
||||
pool,
|
||||
timeout,
|
||||
browser_receipt_sink=recorder.record_browser,
|
||||
llm_receipt_sink=recorder.record_llm,
|
||||
run_phase=phase,
|
||||
))
|
||||
result = await coordinator.run()
|
||||
recorder.record_bus(phase, bus)
|
||||
finally:
|
||||
await pool.close()
|
||||
return {
|
||||
"result": result,
|
||||
"contexts_created": pool.contexts_created,
|
||||
"contexts_closed": pool.contexts_closed,
|
||||
"chromium_version": browser_version,
|
||||
}
|
||||
|
||||
|
||||
async def run_serial_phase(
|
||||
sites: List[Website],
|
||||
target: str,
|
||||
timeout: float,
|
||||
recorder: ReceiptRecorder,
|
||||
) -> Dict[str, Any]:
|
||||
pool = BrowserPool(headless=True)
|
||||
await pool.start()
|
||||
browser_version = pool.browser.version if pool.browser else None
|
||||
try:
|
||||
result = await run_sequential(
|
||||
sites,
|
||||
target,
|
||||
pool,
|
||||
timeout,
|
||||
browser_receipt_sink=recorder.record_browser,
|
||||
llm_receipt_sink=recorder.record_llm,
|
||||
run_phase="default_serial",
|
||||
)
|
||||
finally:
|
||||
await pool.close()
|
||||
return {
|
||||
"result": result,
|
||||
"contexts_created": pool.contexts_created,
|
||||
"contexts_closed": pool.contexts_closed,
|
||||
"chromium_version": browser_version,
|
||||
}
|
||||
|
||||
|
||||
def gate(status: bool, **details: Any) -> Dict[str, Any]:
|
||||
return {"status": "pass" if status else "fail", **details}
|
||||
|
||||
|
||||
def find_credential_hits(payloads: Iterable[bytes]) -> Dict[str, int]:
|
||||
blobs = list(payloads)
|
||||
actual_secret_hits = 0
|
||||
for name in SECRET_ENV_NAMES:
|
||||
secret = os.getenv(name, "").encode("utf-8")
|
||||
if len(secret) >= 8:
|
||||
actual_secret_hits += sum(blob.count(secret) for blob in blobs)
|
||||
|
||||
generic_patterns = (
|
||||
re.compile(rb'(?i)"(?:api[_-]?key|authorization)"\s*:\s*"(?!<redacted>|null|")[^"]+"'),
|
||||
re.compile(rb'(?i)bearer\s+[a-z0-9._~+/=-]{16,}'),
|
||||
)
|
||||
pattern_hits = sum(len(pattern.findall(blob)) for pattern in generic_patterns for blob in blobs)
|
||||
return {"actual_secret_hits": actual_secret_hits, "credential_pattern_hits": pattern_hits}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--target", default=TARGET)
|
||||
parser.add_argument("--timeout", type=float, default=120.0)
|
||||
parser.add_argument("--run-id", help="immutable validation/runs directory name")
|
||||
parser.add_argument("--output-root", default=str(ROOT / "validation" / "runs"))
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace) -> int:
|
||||
run_id = args.run_id or f"exp10-4-real-receipts-{datetime.now(timezone.utc):%Y%m%dT%H%M%SZ}"
|
||||
run_dir = Path(args.output_root).resolve() / run_id
|
||||
run_dir.mkdir(parents=True, exist_ok=False)
|
||||
started_at = utc_now()
|
||||
started_monotonic = time.monotonic()
|
||||
|
||||
source_hashes = {
|
||||
path: sha256_file(ROOT / path)
|
||||
for path in SOURCE_FILES
|
||||
}
|
||||
default_sites = load_sites(None)
|
||||
cascade_sites = load_sites(str(ROOT / "cascade-stress.example.json"))
|
||||
recorder = ReceiptRecorder()
|
||||
|
||||
default_parallel = await run_parallel_phase(
|
||||
default_sites, args.target, args.timeout, "default_parallel", recorder
|
||||
)
|
||||
default_serial = await run_serial_phase(
|
||||
default_sites, args.target, args.timeout, recorder
|
||||
)
|
||||
cascade = await run_parallel_phase(
|
||||
cascade_sites, args.target, args.timeout, "cascade_stress", recorder
|
||||
)
|
||||
|
||||
browser_path = run_dir / "browser_receipts.json"
|
||||
llm_path = run_dir / "llm_receipts.json"
|
||||
bus_path = run_dir / "message_bus_receipts.json"
|
||||
write_json(browser_path, {"schema_version": 1, "receipts": recorder.browser})
|
||||
write_json(llm_path, {"schema_version": 1, "receipts": recorder.llm})
|
||||
write_json(bus_path, {"schema_version": 1, "receipts": recorder.bus})
|
||||
|
||||
parallel_result = default_parallel["result"]
|
||||
serial_result = default_serial["result"]
|
||||
cascade_result = cascade["result"]
|
||||
speedup = (
|
||||
round(serial_result["seconds"] / parallel_result["parallel_seconds"], 3)
|
||||
if parallel_result["parallel_seconds"]
|
||||
else None
|
||||
)
|
||||
successful_llm = [r for r in recorder.llm if r["kind"] == "llm_chat_completion"]
|
||||
phases_with_browser_receipts = sorted({r["phase"] for r in recorder.browser})
|
||||
phases_with_llm_receipts = sorted({r["context"]["phase"] for r in successful_llm})
|
||||
cascade_expected_acks = set(cascade_result["expected_loser_acks"])
|
||||
cascade_actual_acks = set(cascade_result["acks"])
|
||||
|
||||
credential_scan = find_credential_hits(
|
||||
[browser_path.read_bytes(), llm_path.read_bytes(), bus_path.read_bytes()]
|
||||
)
|
||||
gates = {
|
||||
"ten_real_default_sites": gate(
|
||||
len(default_sites) == 10 and all(s.url.startswith("https://") for s in default_sites),
|
||||
count=len(default_sites),
|
||||
),
|
||||
"same_sites_parallel_and_serial": gate(
|
||||
serial_result["visited"] == len(default_sites),
|
||||
configured_count=len(default_sites),
|
||||
serial_visited=serial_result["visited"],
|
||||
),
|
||||
"default_target_found_both_modes": gate(
|
||||
parallel_result["outcome"] == "found"
|
||||
and any(item.get("profile", {}).get("found") for item in serial_result["results"]),
|
||||
parallel_winner=parallel_result["winner"],
|
||||
),
|
||||
"default_resources_closed": gate(
|
||||
default_parallel["contexts_created"] == default_parallel["contexts_closed"] == len(default_sites)
|
||||
and default_serial["contexts_created"] == default_serial["contexts_closed"] == len(default_sites),
|
||||
parallel_created=default_parallel["contexts_created"],
|
||||
parallel_closed=default_parallel["contexts_closed"],
|
||||
serial_created=default_serial["contexts_created"],
|
||||
serial_closed=default_serial["contexts_closed"],
|
||||
),
|
||||
"measured_parallel_speedup": gate(speedup is not None and speedup > 1, speedup=speedup),
|
||||
"raw_browser_receipts": gate(
|
||||
len(recorder.browser) >= len(default_sites)
|
||||
and {"default_parallel", "default_serial", "cascade_stress"}.issubset(phases_with_browser_receipts),
|
||||
count=len(recorder.browser),
|
||||
phases=phases_with_browser_receipts,
|
||||
),
|
||||
"raw_llm_provider_receipts": gate(
|
||||
len(successful_llm) >= 3
|
||||
and all(r.get("response_id") and r.get("response") for r in successful_llm)
|
||||
and {"default_parallel", "default_serial", "cascade_stress"}.issubset(phases_with_llm_receipts),
|
||||
successful_count=len(successful_llm),
|
||||
response_ids=[r.get("response_id") for r in successful_llm],
|
||||
phases=phases_with_llm_receipts,
|
||||
),
|
||||
"single_cascade_settlement": gate(
|
||||
cascade_result["winner"] is not None
|
||||
and cascade_result["terminate_broadcasts"] == 1
|
||||
and not cascade_result["duplicate_hits"],
|
||||
winner=cascade_result["winner"],
|
||||
terminate_broadcasts=cascade_result["terminate_broadcasts"],
|
||||
duplicate_hits=cascade_result["duplicate_hits"],
|
||||
),
|
||||
"cascade_loser_acknowledgements": gate(
|
||||
cascade_expected_acks == cascade_actual_acks
|
||||
and not cascade_result["missing_loser_acks"],
|
||||
expected=sorted(cascade_expected_acks),
|
||||
actual=sorted(cascade_actual_acks),
|
||||
),
|
||||
"cascade_resources_closed": gate(
|
||||
cascade["contexts_created"] == cascade["contexts_closed"] == len(cascade_sites),
|
||||
created=cascade["contexts_created"],
|
||||
closed=cascade["contexts_closed"],
|
||||
),
|
||||
"runtime_source_hashes": gate(
|
||||
len(source_hashes) == len(SOURCE_FILES)
|
||||
and all(len(value) == 64 for value in source_hashes.values()),
|
||||
count=len(source_hashes),
|
||||
),
|
||||
"credential_free_artifacts": gate(
|
||||
credential_scan["actual_secret_hits"] == 0
|
||||
and credential_scan["credential_pattern_hits"] == 0,
|
||||
**credential_scan,
|
||||
),
|
||||
}
|
||||
overall_status = "pass" if all(item["status"] == "pass" for item in gates.values()) else "incomplete"
|
||||
|
||||
evidence = {
|
||||
"schema_version": 2,
|
||||
"experiment": "10-4",
|
||||
"run_id": run_id,
|
||||
"run_type": "real_parallel_serial_and_cascade_with_raw_receipts",
|
||||
"started_at": started_at,
|
||||
"completed_at": utc_now(),
|
||||
"duration_seconds": round(time.monotonic() - started_monotonic, 3),
|
||||
"target": args.target,
|
||||
"git_commit": git_commit(),
|
||||
"environment": {
|
||||
"python": sys.version.split()[0],
|
||||
"platform": platform.platform(),
|
||||
"playwright": importlib.metadata.version("playwright"),
|
||||
"parallel_chromium": default_parallel["chromium_version"],
|
||||
"serial_chromium": default_serial["chromium_version"],
|
||||
"cascade_chromium": cascade["chromium_version"],
|
||||
},
|
||||
"inputs": {
|
||||
"default_sites": [site.__dict__ for site in default_sites],
|
||||
"cascade_sites": [site.__dict__ for site in cascade_sites],
|
||||
"timeout_seconds": args.timeout,
|
||||
},
|
||||
"default_parallel": default_parallel,
|
||||
"default_serial": default_serial,
|
||||
"measured_speedup": speedup,
|
||||
"cascade_stress": cascade,
|
||||
"receipt_counts": {
|
||||
"browser": len(recorder.browser),
|
||||
"llm_all_attempts": len(recorder.llm),
|
||||
"llm_successful": len(successful_llm),
|
||||
"message_bus": len(recorder.bus),
|
||||
},
|
||||
"gates": gates,
|
||||
"overall_status": overall_status,
|
||||
}
|
||||
evidence_path = run_dir / "evidence.json"
|
||||
write_json(evidence_path, evidence)
|
||||
|
||||
artifact_paths = [evidence_path, browser_path, llm_path, bus_path]
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"experiment": "10-4",
|
||||
"run_id": run_id,
|
||||
"generated_at": utc_now(),
|
||||
"git_commit": evidence["git_commit"],
|
||||
"runtime_source_sha256": source_hashes,
|
||||
"input_sha256": {
|
||||
"default_sites_canonical_json": sha256_bytes(canonical_bytes(evidence["inputs"]["default_sites"])),
|
||||
"cascade_sites_canonical_json": sha256_bytes(canonical_bytes(evidence["inputs"]["cascade_sites"])),
|
||||
},
|
||||
"artifact_sha256": {
|
||||
path.name: sha256_file(path)
|
||||
for path in artifact_paths
|
||||
},
|
||||
"acceptance": {
|
||||
"overall_status": overall_status,
|
||||
"passed_gates": sum(item["status"] == "pass" for item in gates.values()),
|
||||
"total_gates": len(gates),
|
||||
},
|
||||
}
|
||||
manifest_path = run_dir / "manifest.json"
|
||||
write_json(manifest_path, manifest)
|
||||
|
||||
latest = ROOT / "validation" / "latest.json"
|
||||
write_json(latest, {
|
||||
"schema_version": 1,
|
||||
"run_id": run_id,
|
||||
"run_directory": str(run_dir.relative_to(ROOT)),
|
||||
"manifest_sha256": sha256_file(manifest_path),
|
||||
"overall_status": overall_status,
|
||||
})
|
||||
print(json.dumps({
|
||||
"run_id": run_id,
|
||||
"run_directory": str(run_dir),
|
||||
"overall_status": overall_status,
|
||||
"passed_gates": manifest["acceptance"]["passed_gates"],
|
||||
"total_gates": manifest["acceptance"]["total_gates"],
|
||||
"measured_speedup": speedup,
|
||||
"receipt_counts": evidence["receipt_counts"],
|
||||
}, ensure_ascii=False, indent=2))
|
||||
return 0 if overall_status == "pass" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(asyncio.run(main(parse_args())))
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"name": "law", "college": "Law School", "url": "https://law.stanford.edu/directory/?tax_and_terms=1067"},
|
||||
{"name": "education", "college": "Graduate School of Education", "url": "https://ed.stanford.edu/faculty"},
|
||||
{"name": "profile", "college": "Stanford Profiles", "url": "https://profiles.stanford.edu/andrew-ng"}
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Real university website inputs for Experiment 10-4."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Website:
|
||||
name: str
|
||||
college: str
|
||||
url: str
|
||||
|
||||
|
||||
TARGET = "Andrew Ng"
|
||||
|
||||
# All are real Stanford-owned web pages. They intentionally mix directories and
|
||||
# school/center profiles, as a user-supplied experiment should. URLs are data, not
|
||||
# mocked content; every worker launches an isolated Playwright browser context.
|
||||
DEFAULT_SITES: List[Website] = [
|
||||
Website("medicine-profiles", "School of Medicine", "https://med.stanford.edu/profiles/browse"),
|
||||
Website("law-faculty", "Stanford Law School", "https://law.stanford.edu/directory/?tax_and_terms=1067"),
|
||||
Website("education-faculty", "Graduate School of Education", "https://ed.stanford.edu/faculty"),
|
||||
Website("business-faculty", "Graduate School of Business", "https://www.gsb.stanford.edu/faculty-research/faculty"),
|
||||
Website("sustainability-faculty", "Doerr School of Sustainability", "https://sustainability.stanford.edu/people/faculty"),
|
||||
Website("humanities-faculty", "School of Humanities and Sciences", "https://humsci.stanford.edu/about/leadership-and-administration/deans-office"),
|
||||
Website("engineering-faculty", "School of Engineering", "https://engineering.stanford.edu/faculty-research/faculty"),
|
||||
Website("computer-science", "School of Engineering / Computer Science", "https://www.cs.stanford.edu/people/faculty"),
|
||||
Website("stanford-profiles", "Stanford Profiles", "https://profiles.stanford.edu/andrew-ng"),
|
||||
Website("human-ai", "Stanford HAI", "https://hai.stanford.edu/people/andrew-ng"),
|
||||
]
|
||||
|
||||
|
||||
def load_sites(path: str | None, limit: int | None = None) -> List[Website]:
|
||||
if path:
|
||||
raw = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
sites = [Website(**item) for item in raw]
|
||||
else:
|
||||
sites = list(DEFAULT_SITES)
|
||||
if limit is not None:
|
||||
sites = sites[:limit]
|
||||
if not sites:
|
||||
raise ValueError("网站列表不能为空")
|
||||
for site in sites:
|
||||
if not site.url.startswith(("http://", "https://")):
|
||||
raise ValueError(f"{site.name} 不是 HTTP(S) URL")
|
||||
return sites
|
||||
@@ -0,0 +1,82 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
from agents import Coordinator, TaskRecord
|
||||
from message_bus import MessageBus
|
||||
from sources import Website
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_worker_completing_before_winner_is_not_reported_as_missing_ack():
|
||||
"""Contract proved: Coordinator.run excludes workers that completed with not_found or errors prior to target settlement from expected_loser_acks.
|
||||
Bug locked out: falsely reporting self-completed workers as missing loser ACKs when a winner settles."""
|
||||
bus = MessageBus(verbose=False)
|
||||
coordinator = Coordinator(bus, "target")
|
||||
|
||||
site1 = Website("Site 1", "https://site1.edu", "College 1")
|
||||
site2 = Website("Site 2", "https://site2.edu", "College 2")
|
||||
site3 = Website("Site 3", "https://site3.edu", "College 3")
|
||||
|
||||
# Create fake worker objects for coordinator.workers
|
||||
class FakeWorker:
|
||||
def __init__(self, wid, site):
|
||||
self.id = wid
|
||||
self.site = site
|
||||
self.timeout = 10
|
||||
async def run(self):
|
||||
pass
|
||||
|
||||
workers = [
|
||||
FakeWorker("worker-1", site1),
|
||||
FakeWorker("worker-2", site2),
|
||||
FakeWorker("worker-3", site3),
|
||||
]
|
||||
|
||||
for w in workers:
|
||||
coordinator.add_worker(w)
|
||||
|
||||
# Worker 1 completed with not_found BEFORE settlement
|
||||
await bus.send("worker-1", "coordinator", "not_found", {"reason": "not found", "source": "Site 1"})
|
||||
await bus.send("worker-1", "coordinator", "resource_closed", {"browser_context_closed": True, "source": "Site 1"})
|
||||
|
||||
# Worker 2 found target (winner)
|
||||
await bus.send("worker-2", "coordinator", "target_found", {"data": {"found": True}, "source": "Site 2"})
|
||||
await bus.send("worker-2", "coordinator", "resource_closed", {"browser_context_closed": True, "source": "Site 2"})
|
||||
|
||||
# Worker 3 received terminate and acknowledged it
|
||||
await bus.send("worker-3", "coordinator", "ack", {"acked": "terminate", "source": "Site 3"})
|
||||
await bus.send("worker-3", "coordinator", "resource_closed", {"browser_context_closed": True, "source": "Site 3"})
|
||||
|
||||
result = await coordinator.run()
|
||||
|
||||
assert result["winner"] == "worker-2"
|
||||
assert result["expected_loser_acks"] == ["worker-3"]
|
||||
assert result["missing_loser_acks"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_worker_completing_after_winner_still_owes_ack():
|
||||
"""The expected ACK set is a snapshot taken when the winner settles."""
|
||||
bus = MessageBus(verbose=False)
|
||||
coordinator = Coordinator(bus, "target")
|
||||
|
||||
class FakeWorker:
|
||||
timeout = 10
|
||||
|
||||
def __init__(self, wid):
|
||||
self.id = wid
|
||||
self.site = Website(wid, f"https://{wid}.edu", wid)
|
||||
|
||||
async def run(self):
|
||||
pass
|
||||
|
||||
for worker_id in ("worker-1", "worker-2"):
|
||||
coordinator.add_worker(FakeWorker(worker_id))
|
||||
|
||||
await bus.send("worker-2", "coordinator", "target_found", {"data": {"found": True}})
|
||||
await bus.send("worker-1", "coordinator", "not_found", {"reason": "finished after settlement"})
|
||||
for worker_id in ("worker-1", "worker-2"):
|
||||
await bus.send(worker_id, "coordinator", "resource_closed", {"browser_context_closed": True})
|
||||
|
||||
result = await coordinator.run()
|
||||
|
||||
assert result["expected_loser_acks"] == ["worker-1"]
|
||||
assert result["missing_loser_acks"] == ["worker-1"]
|
||||
@@ -0,0 +1,151 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from agents import Coordinator
|
||||
from agents import WorkerAgent
|
||||
from message_bus import MessageBus
|
||||
from sources import DEFAULT_SITES, load_sites
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_near_simultaneous_hits_settle_and_broadcast_once():
|
||||
bus = MessageBus(verbose=False)
|
||||
coordinator = Coordinator(bus, "target")
|
||||
await asyncio.gather(
|
||||
coordinator._settle("agent-a", {"name": "target"}),
|
||||
coordinator._settle("agent-b", {"name": "target"}),
|
||||
)
|
||||
assert coordinator.winner in {"agent-a", "agent-b"}
|
||||
assert len(coordinator.duplicate_hits) == 1
|
||||
assert sum(m.type == "terminate" for m in bus.history) == 1
|
||||
|
||||
|
||||
def test_default_dataset_is_ten_real_http_university_pages():
|
||||
sites = load_sites(None)
|
||||
assert len(sites) == 10
|
||||
assert all(s.url.startswith("https://") for s in sites)
|
||||
assert all(not hasattr(s, "content") and not hasattr(s, "latency") for s in sites)
|
||||
|
||||
|
||||
class StubWorker:
|
||||
def __init__(self, worker_id, bus, events):
|
||||
self.id = worker_id
|
||||
self.site = SimpleNamespace(
|
||||
name=f"source-{worker_id}", url=f"https://example.test/{worker_id}"
|
||||
)
|
||||
self.bus = bus
|
||||
self.events = events
|
||||
self.timeout = 0.1
|
||||
self.sub = bus.subscribe(worker_id, types=["task_assigned", "terminate"])
|
||||
|
||||
async def run(self):
|
||||
assigned = await self.sub.get()
|
||||
assert assigned.type == "task_assigned"
|
||||
for event_type, payload in self.events:
|
||||
if event_type == "status_update":
|
||||
payload = {"source": self.site.name, **payload}
|
||||
else:
|
||||
payload = {**payload, "source": self.site.name}
|
||||
await self.bus.send(self.id, "coordinator", event_type, payload)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_all_not_found_has_no_cascade_and_returns_reason_and_status_aggregation():
|
||||
bus = MessageBus(verbose=False)
|
||||
coordinator = Coordinator(bus, "missing")
|
||||
for worker_id in ("agent-a", "agent-b"):
|
||||
coordinator.add_worker(StubWorker(worker_id, bus, [
|
||||
("status_update", {"state": "执行中", "note": "reading"}),
|
||||
("not_found", {"reason": "target absent"}),
|
||||
("status_update", {"state": "已完成", "note": "未找到目标"}),
|
||||
("resource_closed", {"browser_context_closed": True}),
|
||||
]))
|
||||
|
||||
result = await coordinator.run()
|
||||
|
||||
assert result["outcome"] == "not_found"
|
||||
assert result["winner"] is None
|
||||
assert result["terminate_broadcasts"] == 0
|
||||
assert result["not_found_reasons"] == {
|
||||
"agent-a": "target absent", "agent-b": "target absent",
|
||||
}
|
||||
assert all(row["state"] == "已完成" for row in result["status_table"].values())
|
||||
assert result["failure_summary"] == {"count": 0, "by_type": {}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_worker_failure_is_isolated_and_summarized_while_peer_completes():
|
||||
bus = MessageBus(verbose=False)
|
||||
coordinator = Coordinator(bus, "missing")
|
||||
coordinator.add_worker(StubWorker("bad", bus, [
|
||||
("worker_error", {"error": "TimeoutError: deadline"}),
|
||||
("status_update", {"state": "失败", "note": "timeout"}),
|
||||
("resource_closed", {"browser_context_closed": True}),
|
||||
]))
|
||||
coordinator.add_worker(StubWorker("good", bus, [
|
||||
("not_found", {"reason": "target absent"}),
|
||||
("status_update", {"state": "已完成", "note": "peer completed"}),
|
||||
("resource_closed", {"browser_context_closed": True}),
|
||||
]))
|
||||
|
||||
result = await coordinator.run()
|
||||
|
||||
assert result["outcome"] == "not_found"
|
||||
assert result["errors"] == {"bad": "TimeoutError: deadline"}
|
||||
assert result["not_found_reasons"] == {"good": "target absent"}
|
||||
assert result["failure_summary"] == {"count": 1, "by_type": {"TimeoutError": 1}}
|
||||
assert result["status_table"]["good"]["state"] == "已完成"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cancellation_closes_real_worker_context():
|
||||
class BlockingPage:
|
||||
async def goto(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def locator(self, _selector):
|
||||
return self
|
||||
|
||||
async def inner_text(self, **kwargs):
|
||||
await asyncio.Future()
|
||||
|
||||
class Context:
|
||||
def __init__(self):
|
||||
self.closed = False
|
||||
|
||||
async def new_page(self):
|
||||
return BlockingPage()
|
||||
|
||||
async def close(self):
|
||||
self.closed = True
|
||||
|
||||
class Pool:
|
||||
def __init__(self):
|
||||
self.context = Context()
|
||||
self.closed = 0
|
||||
|
||||
async def new_context(self):
|
||||
return self.context
|
||||
|
||||
async def mark_closed(self):
|
||||
self.closed += 1
|
||||
|
||||
bus = MessageBus(verbose=False)
|
||||
pool = Pool()
|
||||
site = SimpleNamespace(name="blocking", url="https://example.test")
|
||||
worker = WorkerAgent("agent-timeout", site, bus, "target", pool, timeout=0.01)
|
||||
coordinator_sub = bus.subscribe("coordinator", types=None)
|
||||
await bus.send("coordinator", worker.id, "task_assigned", {})
|
||||
|
||||
# The outer Manager deadline cancels the worker while body text is pending.
|
||||
await asyncio.wait_for(worker.run(), timeout=0.05)
|
||||
|
||||
assert pool.context.closed is True
|
||||
assert pool.closed == 1
|
||||
messages = []
|
||||
while not coordinator_sub.inbox.empty():
|
||||
messages.append(coordinator_sub.inbox.get_nowait())
|
||||
assert any(m.type == "worker_error" and "TimeoutError" in m.payload["error"] for m in messages)
|
||||
assert any(m.type == "resource_closed" and m.payload["browser_context_closed"] for m in messages)
|
||||
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).parent / "validation"
|
||||
|
||||
|
||||
def test_real_parallel_serial_evidence_closes_every_context_and_measures_speedup():
|
||||
data = json.loads((ROOT / "real_parallel_serial_2026-07-29.json").read_text())
|
||||
assert data["overall_status"] == "pass"
|
||||
assert data["parallel"]["contexts_created"] == data["parallel"]["contexts_closed"] == 10
|
||||
assert data["serial"]["contexts_created"] == data["serial"]["contexts_closed"] == 10
|
||||
assert data["parallel"]["errors"] == {}
|
||||
assert data["measured_speedup"] > 1
|
||||
|
||||
|
||||
def test_real_cascade_evidence_has_one_broadcast_all_acks_and_no_leaks():
|
||||
data = json.loads((ROOT / "real_cascade_2026-07-29.json").read_text())
|
||||
assert data["terminate_broadcasts"] == 1
|
||||
assert len(data["loser_acknowledgements"]) == data["workers"] - 1
|
||||
assert data["contexts_created"] == data["contexts_closed"]
|
||||
assert data["errors"] == {}
|
||||
@@ -0,0 +1,71 @@
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
RUN = ROOT / "validation" / "runs" / "exp10-4-real-receipts-20260730-v2"
|
||||
|
||||
|
||||
def sha256_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def canonical_bytes(value) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def test_official_manifest_binds_artifacts_and_runtime_sources():
|
||||
manifest = json.loads((RUN / "manifest.json").read_text(encoding="utf-8"))
|
||||
assert manifest["acceptance"] == {
|
||||
"overall_status": "pass",
|
||||
"passed_gates": 12,
|
||||
"total_gates": 12,
|
||||
}
|
||||
for name, expected in manifest["artifact_sha256"].items():
|
||||
assert sha256_bytes((RUN / name).read_bytes()) == expected
|
||||
for name, expected in manifest["runtime_source_sha256"].items():
|
||||
assert sha256_bytes((ROOT / name).read_bytes()) == expected
|
||||
|
||||
|
||||
def test_official_receipts_are_raw_hashed_and_cover_all_three_phases():
|
||||
browser = json.loads((RUN / "browser_receipts.json").read_text(encoding="utf-8"))["receipts"]
|
||||
llm = json.loads((RUN / "llm_receipts.json").read_text(encoding="utf-8"))["receipts"]
|
||||
successful = [item for item in llm if item["kind"] == "llm_chat_completion"]
|
||||
|
||||
assert len(browser) == 24
|
||||
assert {item["phase"] for item in browser} == {
|
||||
"default_parallel", "default_serial", "cascade_stress",
|
||||
}
|
||||
for item in browser:
|
||||
raw = item["rendered_body_text"].encode("utf-8")
|
||||
assert len(raw) == item["rendered_body_bytes"]
|
||||
assert sha256_bytes(raw) == item["rendered_body_sha256"]
|
||||
|
||||
assert len(successful) == 3
|
||||
assert len({item["response_id"] for item in successful}) == 3
|
||||
assert {item["context"]["phase"] for item in successful} == {
|
||||
"default_parallel", "default_serial", "cascade_stress",
|
||||
}
|
||||
for item in successful:
|
||||
assert item["response"]
|
||||
assert item["usage"]["total_tokens"] > 0
|
||||
assert sha256_bytes(canonical_bytes(item["request"])) == item["request_sha256"]
|
||||
assert sha256_bytes(canonical_bytes(item["response"])) == item["response_sha256"]
|
||||
|
||||
|
||||
def test_official_acceptance_and_latest_pointer_are_consistent_and_credential_free():
|
||||
evidence = json.loads((RUN / "evidence.json").read_text(encoding="utf-8"))
|
||||
latest = json.loads((ROOT / "validation" / "latest.json").read_text(encoding="utf-8"))
|
||||
assert evidence["overall_status"] == "pass"
|
||||
assert all(item["status"] == "pass" for item in evidence["gates"].values())
|
||||
assert evidence["measured_speedup"] > 1
|
||||
assert latest["run_id"] == evidence["run_id"]
|
||||
assert latest["manifest_sha256"] == sha256_bytes((RUN / "manifest.json").read_bytes())
|
||||
|
||||
combined = b"\n".join(path.read_bytes() for path in RUN.iterdir() if path.is_file())
|
||||
assert not re.search(rb'(?i)"(?:api[_-]?key|authorization)"\s*:\s*"(?!<redacted>|null|")[^"]+"', combined)
|
||||
assert not re.search(rb'(?i)bearer\s+[a-z0-9._~+/=-]{16,}', combined)
|
||||
@@ -0,0 +1,32 @@
|
||||
import asyncio
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure chapter10/parallel-web-research is in sys.path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from agents import WorkerAgent
|
||||
from message_bus import MessageBus, BROADCAST
|
||||
from sources import Website
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_worker_exits_early_when_terminate_arrives_before_task_assigned():
|
||||
"""Contract proved: WorkerAgent handles terminate broadcast received before task_assigned without hanging or ignoring the signal.
|
||||
Bug locked out: infinite loop awaiting task_assigned while ignoring terminate signal."""
|
||||
bus = MessageBus(verbose=False)
|
||||
site = Website("s1", "http://site1.edu", "College 1")
|
||||
w = WorkerAgent("worker-1", site, bus, "target", None)
|
||||
|
||||
# Broadcast terminate to bus BEFORE worker-1 gets task_assigned
|
||||
await bus.send("coordinator", BROADCAST, "terminate", {"reason": "target_found_by_other"})
|
||||
|
||||
# w.run() must exit promptly (not hang awaiting task_assigned), set terminate event, and send ACK
|
||||
await asyncio.wait_for(w.run(), timeout=2.0)
|
||||
|
||||
assert w.terminate.is_set()
|
||||
assert w._termination_reason == "target_found_by_other"
|
||||
acks = [m for m in bus.history if m.type == "ack" and m.sender_id == "worker-1"]
|
||||
assert len(acks) == 1
|
||||
assert acks[0].payload.get("acked") == "terminate"
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"run_id": "exp10-4-real-receipts-20260730-v2",
|
||||
"run_directory": "validation/runs/exp10-4-real-receipts-20260730-v2",
|
||||
"manifest_sha256": "da70764d5917f999a14d52f10cb147ce53c8c378853dd8dbc947b8d97bbfbc97",
|
||||
"overall_status": "pass"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"experiment": "10-4",
|
||||
"run_type": "real_browser_cascade_stress",
|
||||
"measured_at": "2026-07-29T17:58:19+0800",
|
||||
"target": "Andrew Ng",
|
||||
"browser": "Playwright Chromium",
|
||||
"llm_provider": "Volcengine ARK",
|
||||
"source": "live Stanford HAI profile repeated with distinct query URLs to expose cancellation timing",
|
||||
"workers": 4,
|
||||
"winner": "agent-03",
|
||||
"winner_url": "https://hai.stanford.edu/people/andrew-ng?worker=d",
|
||||
"parallel_seconds": 16.313,
|
||||
"terminate_broadcasts": 1,
|
||||
"loser_acknowledgements": ["agent-00", "agent-01", "agent-02"],
|
||||
"duplicate_hits": [],
|
||||
"errors": {},
|
||||
"contexts_created": 4,
|
||||
"contexts_closed": 4,
|
||||
"gates": {
|
||||
"real_browser_sessions": {"status": "pass", "count": 4},
|
||||
"single_locked_settlement": {"status": "pass"},
|
||||
"single_terminate_broadcast": {"status": "pass", "count": 1},
|
||||
"all_losing_workers_acknowledged": {"status": "pass", "count": 3},
|
||||
"all_browser_resources_closed": {"status": "pass", "created": 4, "closed": 4}
|
||||
},
|
||||
"overall_status": "pass"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"experiment": "10-4",
|
||||
"run_type": "real_parallel_and_serial_same_sites",
|
||||
"measured_at": "2026-07-29T17:57:11+0800",
|
||||
"target": "Andrew Ng",
|
||||
"browser": "Playwright Chromium",
|
||||
"llm_provider": "Volcengine ARK",
|
||||
"llm_model": "doubao-seed-1-6-250615",
|
||||
"sites": [
|
||||
"https://med.stanford.edu/profiles/browse",
|
||||
"https://law.stanford.edu/directory/?tax_and_terms=1067",
|
||||
"https://ed.stanford.edu/faculty",
|
||||
"https://www.gsb.stanford.edu/faculty-research/faculty",
|
||||
"https://sustainability.stanford.edu/people/faculty",
|
||||
"https://humsci.stanford.edu/about/leadership-and-administration/deans-office",
|
||||
"https://engineering.stanford.edu/faculty-research/faculty",
|
||||
"https://www.cs.stanford.edu/people/faculty",
|
||||
"https://profiles.stanford.edu/andrew-ng",
|
||||
"https://hai.stanford.edu/people/andrew-ng"
|
||||
],
|
||||
"parallel": {
|
||||
"seconds": 18.542,
|
||||
"winner": "agent-09",
|
||||
"errors": {},
|
||||
"contexts_created": 10,
|
||||
"contexts_closed": 10,
|
||||
"terminate_broadcasts": 1,
|
||||
"result": {
|
||||
"name": "Andrew Ng",
|
||||
"college": "Stanford HAI",
|
||||
"position": "Adjunct Professor at Stanford University",
|
||||
"research": "machine learning with an emphasis on deep learning, applications to computer vision and speech, autonomous driving",
|
||||
"evidence": "Founder of DeepLearning.AI and Adjunct Professor at Stanford University",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng"
|
||||
}
|
||||
},
|
||||
"serial": {
|
||||
"seconds": 58.264,
|
||||
"sites_visited": 10,
|
||||
"contexts_created": 10,
|
||||
"contexts_closed": 10,
|
||||
"winner_site": "human-ai"
|
||||
},
|
||||
"measured_speedup": 3.142,
|
||||
"gates": {
|
||||
"real_websites": {"status": "pass", "count": 10},
|
||||
"independent_browser_sessions": {"status": "pass", "count": 10},
|
||||
"real_llm_grounded_extraction": {"status": "pass"},
|
||||
"parallel_serial_same_site_comparison": {"status": "pass"},
|
||||
"all_browser_resources_closed": {"status": "pass", "created": 20, "closed": 20},
|
||||
"measured_parallel_improvement": {"status": "pass", "speedup": 3.142}
|
||||
},
|
||||
"overall_status": "pass"
|
||||
}
|
||||
+341
File diff suppressed because one or more lines are too long
+461
@@ -0,0 +1,461 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"experiment": "10-4",
|
||||
"run_id": "exp10-4-real-receipts-20260730-v2",
|
||||
"run_type": "real_parallel_serial_and_cascade_with_raw_receipts",
|
||||
"started_at": "2026-07-30T05:08:35.893Z",
|
||||
"completed_at": "2026-07-30T05:09:57.065Z",
|
||||
"duration_seconds": 81.172,
|
||||
"target": "Andrew Ng",
|
||||
"git_commit": "f119cf510f3746cd5d071d0bce0051617f02f163",
|
||||
"environment": {
|
||||
"python": "3.11.4",
|
||||
"platform": "macOS-26.3-arm64-arm-64bit",
|
||||
"playwright": "1.54.0",
|
||||
"parallel_chromium": "139.0.7258.5",
|
||||
"serial_chromium": "139.0.7258.5",
|
||||
"cascade_chromium": "139.0.7258.5"
|
||||
},
|
||||
"inputs": {
|
||||
"default_sites": [
|
||||
{
|
||||
"name": "medicine-profiles",
|
||||
"college": "School of Medicine",
|
||||
"url": "https://med.stanford.edu/profiles/browse"
|
||||
},
|
||||
{
|
||||
"name": "law-faculty",
|
||||
"college": "Stanford Law School",
|
||||
"url": "https://law.stanford.edu/directory/?tax_and_terms=1067"
|
||||
},
|
||||
{
|
||||
"name": "education-faculty",
|
||||
"college": "Graduate School of Education",
|
||||
"url": "https://ed.stanford.edu/faculty"
|
||||
},
|
||||
{
|
||||
"name": "business-faculty",
|
||||
"college": "Graduate School of Business",
|
||||
"url": "https://www.gsb.stanford.edu/faculty-research/faculty"
|
||||
},
|
||||
{
|
||||
"name": "sustainability-faculty",
|
||||
"college": "Doerr School of Sustainability",
|
||||
"url": "https://sustainability.stanford.edu/people/faculty"
|
||||
},
|
||||
{
|
||||
"name": "humanities-faculty",
|
||||
"college": "School of Humanities and Sciences",
|
||||
"url": "https://humsci.stanford.edu/about/leadership-and-administration/deans-office"
|
||||
},
|
||||
{
|
||||
"name": "engineering-faculty",
|
||||
"college": "School of Engineering",
|
||||
"url": "https://engineering.stanford.edu/faculty-research/faculty"
|
||||
},
|
||||
{
|
||||
"name": "computer-science",
|
||||
"college": "School of Engineering / Computer Science",
|
||||
"url": "https://www.cs.stanford.edu/people/faculty"
|
||||
},
|
||||
{
|
||||
"name": "stanford-profiles",
|
||||
"college": "Stanford Profiles",
|
||||
"url": "https://profiles.stanford.edu/andrew-ng"
|
||||
},
|
||||
{
|
||||
"name": "human-ai",
|
||||
"college": "Stanford HAI",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng"
|
||||
}
|
||||
],
|
||||
"cascade_sites": [
|
||||
{
|
||||
"name": "hai-a",
|
||||
"college": "Stanford HAI",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng?worker=a"
|
||||
},
|
||||
{
|
||||
"name": "hai-b",
|
||||
"college": "Stanford HAI",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng?worker=b"
|
||||
},
|
||||
{
|
||||
"name": "hai-c",
|
||||
"college": "Stanford HAI",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng?worker=c"
|
||||
},
|
||||
{
|
||||
"name": "hai-d",
|
||||
"college": "Stanford HAI",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng?worker=d"
|
||||
}
|
||||
],
|
||||
"timeout_seconds": 120.0
|
||||
},
|
||||
"default_parallel": {
|
||||
"result": {
|
||||
"outcome": "found",
|
||||
"winner": "agent-09",
|
||||
"profile": {
|
||||
"found": true,
|
||||
"name": "Andrew Ng",
|
||||
"college": "Stanford HAI",
|
||||
"position": "Adjunct Professor at Stanford University",
|
||||
"research": "machine learning with an emphasis on deep learning. He founded and led the “Google Brain” project which developed massive-scale deep learning algorithms... including such applications as autonomous driving",
|
||||
"evidence": "PEOPLE\nFACULTY\nAndrew Ng\n\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University",
|
||||
"provider": "ark",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng"
|
||||
},
|
||||
"duplicate_hits": [],
|
||||
"acks": [],
|
||||
"expected_loser_acks": [
|
||||
"agent-00",
|
||||
"agent-01",
|
||||
"agent-02",
|
||||
"agent-03",
|
||||
"agent-04",
|
||||
"agent-05",
|
||||
"agent-06",
|
||||
"agent-07",
|
||||
"agent-08"
|
||||
],
|
||||
"missing_loser_acks": [
|
||||
"agent-00",
|
||||
"agent-01",
|
||||
"agent-02",
|
||||
"agent-03",
|
||||
"agent-04",
|
||||
"agent-05",
|
||||
"agent-06",
|
||||
"agent-07",
|
||||
"agent-08"
|
||||
],
|
||||
"errors": {},
|
||||
"failure_summary": {
|
||||
"count": 0,
|
||||
"by_type": {}
|
||||
},
|
||||
"not_found_reasons": {
|
||||
"agent-07": "target name absent from rendered page",
|
||||
"agent-06": "target name absent from rendered page",
|
||||
"agent-02": "target name absent from rendered page",
|
||||
"agent-00": "target name absent from rendered page",
|
||||
"agent-04": "target name absent from rendered page",
|
||||
"agent-03": "target name absent from rendered page",
|
||||
"agent-08": "target name absent from rendered page",
|
||||
"agent-05": "target name absent from rendered page",
|
||||
"agent-01": "target name absent from rendered page"
|
||||
},
|
||||
"status_table": {
|
||||
"agent-00": {
|
||||
"source": "medicine-profiles",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-01": {
|
||||
"source": "law-faculty",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-02": {
|
||||
"source": "education-faculty",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-03": {
|
||||
"source": "business-faculty",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-04": {
|
||||
"source": "sustainability-faculty",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-05": {
|
||||
"source": "humanities-faculty",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-06": {
|
||||
"source": "engineering-faculty",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-07": {
|
||||
"source": "computer-science",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-08": {
|
||||
"source": "stanford-profiles",
|
||||
"state": "已完成",
|
||||
"note": "页面中未找到目标"
|
||||
},
|
||||
"agent-09": {
|
||||
"source": "human-ai",
|
||||
"state": "已完成",
|
||||
"note": "找到目标教师"
|
||||
}
|
||||
},
|
||||
"terminate_broadcasts": 1,
|
||||
"parallel_seconds": 24.022,
|
||||
"contexts_closed": 10,
|
||||
"resource_failures": {}
|
||||
},
|
||||
"contexts_created": 10,
|
||||
"contexts_closed": 10,
|
||||
"chromium_version": "139.0.7258.5"
|
||||
},
|
||||
"default_serial": {
|
||||
"result": {
|
||||
"seconds": 44.971,
|
||||
"visited": 10,
|
||||
"results": [
|
||||
{
|
||||
"site": "medicine-profiles",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 3.14453391591087
|
||||
},
|
||||
{
|
||||
"site": "law-faculty",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 3.242926917038858
|
||||
},
|
||||
{
|
||||
"site": "education-faculty",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 2.353850208222866
|
||||
},
|
||||
{
|
||||
"site": "business-faculty",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 2.5877689169719815
|
||||
},
|
||||
{
|
||||
"site": "sustainability-faculty",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 2.4759700000286102
|
||||
},
|
||||
{
|
||||
"site": "humanities-faculty",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 2.9777685422450304
|
||||
},
|
||||
{
|
||||
"site": "engineering-faculty",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 2.225629042368382
|
||||
},
|
||||
{
|
||||
"site": "computer-science",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 2.5532497079111636
|
||||
},
|
||||
{
|
||||
"site": "stanford-profiles",
|
||||
"profile": {
|
||||
"found": false,
|
||||
"reason": "target name absent from rendered page"
|
||||
},
|
||||
"seconds": 2.9755149162374437
|
||||
},
|
||||
{
|
||||
"site": "human-ai",
|
||||
"profile": {
|
||||
"found": true,
|
||||
"name": "Andrew Ng",
|
||||
"college": "Stanford HAI",
|
||||
"position": "Adjunct Professor at Stanford University",
|
||||
"research": "machine learning with an emphasis on deep learning, including applications to computer vision, speech, and autonomous driving",
|
||||
"evidence": "Andrew Ng\n\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University",
|
||||
"provider": "ark",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng"
|
||||
},
|
||||
"seconds": 20.37298774998635
|
||||
}
|
||||
]
|
||||
},
|
||||
"contexts_created": 10,
|
||||
"contexts_closed": 10,
|
||||
"chromium_version": "139.0.7258.5"
|
||||
},
|
||||
"measured_speedup": 1.872,
|
||||
"cascade_stress": {
|
||||
"result": {
|
||||
"outcome": "found",
|
||||
"winner": "agent-02",
|
||||
"profile": {
|
||||
"found": true,
|
||||
"name": "Andrew Ng",
|
||||
"college": "Stanford HAI",
|
||||
"position": "Adjunct Professor at Stanford University",
|
||||
"research": "machine learning with an emphasis on deep learning, applications to computer vision and speech, including autonomous driving",
|
||||
"evidence": "Andrew Ng\n\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University",
|
||||
"provider": "ark",
|
||||
"url": "https://hai.stanford.edu/people/andrew-ng?worker=c"
|
||||
},
|
||||
"duplicate_hits": [],
|
||||
"acks": [
|
||||
"agent-00",
|
||||
"agent-01",
|
||||
"agent-03"
|
||||
],
|
||||
"expected_loser_acks": [
|
||||
"agent-00",
|
||||
"agent-01",
|
||||
"agent-03"
|
||||
],
|
||||
"missing_loser_acks": [],
|
||||
"errors": {},
|
||||
"failure_summary": {
|
||||
"count": 0,
|
||||
"by_type": {}
|
||||
},
|
||||
"not_found_reasons": {},
|
||||
"status_table": {
|
||||
"agent-00": {
|
||||
"source": "hai-a",
|
||||
"state": "已终止",
|
||||
"note": "安全点响应终止:target_found_by_agent-02"
|
||||
},
|
||||
"agent-01": {
|
||||
"source": "hai-b",
|
||||
"state": "已终止",
|
||||
"note": "安全点响应终止:target_found_by_agent-02"
|
||||
},
|
||||
"agent-02": {
|
||||
"source": "hai-c",
|
||||
"state": "已完成",
|
||||
"note": "找到目标教师"
|
||||
},
|
||||
"agent-03": {
|
||||
"source": "hai-d",
|
||||
"state": "已终止",
|
||||
"note": "安全点响应终止:target_found_by_agent-02"
|
||||
}
|
||||
},
|
||||
"terminate_broadcasts": 1,
|
||||
"parallel_seconds": 10.878,
|
||||
"contexts_closed": 4,
|
||||
"resource_failures": {}
|
||||
},
|
||||
"contexts_created": 4,
|
||||
"contexts_closed": 4,
|
||||
"chromium_version": "139.0.7258.5"
|
||||
},
|
||||
"receipt_counts": {
|
||||
"browser": 24,
|
||||
"llm_all_attempts": 3,
|
||||
"llm_successful": 3,
|
||||
"message_bus": 114
|
||||
},
|
||||
"gates": {
|
||||
"ten_real_default_sites": {
|
||||
"status": "pass",
|
||||
"count": 10
|
||||
},
|
||||
"same_sites_parallel_and_serial": {
|
||||
"status": "pass",
|
||||
"configured_count": 10,
|
||||
"serial_visited": 10
|
||||
},
|
||||
"default_target_found_both_modes": {
|
||||
"status": "pass",
|
||||
"parallel_winner": "agent-09"
|
||||
},
|
||||
"default_resources_closed": {
|
||||
"status": "pass",
|
||||
"parallel_created": 10,
|
||||
"parallel_closed": 10,
|
||||
"serial_created": 10,
|
||||
"serial_closed": 10
|
||||
},
|
||||
"measured_parallel_speedup": {
|
||||
"status": "pass",
|
||||
"speedup": 1.872
|
||||
},
|
||||
"raw_browser_receipts": {
|
||||
"status": "pass",
|
||||
"count": 24,
|
||||
"phases": [
|
||||
"cascade_stress",
|
||||
"default_parallel",
|
||||
"default_serial"
|
||||
]
|
||||
},
|
||||
"raw_llm_provider_receipts": {
|
||||
"status": "pass",
|
||||
"successful_count": 3,
|
||||
"response_ids": [
|
||||
"021785388119659a5cf16cfa3c4ebff37e7c43914719d1a235950",
|
||||
"021785388168110fc13f77dd38da39bcdb33ad9bd57064028fde0",
|
||||
"0217853881892313749365b55318faa75db47eb4778acb39034a4"
|
||||
],
|
||||
"phases": [
|
||||
"cascade_stress",
|
||||
"default_parallel",
|
||||
"default_serial"
|
||||
]
|
||||
},
|
||||
"single_cascade_settlement": {
|
||||
"status": "pass",
|
||||
"winner": "agent-02",
|
||||
"terminate_broadcasts": 1,
|
||||
"duplicate_hits": []
|
||||
},
|
||||
"cascade_loser_acknowledgements": {
|
||||
"status": "pass",
|
||||
"expected": [
|
||||
"agent-00",
|
||||
"agent-01",
|
||||
"agent-03"
|
||||
],
|
||||
"actual": [
|
||||
"agent-00",
|
||||
"agent-01",
|
||||
"agent-03"
|
||||
]
|
||||
},
|
||||
"cascade_resources_closed": {
|
||||
"status": "pass",
|
||||
"created": 4,
|
||||
"closed": 4
|
||||
},
|
||||
"runtime_source_hashes": {
|
||||
"status": "pass",
|
||||
"count": 7
|
||||
},
|
||||
"credential_free_artifacts": {
|
||||
"status": "pass",
|
||||
"actual_secret_hits": 0,
|
||||
"credential_pattern_hits": 0
|
||||
}
|
||||
},
|
||||
"overall_status": "pass"
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"receipts": [
|
||||
{
|
||||
"kind": "llm_chat_completion",
|
||||
"context": {
|
||||
"phase": "default_parallel",
|
||||
"worker_id": "agent-09",
|
||||
"site": "human-ai"
|
||||
},
|
||||
"provider": "ark",
|
||||
"request": {
|
||||
"model": "doubao-seed-1-6-250615",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "{\"target\": \"Andrew Ng\", \"site_college\": \"Stanford HAI\", \"url\": \"https://hai.stanford.edu/people/andrew-ng\", \"rendered_page_text\": \"Skip to content\\nAbout\\nResearch\\nEducation\\nPolicy\\nAI Index\\nNews\\nEvents\\nIndustry\\nCenters & Labs\\nPEOPLE\\nFACULTY\\nAndrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\\n\\nAndrew Ng is Founder & CEO of Landing AI, Founder of deeplearning.ai, Co-Chairman and Co-Founder of Coursera, and is currently an Adjunct Professor at Stanford University. He was also Chief Scientist at Baidu Inc., and Founder & Lead for the Google Brain Project.\\n\\nIn 2011 he led the development of Stanford University’s main MOOC (Massive Open Online Courses) platform and also taught an online Machine Learning class to over 100,000 students, leading to the founding of Coursera. Ng’s goal is to give everyone in the world access to a great education, for free. Today, Coursera partners with some of the top universities in the world to offer high quality online courses, and is the largest MOOC platform in the world.\\n\\nNg also works on machine learning with an emphasis on deep learning. He founded and led the “Google Brain” project which developed massive-scale deep learning algorithms. This resulted in the famous “Google cat” result, in which a massive neural network with 1 billion parameters learned from unlabeled YouTube videos to detect cats. More recently, he continues to work on deep learning and its applications to computer vision and speech, including such applications as autonomous driving.\\n\\nSHARE\\nNAVIGATE\\nAbout\\nEvents\\nAI Glossary\\nCareers\\nSearch\\nPARTICIPATE\\nGet Involved\\nSupport HAI\\nContact Us\\nStay Up To Date\\n\\nGet the latest news, advances in research, policy work, and education program updates from HAI in your inbox weekly.\\n\\nSign Up For Latest News\\n\\nStanford\\nUniversity\\nStanford HomeMaps & DirectionsSearch StanfordEmergency Info\\nTerms of UsePrivacyCopyrightTrademarksNon-DiscriminationAccessibility\\n© Stanford University. Stanford, California 94305.\\nPause Media\", \"instruction\": \"Use only rendered_page_text. Decide whether it contains this exact person's faculty profile. Return JSON keys found, name, college, position, research, evidence. If the name is only a link/listing, found may be true but leave unsupported fields empty. evidence must be a short verbatim excerpt.\"}"
|
||||
}
|
||||
],
|
||||
"response_format": {
|
||||
"type": "json_object"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"id": "021785388119659a5cf16cfa3c4ebff37e7c43914719d1a235950",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"logprobs": null,
|
||||
"message": {
|
||||
"content": "{\"found\": true, \"name\": \"Andrew Ng\", \"college\": \"Stanford HAI\", \"position\": \"Adjunct Professor at Stanford University\", \"research\": \"machine learning with an emphasis on deep learning. He founded and led the “Google Brain” project which developed massive-scale deep learning algorithms... including such applications as autonomous driving\", \"evidence\": \"PEOPLE\\nFACULTY\\nAndrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\"}",
|
||||
"refusal": null,
|
||||
"role": "assistant",
|
||||
"annotations": null,
|
||||
"audio": null,
|
||||
"function_call": null,
|
||||
"tool_calls": null,
|
||||
"reasoning_content": "\nI now need to solve this problem. Let's start by carefully reading the user's instruction. The task is to use only the provided rendered_page_text to determine if it contains the exact person's faculty profile. Then, return a JSON with keys: found, name, college, position, research, evidence. \n\nFirst, the target person is Andrew Ng, and the site_college is Stanford HAI. Let's look at the rendered_page_text. \n\nLooking through the text, there's a section: \"PEOPLE\\nFACULTY\\nAndrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\\n\\nAndrew Ng is Founder & CEO of Landing AI, Founder of deeplearning.ai, Co-Chairman and Co-Founder of Coursera, and is currently an Adjunct Professor at Stanford University...\" \n\nSo, the name is clearly Andrew Ng. Now, check if it's a faculty profile. The text under \"PEOPLE\" and \"FACULTY\" lists Andrew Ng, so that's a faculty profile. \n\nFound: True, because it's a faculty profile for Andrew Ng.\n\nName: Andrew Ng (explicitly stated).\n\nCollege: The site_college is Stanford HAI, and the page is from Stanford HAI (url is https://hai.stanford.edu/people/andrew-ng), so the college should be Stanford HAI.\n\nPosition: The text says \"Adjunct Professor at Stanford University\".\n\nResearch: The text mentions \"Ng also works on machine learning with an emphasis on deep learning. He founded and led the “Google Brain” project which developed massive-scale deep learning algorithms... including such applications as autonomous driving.\" So research is machine learning with an emphasis on deep learning, including applications like autonomous driving.\n\nEvidence: Need a short verbatim excerpt. The line \"Andrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\" is a good excerpt as it directly links the name and position.\n\nNow, check if all fields are supported. The name is present, college is Stanford HAI (since the page is from Stanford HAI), position is Adjunct Professor at Stanford University, research is mentioned, evidence is available.\n\nSo putting it all together:\n\nfound: true\n\nname: \"Andrew Ng\"\n\ncollege: \"Stanford HAI\"\n\nposition: \"Adjunct Professor at Stanford University\"\n\nresearch: \"machine learning with an emphasis on deep learning, including applications such as autonomous driving\"\n\nevidence: \"Andrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\"\n\nWait, but need to make sure research is directly from the text. The exact research part is \"machine learning with an emphasis on deep learning. He founded and led the “Google Brain” project which developed massive-scale deep learning algorithms... including such applications as autonomous driving.\" So maybe more concise: \"machine learning with an emphasis on deep learning, including applications such as autonomous driving\" (verbatim parts: \"machine learning with an emphasis on deep learning\" and \"including such applications as autonomous driving\").\n\nEvidence should be verbatim. Let's pick the excerpt where it starts with \"Andrew Ng\" under FACULTY. The text has: \"PEOPLE\\nFACULTY\\nAndrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\" – that's a verbatim excerpt.\n\nNow, check if any fields are unsupported. All fields have support from the text. So the JSON should have all these filled."
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1785388140,
|
||||
"model": "doubao-seed-1-6-250615",
|
||||
"object": "chat.completion",
|
||||
"service_tier": "default",
|
||||
"system_fingerprint": null,
|
||||
"usage": {
|
||||
"completion_tokens": 839,
|
||||
"prompt_tokens": 643,
|
||||
"total_tokens": 1482,
|
||||
"completion_tokens_details": {
|
||||
"accepted_prediction_tokens": null,
|
||||
"audio_tokens": null,
|
||||
"reasoning_tokens": 739,
|
||||
"rejected_prediction_tokens": null
|
||||
},
|
||||
"prompt_tokens_details": {
|
||||
"audio_tokens": null,
|
||||
"cached_tokens": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_id": "021785388119659a5cf16cfa3c4ebff37e7c43914719d1a235950",
|
||||
"response_model": "doubao-seed-1-6-250615",
|
||||
"usage": {
|
||||
"completion_tokens": 839,
|
||||
"prompt_tokens": 643,
|
||||
"total_tokens": 1482,
|
||||
"completion_tokens_details": {
|
||||
"accepted_prediction_tokens": null,
|
||||
"audio_tokens": null,
|
||||
"reasoning_tokens": 739,
|
||||
"rejected_prediction_tokens": null
|
||||
},
|
||||
"prompt_tokens_details": {
|
||||
"audio_tokens": null,
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"duration_seconds": 21.299,
|
||||
"request_sha256": "685ffe4d54aa05d0ba113de8d9768265eb9168d74198810e28121f9e9cd5f749",
|
||||
"response_sha256": "399e874edaed8591ce68c7661f881afac2e097ad3d56e050cee3035c94c4df5f",
|
||||
"captured_at": "2026-07-30T05:09:00.593Z"
|
||||
},
|
||||
{
|
||||
"kind": "llm_chat_completion",
|
||||
"context": {
|
||||
"phase": "default_serial",
|
||||
"worker_id": "serial-09",
|
||||
"site": "human-ai"
|
||||
},
|
||||
"provider": "ark",
|
||||
"request": {
|
||||
"model": "doubao-seed-1-6-250615",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "{\"target\": \"Andrew Ng\", \"site_college\": \"Stanford HAI\", \"url\": \"https://hai.stanford.edu/people/andrew-ng\", \"rendered_page_text\": \"Skip to content\\nAbout\\nResearch\\nEducation\\nPolicy\\nAI Index\\nNews\\nEvents\\nIndustry\\nCenters & Labs\\nPEOPLE\\nFACULTY\\nAndrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\\n\\nAndrew Ng is Founder & CEO of Landing AI, Founder of deeplearning.ai, Co-Chairman and Co-Founder of Coursera, and is currently an Adjunct Professor at Stanford University. He was also Chief Scientist at Baidu Inc., and Founder & Lead for the Google Brain Project.\\n\\nIn 2011 he led the development of Stanford University’s main MOOC (Massive Open Online Courses) platform and also taught an online Machine Learning class to over 100,000 students, leading to the founding of Coursera. Ng’s goal is to give everyone in the world access to a great education, for free. Today, Coursera partners with some of the top universities in the world to offer high quality online courses, and is the largest MOOC platform in the world.\\n\\nNg also works on machine learning with an emphasis on deep learning. He founded and led the “Google Brain” project which developed massive-scale deep learning algorithms. This resulted in the famous “Google cat” result, in which a massive neural network with 1 billion parameters learned from unlabeled YouTube videos to detect cats. More recently, he continues to work on deep learning and its applications to computer vision and speech, including such applications as autonomous driving.\\n\\nSHARE\\nNAVIGATE\\nAbout\\nEvents\\nAI Glossary\\nCareers\\nSearch\\nPARTICIPATE\\nGet Involved\\nSupport HAI\\nContact Us\\nStay Up To Date\\n\\nGet the latest news, advances in research, policy work, and education program updates from HAI in your inbox weekly.\\n\\nSign Up For Latest News\\n\\nStanford\\nUniversity\\nStanford HomeMaps & DirectionsSearch StanfordEmergency Info\\nTerms of UsePrivacyCopyrightTrademarksNon-DiscriminationAccessibility\\n© Stanford University. Stanford, California 94305.\\nPause Media\", \"instruction\": \"Use only rendered_page_text. Decide whether it contains this exact person's faculty profile. Return JSON keys found, name, college, position, research, evidence. If the name is only a link/listing, found may be true but leave unsupported fields empty. evidence must be a short verbatim excerpt.\"}"
|
||||
}
|
||||
],
|
||||
"response_format": {
|
||||
"type": "json_object"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"id": "021785388168110fc13f77dd38da39bcdb33ad9bd57064028fde0",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"logprobs": null,
|
||||
"message": {
|
||||
"content": "{\"found\": true, \"name\": \"Andrew Ng\", \"college\": \"Stanford HAI\", \"position\": \"Adjunct Professor at Stanford University\", \"research\": \"machine learning with an emphasis on deep learning, including applications to computer vision, speech, and autonomous driving\", \"evidence\": \"Andrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\"}",
|
||||
"refusal": null,
|
||||
"role": "assistant",
|
||||
"annotations": null,
|
||||
"audio": null,
|
||||
"function_call": null,
|
||||
"tool_calls": null,
|
||||
"reasoning_content": "\nI now need to solve this problem. The user provided a rendered_page_text, and the task is to determine if it contains the exact faculty profile of Andrew Ng. Then, return JSON with keys found, name, college, position, research, evidence. Let's start step by step.\n\nFirst, check if the rendered_page_text has Andrew Ng's faculty profile. The target is Andrew Ng, site_college is Stanford HAI. Looking at the rendered_page_text, there's a section under \"PEOPLE\" and \"FACULTY\" with \"Andrew Ng\" as a heading. Then it says \"Founder of DeepLearning.AI and Adjunct Professor at Stanford University\". So that's a faculty profile.\n\nNow, the keys: found should be true. Name is Andrew Ng. College: the site_college is Stanford HAI, and the page is from Stanford HAI, so college is Stanford HAI. Position: \"Adjunct Professor at Stanford University\" is mentioned, so position is \"Adjunct Professor at Stanford University\". Research: The text says \"Ng also works on machine learning with an emphasis on deep learning. He founded and led the 'Google Brain' project which developed massive-scale deep learning algorithms... continues to work on deep learning and its applications to computer vision and speech, including such applications as autonomous driving.\" So research is \"machine learning with an emphasis on deep learning, including applications to computer vision, speech, and autonomous driving\". Evidence needs to be a short verbatim excerpt. The relevant excerpt could be \"Andrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\" as that's the start of his profile.\n\nLet me verify each key:\n\nfound: true, because there's a faculty profile for Andrew Ng.\n\nname: Andrew Ng (exact name mentioned).\n\ncollege: Stanford HAI (since the site is Stanford HAI, and the profile is on Stanford HAI's page).\n\nposition: \"Adjunct Professor at Stanford University\" (verbatim from the text: \"Adjunct Professor at Stanford University\").\n\nresearch: The research section mentions machine learning with emphasis on deep learning, Google Brain project, applications to computer vision, speech, autonomous driving. So research is \"machine learning with an emphasis on deep learning, including applications to computer vision, speech, and autonomous driving\".\n\nevidence: verbatim excerpt. The text starts with \"Andrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\", which is a verbatim part.\n\nUnsupported fields: none, since all required fields are supported.\n\nSo putting it all together, the JSON should have these values."
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1785388186,
|
||||
"model": "doubao-seed-1-6-250615",
|
||||
"object": "chat.completion",
|
||||
"service_tier": "default",
|
||||
"system_fingerprint": null,
|
||||
"usage": {
|
||||
"completion_tokens": 627,
|
||||
"prompt_tokens": 643,
|
||||
"total_tokens": 1270,
|
||||
"completion_tokens_details": {
|
||||
"accepted_prediction_tokens": null,
|
||||
"audio_tokens": null,
|
||||
"reasoning_tokens": 547,
|
||||
"rejected_prediction_tokens": null
|
||||
},
|
||||
"prompt_tokens_details": {
|
||||
"audio_tokens": null,
|
||||
"cached_tokens": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_id": "021785388168110fc13f77dd38da39bcdb33ad9bd57064028fde0",
|
||||
"response_model": "doubao-seed-1-6-250615",
|
||||
"usage": {
|
||||
"completion_tokens": 627,
|
||||
"prompt_tokens": 643,
|
||||
"total_tokens": 1270,
|
||||
"completion_tokens_details": {
|
||||
"accepted_prediction_tokens": null,
|
||||
"audio_tokens": null,
|
||||
"reasoning_tokens": 547,
|
||||
"rejected_prediction_tokens": null
|
||||
},
|
||||
"prompt_tokens_details": {
|
||||
"audio_tokens": null,
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"duration_seconds": 18.008,
|
||||
"request_sha256": "685ffe4d54aa05d0ba113de8d9768265eb9168d74198810e28121f9e9cd5f749",
|
||||
"response_sha256": "3936add6e391127252cb2f79371ab5ffd114d16fa21e97d8ca4127eb5846de03",
|
||||
"captured_at": "2026-07-30T05:09:45.877Z"
|
||||
},
|
||||
{
|
||||
"kind": "llm_chat_completion",
|
||||
"context": {
|
||||
"phase": "cascade_stress",
|
||||
"worker_id": "agent-02",
|
||||
"site": "hai-c"
|
||||
},
|
||||
"provider": "ark",
|
||||
"request": {
|
||||
"model": "doubao-seed-1-6-250615",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "{\"target\": \"Andrew Ng\", \"site_college\": \"Stanford HAI\", \"url\": \"https://hai.stanford.edu/people/andrew-ng?worker=c\", \"rendered_page_text\": \"Skip to content\\nAbout\\nResearch\\nEducation\\nPolicy\\nAI Index\\nNews\\nEvents\\nIndustry\\nCenters & Labs\\nPEOPLE\\nFACULTY\\nAndrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\\n\\nAndrew Ng is Founder & CEO of Landing AI, Founder of deeplearning.ai, Co-Chairman and Co-Founder of Coursera, and is currently an Adjunct Professor at Stanford University. He was also Chief Scientist at Baidu Inc., and Founder & Lead for the Google Brain Project.\\n\\nIn 2011 he led the development of Stanford University’s main MOOC (Massive Open Online Courses) platform and also taught an online Machine Learning class to over 100,000 students, leading to the founding of Coursera. Ng’s goal is to give everyone in the world access to a great education, for free. Today, Coursera partners with some of the top universities in the world to offer high quality online courses, and is the largest MOOC platform in the world.\\n\\nNg also works on machine learning with an emphasis on deep learning. He founded and led the “Google Brain” project which developed massive-scale deep learning algorithms. This resulted in the famous “Google cat” result, in which a massive neural network with 1 billion parameters learned from unlabeled YouTube videos to detect cats. More recently, he continues to work on deep learning and its applications to computer vision and speech, including such applications as autonomous driving.\\n\\nSHARE\\nNAVIGATE\\nAbout\\nEvents\\nAI Glossary\\nCareers\\nSearch\\nPARTICIPATE\\nGet Involved\\nSupport HAI\\nContact Us\\nStay Up To Date\\n\\nGet the latest news, advances in research, policy work, and education program updates from HAI in your inbox weekly.\\n\\nSign Up For Latest News\\n\\nStanford\\nUniversity\\nStanford HomeMaps & DirectionsSearch StanfordEmergency Info\\nTerms of UsePrivacyCopyrightTrademarksNon-DiscriminationAccessibility\\n© Stanford University. Stanford, California 94305.\\nPause Media\", \"instruction\": \"Use only rendered_page_text. Decide whether it contains this exact person's faculty profile. Return JSON keys found, name, college, position, research, evidence. If the name is only a link/listing, found may be true but leave unsupported fields empty. evidence must be a short verbatim excerpt.\"}"
|
||||
}
|
||||
],
|
||||
"response_format": {
|
||||
"type": "json_object"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"id": "0217853881892313749365b55318faa75db47eb4778acb39034a4",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"logprobs": null,
|
||||
"message": {
|
||||
"content": "{\"found\": true, \"name\": \"Andrew Ng\", \"college\": \"Stanford HAI\", \"position\": \"Adjunct Professor at Stanford University\", \"research\": \"machine learning with an emphasis on deep learning, applications to computer vision and speech, including autonomous driving\", \"evidence\": \"Andrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\"}",
|
||||
"refusal": null,
|
||||
"role": "assistant",
|
||||
"annotations": null,
|
||||
"audio": null,
|
||||
"function_call": null,
|
||||
"tool_calls": null,
|
||||
"reasoning_content": "\nI need to process the given query. Let's start by understanding the requirements. The user wants to use only the \"rendered_page_text\" to determine if it contains Andrew Ng's faculty profile. Then return JSON with keys: found, name, college, position, research, evidence.\n\nFirst, check if the name is present. The rendered_page_text has \"Andrew Ng\" under \"FACULTY\". So \"found\" should be true.\n\n\"name\" is clearly \"Andrew Ng\".\n\n\"college\": The \"site_college\" is \"Stanford HAI\", and in the text, it's part of Stanford University, so college would be \"Stanford HAI\".\n\n\"position\": The text states \"Adjunct Professor at Stanford University\".\n\n\"research\": It mentions \"machine learning with an emphasis on deep learning\" and applications to \"computer vision and speech, including such applications as autonomous driving\".\n\n\"evidence\" needs to be a short verbatim excerpt. A good excerpt is \"Andrew Ng\\n\\nFounder of DeepLearning.AI and Adjunct Professor at Stanford University\" as it directly identifies him and his position.\n\nNow, structure all these into the required JSON."
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1785388197,
|
||||
"model": "doubao-seed-1-6-250615",
|
||||
"object": "chat.completion",
|
||||
"service_tier": "default",
|
||||
"system_fingerprint": null,
|
||||
"usage": {
|
||||
"completion_tokens": 325,
|
||||
"prompt_tokens": 646,
|
||||
"total_tokens": 971,
|
||||
"completion_tokens_details": {
|
||||
"accepted_prediction_tokens": null,
|
||||
"audio_tokens": null,
|
||||
"reasoning_tokens": 246,
|
||||
"rejected_prediction_tokens": null
|
||||
},
|
||||
"prompt_tokens_details": {
|
||||
"audio_tokens": null,
|
||||
"cached_tokens": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"response_id": "0217853881892313749365b55318faa75db47eb4778acb39034a4",
|
||||
"response_model": "doubao-seed-1-6-250615",
|
||||
"usage": {
|
||||
"completion_tokens": 325,
|
||||
"prompt_tokens": 646,
|
||||
"total_tokens": 971,
|
||||
"completion_tokens_details": {
|
||||
"accepted_prediction_tokens": null,
|
||||
"audio_tokens": null,
|
||||
"reasoning_tokens": 246,
|
||||
"rejected_prediction_tokens": null
|
||||
},
|
||||
"prompt_tokens_details": {
|
||||
"audio_tokens": null,
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"duration_seconds": 8.026,
|
||||
"request_sha256": "5b59d5243bb3dd829d888e5aa2f9baa0906662ad520ac7b73c4e3aa63ea42629",
|
||||
"response_sha256": "2b82f2e8eb37313b43e5d655d72bb7a4d37fbc9082a07b22995d495a1e4e9454",
|
||||
"captured_at": "2026-07-30T05:09:57.026Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"experiment": "10-4",
|
||||
"run_id": "exp10-4-real-receipts-20260730-v2",
|
||||
"generated_at": "2026-07-30T05:09:57.077Z",
|
||||
"git_commit": "f119cf510f3746cd5d071d0bce0051617f02f163",
|
||||
"runtime_source_sha256": {
|
||||
"run_official_experiment.py": "e09e215e433fbc96073bc57bb9ecfda611a967a0854ec89a0846755f8d4d276e",
|
||||
"demo.py": "0e974987333715f931d6a9f9fa914a9efa7ead3608c21511c407480dc2bd5e86",
|
||||
"agents.py": "1dc78ff96f765de2fee8ee96ac72ae64b706c924d2ae8d9caa084bd4a874eb73",
|
||||
"llm.py": "b7d820398a3e5be6f3cf6fde833f20036ec8039cc9c62aa7560f091dc0ae60a0",
|
||||
"message_bus.py": "06c25d988884830422a1f902d4a6b32c96566af6a180b6f710cd8fa48f6caa42",
|
||||
"sources.py": "05c1115ea2538c32d906c9bb965fe23209078847053e018fe3190a8ff693e27f",
|
||||
"cascade-stress.example.json": "94126527d495304a5f397f9d84de25e4c12f4a916067e38bed8bc8fab82fb146"
|
||||
},
|
||||
"input_sha256": {
|
||||
"default_sites_canonical_json": "6b6c1ecf715b826c18bf553140c0d769e9c65c1c64b51310f6f54f6b6b035223",
|
||||
"cascade_sites_canonical_json": "2fcb099b49d4402967f067a1cb98af51f955c0a809f5fdc4767e2460cccf2dc3"
|
||||
},
|
||||
"artifact_sha256": {
|
||||
"evidence.json": "06dd7becf5602b7a3f98f012b5abf1a2000e0faa7198d7594f59a6cd36887b37",
|
||||
"browser_receipts.json": "7706a362b3f1a09af9351ee1b97b1987f27ef8f442b81eb6d31df44004aaa882",
|
||||
"llm_receipts.json": "6d42e1988095c14f496f3f25b6e47b450ace01d25bcbf8de88fa9c74a2ebdef7",
|
||||
"message_bus_receipts.json": "cddd2ec2da6400c3b2efddb5e2f780bec70dd030d7a9d4d798a9252174cb48c1"
|
||||
},
|
||||
"acceptance": {
|
||||
"overall_status": "pass",
|
||||
"passed_gates": 12,
|
||||
"total_gates": 12
|
||||
}
|
||||
}
|
||||
+1475
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user