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

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,447 @@
#!/usr/bin/env python3
"""Run the complete, real local-server campaign for Chapter 2 Experiment 2-1.
Unlike an OpenAI-compatible client, this runner deliberately uses Ollama's
``/api/generate`` endpoint with ``raw=true``. The exact string emitted by the
Qwen chat template is therefore visible in the evidence, including role
sentinels and the model's XML tool-call protocol. No model output is mocked.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import platform
import re
import statistics
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import requests
from transformers import AutoTokenizer
from tools import ToolRegistry
ROOT = Path(__file__).resolve().parent
PROTOCOL = ROOT / "experiment_protocol.json"
TOOL_PATTERN = re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL)
def sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def sha256_text(text: str) -> str:
return sha256_bytes(text.encode("utf-8"))
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def parse_tool_calls(raw_text: str) -> list[dict[str, Any]]:
calls = []
for match in TOOL_PATTERN.finditer(raw_text):
value = json.loads(match.group(1))
if not isinstance(value, dict) or not isinstance(value.get("name"), str):
raise ValueError("tool call must contain a string name")
arguments = value.get("arguments", {})
if not isinstance(arguments, dict):
raise ValueError("tool-call arguments must be an object")
calls.append({"name": value["name"], "arguments": arguments})
return calls
class OllamaRawClient:
def __init__(self, base_url: str, model: str, timeout: float = 180.0):
self.base_url = base_url.rstrip("/")
self.model = model
self.timeout = timeout
def get_json(self, path: str) -> dict[str, Any]:
response = requests.get(self.base_url + path, timeout=self.timeout)
response.raise_for_status()
return response.json()
def show_model(self) -> dict[str, Any]:
response = requests.post(
self.base_url + "/api/show",
json={"model": self.model},
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def generate(
self,
prompt: str,
*,
num_predict: int,
temperature: float,
) -> dict[str, Any]:
"""Stream one raw request and retain every credential-free chunk."""
request_body = {
"model": self.model,
"prompt": prompt,
"raw": True,
"stream": True,
"keep_alive": "10m",
"options": {
"temperature": temperature,
"num_predict": num_predict,
"seed": 21,
},
}
started_at = utc_now()
started = time.perf_counter()
first_piece_s = None
chunks: list[dict[str, Any]] = []
pieces: list[str] = []
with requests.post(
self.base_url + "/api/generate",
json=request_body,
stream=True,
timeout=self.timeout,
) as response:
response.raise_for_status()
for line in response.iter_lines():
if not line:
continue
chunk = json.loads(line)
chunks.append(chunk)
piece = chunk.get("response") or ""
if piece:
if first_piece_s is None:
first_piece_s = time.perf_counter() - started
pieces.append(piece)
wall_s = time.perf_counter() - started
final = chunks[-1] if chunks else {}
eval_count = int(final.get("eval_count") or 0)
eval_duration_s = float(final.get("eval_duration") or 0) / 1e9
return {
"requested_at": started_at,
"request": request_body,
"request_prompt_sha256": sha256_text(prompt),
"raw_chunks": chunks,
"raw_response": "".join(pieces),
"response_sha256": sha256_text("".join(pieces)),
"ttft_s": first_piece_s if first_piece_s is not None else wall_s,
"wall_s": wall_s,
"server": {
key: final.get(key)
for key in (
"model",
"created_at",
"done",
"done_reason",
"total_duration",
"load_duration",
"prompt_eval_count",
"prompt_eval_duration",
"eval_count",
"eval_duration",
)
},
"decode_tokens_per_second": (
eval_count / eval_duration_s if eval_duration_s > 0 else None
),
}
def normalize_tool_call(call: dict[str, Any]) -> dict[str, Any]:
"""Normalize the small model's harmless city-vs-schema variations."""
name = call["name"]
args = dict(call["arguments"])
if name == "get_current_time":
city = args.pop("city", None)
if city and "timezone" not in args:
args["timezone"] = "America/Vancouver"
elif name in {"get_weather", "get_current_temperature"}:
name = "get_current_temperature"
city = args.pop("city", None)
if city and "location" not in args:
args["location"] = "Vancouver, Canada"
args.setdefault("unit", "celsius")
return {"name": name, "arguments": args}
def execute_parallel(registry: ToolRegistry, calls: list[dict[str, Any]]) -> dict[str, Any]:
started_at = utc_now()
started = time.perf_counter()
def execute(index_and_call):
index, call = index_and_call
one_started = time.perf_counter()
result = registry.execute_tool(call["name"], call["arguments"])
return {
"index": index,
"call": call,
"result": result,
"duration_s": time.perf_counter() - one_started,
}
with ThreadPoolExecutor(max_workers=len(calls)) as executor:
results = list(executor.map(execute, enumerate(calls)))
results.sort(key=lambda item: item["index"])
return {
"started_at": started_at,
"execution": "ThreadPoolExecutor",
"wall_s": time.perf_counter() - started,
"results": results,
}
def render_prompt(tokenizer, messages, tools=None) -> str:
return tokenizer.apply_chat_template(
messages,
tools=tools,
tokenize=False,
add_generation_prompt=True,
enable_thinking=True,
)
def run_tool_case(client, tokenizer, protocol) -> dict[str, Any]:
registry = ToolRegistry()
all_schemas = registry.get_tool_schemas()
required_names = set(protocol["tool_case"]["required_tools"])
tools = [item for item in all_schemas if item["function"]["name"] in required_names]
messages: list[dict[str, Any]] = [
{
"role": "system",
"content": (
"You are a helpful assistant. Use tools for current facts. "
"When asking for Vancouver time, pass the IANA timezone "
"America/Vancouver; do not substitute another city's timezone."
),
},
{"role": "user", "content": protocol["tool_case"]["prompt"]},
]
first_prompt = render_prompt(tokenizer, messages, tools)
first = client.generate(
first_prompt,
num_predict=protocol["runtime"]["num_predict"],
temperature=protocol["runtime"]["temperature"],
)
parsed = parse_tool_calls(first["raw_response"])
normalized = [normalize_tool_call(item) for item in parsed]
parallel = execute_parallel(registry, normalized) if normalized else {
"started_at": utc_now(), "execution": "not_run", "wall_s": 0, "results": []
}
messages.append({"role": "assistant", "content": first["raw_response"]})
for result in parallel["results"]:
messages.append({"role": "tool", "content": result["result"]})
second_prompt = render_prompt(tokenizer, messages, tools)
second = client.generate(
second_prompt,
num_predict=protocol["runtime"]["num_predict"],
temperature=protocol["runtime"]["temperature"],
)
second_calls = parse_tool_calls(second["raw_response"])
observed = [item["name"] for item in normalized]
required = protocol["tool_case"]["required_tools"]
calls_by_name = {item["name"]: item["arguments"] for item in normalized}
time_arguments = calls_by_name.get("get_current_time", {})
weather_arguments = calls_by_name.get("get_current_temperature", {})
tool_results_valid = len(parallel["results"]) == 2 and all(
not str(item["result"]).startswith('{"error"')
for item in parallel["results"]
)
gates = {
"chat_template_special_tokens_visible": all(
token in first_prompt for token in ("<|im_start|>", "<|im_end|>", "<tools>")
),
"raw_tool_tags_visible": "<tool_call>" in first["raw_response"],
"exact_required_tools": len(observed) == 2 and sorted(observed) == sorted(required),
"tool_arguments_match_vancouver": (
time_arguments.get("timezone") == protocol["tool_case"]["required_timezone"]
and "vancouver" in str(weather_arguments.get("location", "")).lower()
),
"parallel_tool_results_valid": tool_results_valid,
"terminated_after_results": bool(second["raw_response"].strip()) and not second_calls,
}
return {
"messages": messages,
"tools": tools,
"first_turn": first,
"parsed_tool_calls": parsed,
"normalized_tool_calls": normalized,
"parallel_execution": parallel,
"second_rendered_prompt": second_prompt,
"second_turn": second,
"second_turn_tool_calls": second_calls,
"gates": gates,
"passed": all(gates.values()),
}
def run_cache_case(client, tokenizer, protocol) -> dict[str, Any]:
cfg = protocol["cache_case"]
filler = "Keep this stable operating-manual sentence unchanged. "
header = "# Stable operating manual\n"
system = header + filler * max(1, int(cfg["approximate_prefix_tokens"] * 4 / len(filler)))
messages = [
{"role": "system", "content": system},
{"role": "user", "content": "Reply with only the word READY."},
]
stable = render_prompt(tokenizer, messages)
warmups = [
client.generate(stable, num_predict=8, temperature=0)
for _ in range(cfg["warmups"])
]
pairs = []
for index in range(cfg["matched_repeats"]):
hit = client.generate(stable, num_predict=8, temperature=0)
marker = f"M{index:07d}" # fixed width and placed at byte zero
mutated_system = marker + system[len(marker):]
mutated = render_prompt(
tokenizer,
[
{"role": "system", "content": mutated_system},
{"role": "user", "content": "Reply with only the word READY."},
],
)
miss = client.generate(mutated, num_predict=8, temperature=0)
pairs.append({
"pair": index + 1,
"hit": hit,
"miss": miss,
"prompt_character_lengths_equal": len(stable) == len(mutated),
})
hit_samples = [item["hit"]["ttft_s"] for item in pairs]
miss_samples = [item["miss"]["ttft_s"] for item in pairs]
return {
"stable_prompt_sha256": sha256_text(stable),
"stable_prompt_character_count": len(stable),
"warmups": warmups,
"pairs": pairs,
"summary": {
"hit_ttft_s": hit_samples,
"miss_ttft_s": miss_samples,
"hit_mean_s": statistics.fmean(hit_samples),
"miss_mean_s": statistics.fmean(miss_samples),
"miss_over_hit": (
statistics.fmean(miss_samples) / statistics.fmean(hit_samples)
if statistics.fmean(hit_samples) else None
),
"hit_faster_in_pairs": sum(
item["hit"]["ttft_s"] < item["miss"]["ttft_s"] for item in pairs
),
"matched_pairs": len(pairs),
},
}
def credential_scan(path: Path) -> list[str]:
text = path.read_text(encoding="utf-8")
findings = []
for pattern in (r"sk-[A-Za-z0-9_-]{16,}", r"sk-or-[A-Za-z0-9_-]{12,}"):
findings.extend(match.group(0)[:8] + "" for match in re.finditer(pattern, text))
return findings
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", default="http://localhost:11434")
parser.add_argument("--model", default="qwen3:0.6b")
parser.add_argument("--tokenizer", default="Qwen/Qwen3-0.6B")
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
protocol_bytes = PROTOCOL.read_bytes()
protocol = json.loads(protocol_bytes)
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=False)
(output / "experiment_protocol.json").write_bytes(protocol_bytes)
client = OllamaRawClient(args.base_url, args.model)
version = client.get_json("/api/version")
tags = client.get_json("/api/tags")
matching = [item for item in tags.get("models", []) if item.get("name") == args.model]
show = client.show_model()
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer, local_files_only=True)
evidence: dict[str, Any] = {
"experiment_id": "2-1",
"started_at": utc_now(),
"protocol_sha256": sha256_bytes(protocol_bytes),
"provider": "local Ollama",
"endpoint": args.base_url,
"model": args.model,
"tokenizer": args.tokenizer,
"host": {
"platform": platform.platform(),
"machine": platform.machine(),
"processor": platform.processor(),
"python": platform.python_version(),
},
"server": {
"version": version,
"tag": matching[0] if matching else None,
"show": {
"modified_at": show.get("modified_at"),
"details": show.get("details"),
"model_info": show.get("model_info"),
},
},
}
evidence["tool_case"] = run_tool_case(client, tokenizer, protocol)
evidence["cache_case"] = run_cache_case(client, tokenizer, protocol)
evidence["finished_at"] = utc_now()
tag = evidence["server"]["tag"] or {}
throughput = [
evidence["tool_case"][turn].get("decode_tokens_per_second")
for turn in ("first_turn", "second_turn")
]
throughput = [value for value in throughput if value is not None]
evidence["summary"] = {
"model_digest": tag.get("digest"),
"local_model_verified": bool(tag.get("digest")),
"tool_case_passed": evidence["tool_case"]["passed"],
"mean_tool_case_decode_tokens_per_second": (
statistics.fmean(throughput) if throughput else None
),
"exceeded_100_tokens_per_second_on_this_host": bool(
throughput and statistics.fmean(throughput) > 100
),
"cache_observation": evidence["cache_case"]["summary"],
}
evidence["official_complete"] = bool(
evidence["summary"]["local_model_verified"]
and evidence["summary"]["tool_case_passed"]
and evidence["cache_case"]["summary"]["matched_pairs"] == cfg_pairs(protocol)
)
evidence_path = output / "evidence.json"
evidence_path.write_text(json.dumps(evidence, indent=2, ensure_ascii=False), encoding="utf-8")
findings = credential_scan(evidence_path)
manifest = {
"experiment_id": "2-1",
"official_complete": evidence["official_complete"] and not findings,
"protocol_sha256": evidence["protocol_sha256"],
"evidence_sha256": sha256_bytes(evidence_path.read_bytes()),
"credential_scan_passed": not findings,
"credential_scan_findings": findings,
"cost": {"amount": 0, "currency": "USD", "qualification": "local inference"},
}
(output / "manifest.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
)
print(json.dumps({"output": str(output), **manifest, "summary": evidence["summary"]}, indent=2))
return 0 if manifest["official_complete"] else 1
def cfg_pairs(protocol: dict[str, Any]) -> int:
return int(protocol["cache_case"]["matched_repeats"])
if __name__ == "__main__":
raise SystemExit(main())