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,397 @@
"""
event_loop_demo.py —— 事件驱动 Agent 的端到端演示(单进程、可离线运行)
本章"事件驱动的异步 Agent"一节指出:真正的"主动服务"不仅需要 Agent 能定时
检查世界,更需要世界能主动通知 Agent。本脚本用最小的代码把这一点跑起来——
1. 注册若干"事件触发器"trigger source),每个触发器在后台线程里运行,
在事件真正发生的那一刻把一个结构化 Event 推入统一的事件队列:
- 一次性定时器 OneShotTimer —— 对应书中 set_timer 的"一次性定时器"
- 循环定时器 RecurringTimer —— 对应书中 set_timer 的"循环定时器"
- 文件监听 FileWatchTrigger —— 对应 n8n 等平台的文件变更触发器
2. 事件循环 EventLoop 从队列里逐个取出事件,唤醒 Agent 处理——这正是
"Agent 注册、外部触发"的完整闭环:注册时声明关心什么事件,触发时被异步唤醒。
与需要起 HTTP 服务器的 server.py / client.py 不同,本脚本在单个进程里同时扮演
"外部世界""Agent",因此适合用来直观演示事件驱动的行为。
离线模式(--mock):不调用大模型,用一个"模拟动作"打印 Agent 被唤醒后的处理
过程,可在没有 API Key 的环境下观察完整的触发→唤醒→处理闭环。
真实模式(默认):接入 EventTriggeredAgent,由大模型真正处理每个事件。
用法示例:
python event_loop_demo.py --mock # 离线演示全部触发器
python event_loop_demo.py --mock --trigger timer # 只演示一次性定时器
python event_loop_demo.py --mock --trigger recurring --interval 3 --duration 12
python event_loop_demo.py --trigger file --watch-dir ./watched # 真实 Agent 处理文件事件
"""
import os
import sys
import time
import queue
import logging
import argparse
import threading
from datetime import datetime
from typing import Optional, Callable
from event_types import Event, EventType
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("event_loop")
# ============================================================================
# 事件触发器(trigger source
# ============================================================================
class TriggerSource(threading.Thread):
"""事件触发器基类:在后台线程中运行,把事件推入共享的事件队列。
注册(register)体现在实例化并 start();触发(fire)体现在 run() 中
满足条件时调用 self.emit(event)。这与书中"注册时由 Agent 主动调用工具、
触发时由外部事件异步回调"的两个时刻一一对应。
"""
def __init__(self, name: str, event_queue: "queue.Queue[Event]"):
super().__init__(name=name, daemon=True)
self.event_queue = event_queue
self._stop = threading.Event()
def emit(self, event: Event):
"""触发:把事件推入事件队列,唤醒事件循环。"""
logger.info(f"⚡ [{self.name}] 触发事件 -> {event.event_type.value}: {event.content}")
self.event_queue.put(event)
def stop(self):
self._stop.set()
class OneShotTimer(TriggerSource):
"""一次性定时器:延迟 delay 秒后触发一次 timer_trigger 事件。
对应书中"用户要求给 DMV 打电话,当前是周六,Agent 设置'下周一上午 10:00
致电 DMV'"这类有明确时间点的任务。
"""
def __init__(self, event_queue, delay: float, content: str, timer_id: str = "oneshot"):
super().__init__(name=f"OneShotTimer({timer_id})", event_queue=event_queue)
self.delay = delay
self.content = content
self.timer_id = timer_id
def run(self):
logger.info(f"⏱️ [{self.name}] 已注册:{self.delay:.0f} 秒后触发")
if self._stop.wait(self.delay):
return
self.emit(Event(
event_type=EventType.TIMER_TRIGGER,
content=self.content,
metadata={"timer_id": self.timer_id, "kind": "one_shot",
"scheduled_delay_seconds": self.delay},
))
class RecurringTimer(TriggerSource):
"""循环定时器:每隔 interval 秒触发一次 timer_trigger 事件。
对应书中"每小时检查一次服务器健康状况""每周五发送进展报告",以及
OpenClaw Heartbeat 式的定时轮询。
"""
def __init__(self, event_queue, interval: float, content: str, timer_id: str = "recurring"):
super().__init__(name=f"RecurringTimer({timer_id})", event_queue=event_queue)
self.interval = interval
self.content = content
self.timer_id = timer_id
def run(self):
logger.info(f"🔁 [{self.name}] 已注册:每 {self.interval:.0f} 秒触发一次")
tick = 0
while not self._stop.wait(self.interval):
tick += 1
self.emit(Event(
event_type=EventType.TIMER_TRIGGER,
content=f"{self.content}(第 {tick} 次)",
metadata={"timer_id": self.timer_id, "kind": "recurring",
"interval_seconds": self.interval, "tick": tick},
))
class FileWatchTrigger(TriggerSource):
"""文件监听:轮询目录,发现新增或被修改的文件时触发 file_change 事件。
对应书中"n8n 等工作流平台的触发器生态:Webhook、定时器、邮件、数据库
变更、文件监听"。这里用轮询实现,不依赖第三方库,便于跨平台离线运行。
"""
def __init__(self, event_queue, watch_dir: str, poll_interval: float = 1.0):
super().__init__(name=f"FileWatch({watch_dir})", event_queue=event_queue)
self.watch_dir = watch_dir
self.poll_interval = poll_interval
self._snapshot = {}
def _scan(self):
snapshot = {}
try:
for entry in os.scandir(self.watch_dir):
if entry.is_file():
snapshot[entry.name] = entry.stat().st_mtime
except FileNotFoundError:
pass
return snapshot
def run(self):
os.makedirs(self.watch_dir, exist_ok=True)
self._snapshot = self._scan()
logger.info(f"👀 [{self.name}] 已注册:轮询间隔 {self.poll_interval:.0f}"
f"(当前已有 {len(self._snapshot)} 个文件)")
while not self._stop.wait(self.poll_interval):
current = self._scan()
for name, mtime in current.items():
if name not in self._snapshot:
change = "created"
elif mtime != self._snapshot[name]:
change = "modified"
else:
continue
self.emit(Event(
event_type=EventType.FILE_CHANGE,
content=f"检测到文件{'新增' if change == 'created' else '修改'},请查看其内容并给出简要处理建议。",
metadata={"path": os.path.join(self.watch_dir, name), "change": change},
))
self._snapshot = current
# ============================================================================
# 事件循环(event loop
# ============================================================================
class EventLoop:
"""统一事件队列 + 单线程分发。
所有触发器把异构事件推入同一个队列;事件循环按到达顺序取出,每个事件
唤醒一次 Agent 处理。这正是书中"将所有输入统一建模为事件流,通过事件
循环驱动 Agent 的思考和行动"的最小实现。
"""
def __init__(self, dispatch: Callable[[Event], None]):
self.event_queue: "queue.Queue[Event]" = queue.Queue()
self.dispatch = dispatch
self.triggers = []
self.processed = 0
def add_trigger(self, trigger: TriggerSource):
self.triggers.append(trigger)
def run(self, duration: float):
"""启动所有触发器,运行 duration 秒后停止。"""
deadline = time.monotonic() + duration
for t in self.triggers:
t.start()
logger.info(f"🟢 事件循环启动,将运行 {duration:.0f} 秒,等待事件唤醒 Agent...\n")
while time.monotonic() < deadline:
try:
event = self.event_queue.get(timeout=0.5)
except queue.Empty:
continue
self.processed += 1
logger.info(f"\n{'='*80}\n📥 事件循环取出第 {self.processed} 个事件"
f" -> 唤醒 Agent\n{'='*80}")
try:
self.dispatch(event)
except Exception as e: # noqa: BLE001 - 演示中不希望单个事件异常终止循环
logger.error(f"❌ 处理事件时出错: {e}")
for t in self.triggers:
t.stop()
logger.info(f"\n🔴 事件循环结束,共处理 {self.processed} 个事件。")
# ============================================================================
# 分发处理器:模拟动作 or 真实 Agent
# ============================================================================
def make_mock_dispatch() -> Callable[[Event], None]:
"""离线模拟处理器:不调用大模型,打印 Agent 被唤醒后的处理过程。"""
def dispatch(event: Event):
logger.info(f"🤖 Agent 被唤醒,收到消息: {event.to_user_message()}")
# 用一个确定性的"模拟动作"代替大模型 + 工具调用
if event.event_type == EventType.TIMER_TRIGGER:
action = "读取定时任务上下文 -> 执行例行检查 -> 汇报结果"
elif event.event_type == EventType.FILE_CHANGE:
path = event.metadata.get("path", "")
preview = ""
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
preview = f.read(120).replace("\n", " ")
except OSError:
preview = "(无法读取文件内容)"
action = f"读取文件 {os.path.basename(path)} -> 内容预览: {preview!r} -> 生成处理建议"
else:
action = "解析事件 -> 调用相关工具 -> 生成处理结果"
logger.info(f"🛠️ [模拟动作] {action}")
logger.info(f"✅ Agent 处理完成: 已响应 {event.event_type.value} 事件")
return dispatch
def make_agent_dispatch(provider: str, model: Optional[str],
max_iterations: int) -> Callable[[Event], None]:
"""真实处理器:接入 EventTriggeredAgent,由大模型处理每个事件。"""
from agent import EventTriggeredAgent, SystemHintConfig, resolve_provider_and_key
# 通用兜底:直连 provider 的 key 缺失时,若有 OPENROUTER_API_KEY 则自动改走 openrouter。
resolved_provider, api_key = resolve_provider_and_key(provider)
if not api_key:
print(f"❌ 未检测到 provider '{provider}' 对应的 API Key(也未配置 OPENROUTER_API_KEY 兜底)。")
print(f" 请先设置环境变量,或改用离线演示:python event_loop_demo.py --mock")
sys.exit(1)
if resolved_provider != provider:
print(f"️ provider '{provider}' 无可用 Key,已自动改用 OpenRouter 兜底(openrouter)。")
provider = resolved_provider
# 保留已是 provider/model 形式的显式模型;否则让 openrouter 用其默认模型。
model = model if (model and "/" in model) else None
config = SystemHintConfig(
enable_timestamps=True,
enable_tool_counter=True,
enable_todo_list=True,
enable_detailed_errors=True,
enable_system_state=True,
save_trajectory=True,
trajectory_file="event_loop_trajectory.json",
temperature=0.7,
max_tokens=4096,
use_mcp_servers=False, # 本演示仅用内置工具,避免额外的 MCP 依赖
)
agent = EventTriggeredAgent(api_key=api_key, provider=provider,
model=model, config=config, verbose=True)
logger.info(f"✅ 真实 Agent 初始化完成(provider={provider}, model={agent.model}")
def dispatch(event: Event):
result = agent.handle_event(event, max_iterations=max_iterations)
logger.info(f"✅ Agent 处理完成: success={result['success']}, "
f"iterations={result['iterations']}, "
f"tool_calls={len(result['tool_calls'])}")
return dispatch
# ============================================================================
# CLI
# ============================================================================
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="事件驱动 Agent 端到端演示:注册触发器,由外部事件异步唤醒 Agent。",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""示例:
python event_loop_demo.py --mock
离线演示全部触发器(一次性定时器 + 循环定时器 + 文件监听),无需 API Key
python event_loop_demo.py --mock --trigger timer
只演示一次性定时器
python event_loop_demo.py --mock --trigger recurring --interval 3 --duration 12
每 3 秒触发一次循环定时器,共运行 12 秒
python event_loop_demo.py --mock --trigger file --watch-dir ./watched
监听 ./watched 目录,向其中写入文件即可触发事件
python event_loop_demo.py --trigger timer --provider kimi
用真实大模型处理一次性定时器事件(需要设置对应的 API Key)
""",
)
parser.add_argument(
"--trigger", choices=["timer", "recurring", "file", "all"], default="all",
help="要演示的触发器类型:timer=一次性定时器,recurring=循环定时器,"
"file=文件监听,all=全部(默认:all)",
)
parser.add_argument(
"--mock", action="store_true",
help="离线模式:不调用大模型,用模拟动作演示触发→唤醒→处理闭环(无需 API Key)",
)
parser.add_argument(
"--duration", type=float, default=12.0,
help="事件循环总运行时长(秒),到时后停止所有触发器(默认:12)",
)
parser.add_argument(
"--delay", type=float, default=3.0,
help="一次性定时器的延迟触发时间(秒)(默认:3)",
)
parser.add_argument(
"--interval", type=float, default=4.0,
help="循环定时器的触发间隔(秒)(默认:4",
)
parser.add_argument(
"--watch-dir", default="watched_dir",
help="文件监听触发器监视的目录,不存在会自动创建(默认:watched_dir",
)
parser.add_argument(
"--provider", default=os.getenv("LLM_PROVIDER", "kimi"),
choices=["dashscope", "qwen", "bailian", "siliconflow", "doubao", "kimi", "moonshot", "openrouter"],
help="真实模式使用的大模型提供商(默认:环境变量 LLM_PROVIDER 或 kimi",
)
parser.add_argument(
"--model", default=os.getenv("LLM_MODEL"),
help="真实模式的模型名覆盖(默认:使用提供商默认模型)",
)
parser.add_argument(
"--max-iterations", type=int, default=10,
help="真实模式下单个事件的最大工具调用轮数(默认:10)",
)
return parser
def main():
args = build_parser().parse_args()
print("\n" + "=" * 80)
print("🚀 事件驱动 Agent 演示(EVENT-DRIVEN AGENT DEMO")
print("=" * 80)
print(f"触发器: {args.trigger} | 模式: {'离线模拟' if args.mock else '真实 Agent'} | "
f"时长: {args.duration:.0f}s")
print("=" * 80 + "\n")
sys.stdout.flush()
if args.mock:
dispatch = make_mock_dispatch()
else:
dispatch = make_agent_dispatch(args.provider, args.model, args.max_iterations)
loop = EventLoop(dispatch)
if args.trigger in ("timer", "all"):
loop.add_trigger(OneShotTimer(
loop.event_queue, delay=args.delay, timer_id="daily_backup_check",
content="一次性定时器到期:请检查每日备份是否已经完成。",
))
if args.trigger in ("recurring", "all"):
loop.add_trigger(RecurringTimer(
loop.event_queue, interval=args.interval, timer_id="health_check",
content="循环定时器到期:请检查服务器健康状况。",
))
if args.trigger in ("file", "all"):
loop.add_trigger(FileWatchTrigger(loop.event_queue, watch_dir=args.watch_dir))
print(f"💡 提示:向目录 {args.watch_dir}/ 写入或修改文件即可触发 file_change 事件。")
print(f" 例如另开一个终端执行:echo hello > {args.watch_dir}/note.txt\n")
sys.stdout.flush()
if not loop.triggers:
print("❌ 没有可运行的触发器。")
sys.exit(1)
try:
loop.run(duration=args.duration)
except KeyboardInterrupt:
print("\n⚠️ 收到中断信号,正在停止...")
for t in loop.triggers:
t.stop()
print("\n" + "=" * 80)
print(f"📊 演示结束:共处理 {loop.processed} 个事件。")
print("=" * 80 + "\n")
if __name__ == "__main__":
main()