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,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()
|
||||
Reference in New Issue
Block a user