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
374 lines
14 KiB
Python
374 lines
14 KiB
Python
"""
|
||
Demo: Email sending with learning capability
|
||
|
||
This demo shows how the agent learns to send emails and reuses the learned workflow.
|
||
Uses Ethereal Email (ethereal.email) as a test email service.
|
||
"""
|
||
|
||
import argparse
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import time
|
||
from dotenv import load_dotenv
|
||
|
||
from browser_use import ChatOpenAI, ChatGoogle
|
||
from learning_agent import LearningAgent
|
||
from llm_factory import make_llm, DEFAULT_MODEL
|
||
|
||
|
||
# Configure logging
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
|
||
load_dotenv()
|
||
|
||
|
||
# Default two-phase tasks. Phase 1 teaches the workflow from scratch (expensive,
|
||
# multimodal LLM loop); Phase 2 replays it with different parameters (cheap, no LLM).
|
||
DEFAULT_LEARNING_TASK = """
|
||
Go to https://ethereal.email and send a test email with:
|
||
- To: test@example.com
|
||
- Subject: Hello from Learning Agent
|
||
- Message: This is a test email sent by the learning agent. The workflow will be captured for future reuse.
|
||
"""
|
||
|
||
DEFAULT_REPLAY_TASK = """
|
||
Send an email to another@example.com with subject "Workflow Test"
|
||
and message "This email is sent using a learned workflow. No LLM calls needed!"
|
||
"""
|
||
|
||
|
||
class EmailDemo:
|
||
"""Demo for email sending with learning capability."""
|
||
|
||
def __init__(self, llm_model=DEFAULT_MODEL, knowledge_base_path="./email_knowledge",
|
||
headless=False, max_steps=20, learning_task=None, replay_task=None,
|
||
output_path=None):
|
||
self.llm_model = llm_model
|
||
self.knowledge_base_path = knowledge_base_path
|
||
self.headless = headless
|
||
self.max_steps = max_steps
|
||
self.learning_task = learning_task or DEFAULT_LEARNING_TASK
|
||
self.replay_task = replay_task or DEFAULT_REPLAY_TASK
|
||
self.output_path = output_path
|
||
|
||
async def run_full_demo(self):
|
||
"""Run complete email demo with learning and replay phases."""
|
||
|
||
print("=" * 80)
|
||
print("EMAIL SENDING DEMO - LEARNING AGENT")
|
||
print("=" * 80)
|
||
print("\nThis demo uses Ethereal Email (ethereal.email) for testing.")
|
||
print("Note: This is a test service - emails won't actually be delivered.")
|
||
print("=" * 80)
|
||
|
||
# Phase 1: Learning
|
||
await self.phase1_learning()
|
||
|
||
# Wait before Phase 2
|
||
print("\n⏳ Waiting 5 seconds before replay phase...")
|
||
await asyncio.sleep(5)
|
||
|
||
# Phase 2: Replay
|
||
await self.phase2_replay()
|
||
|
||
# Phase 3: Statistics
|
||
self.show_statistics()
|
||
|
||
# Optionally persist the before/after comparison for later analysis
|
||
if self.output_path:
|
||
self.save_results()
|
||
|
||
async def phase1_learning(self):
|
||
"""Phase 1: Learn how to send an email."""
|
||
|
||
print("\n" + "📚 PHASE 1: LEARNING - First Email Task ".ljust(70, "="))
|
||
print("\nThe agent will learn how to send an email from scratch.")
|
||
print("This requires multiple LLM calls to explore and understand the interface.")
|
||
print("-" * 70)
|
||
|
||
# Task with specific details
|
||
task = self.learning_task
|
||
|
||
print(f"Task: {task.strip()}")
|
||
print("Expected behavior:")
|
||
print(" 1. Navigate to Ethereal Email")
|
||
print(" 2. Create a test account (if needed)")
|
||
print(" 3. Compose and send the email")
|
||
print(" 4. Capture workflow for future reuse")
|
||
|
||
# Create learning agent
|
||
agent = LearningAgent(
|
||
task=task,
|
||
llm=self._get_llm(),
|
||
knowledge_base_path=self.knowledge_base_path,
|
||
headless=self.headless
|
||
)
|
||
|
||
print("\n🚀 Starting learning phase...")
|
||
start_time = time.time()
|
||
|
||
try:
|
||
result = await agent.run(max_steps=self.max_steps)
|
||
|
||
elapsed = time.time() - start_time
|
||
|
||
print("\n✅ Learning phase completed!")
|
||
print(f" 📊 Results:")
|
||
print(f" - Success: {'✓' if result['success'] else '✗'}")
|
||
print(f" - Execution time: {elapsed:.2f} seconds")
|
||
print(f" - LLM calls made: {result['llm_calls']}")
|
||
print(f" - Workflow captured: {'Yes' if result['success'] else 'No'}")
|
||
|
||
if result['success']:
|
||
print("\n 💡 Workflow successfully learned and saved!")
|
||
print(" The agent can now repeat similar tasks without LLM calls.")
|
||
|
||
# Store metrics for comparison
|
||
self.learning_metrics = {
|
||
'time': elapsed,
|
||
'llm_calls': result['llm_calls'],
|
||
'success': result['success']
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ Learning phase failed: {e}")
|
||
self.learning_metrics = {'time': 0, 'llm_calls': 0, 'success': False}
|
||
|
||
async def phase2_replay(self):
|
||
"""Phase 2: Replay learned workflow with different parameters."""
|
||
|
||
print("\n" + "🚀 PHASE 2: REPLAY - Second Email Task ".ljust(70, "="))
|
||
print("\nThe agent will reuse the learned workflow with different parameters.")
|
||
print("This should be much faster and require NO LLM calls.")
|
||
print("-" * 70)
|
||
|
||
# Different email parameters
|
||
task = self.replay_task
|
||
|
||
print(f"Task: {task.strip()}")
|
||
print("Expected behavior:")
|
||
print(" 1. Match task to learned workflow")
|
||
print(" 2. Extract new parameters (recipient, subject, message)")
|
||
print(" 3. Replay workflow with new parameters")
|
||
print(" 4. Complete task without any LLM calls")
|
||
|
||
# Create agent for replay
|
||
agent = LearningAgent(
|
||
task=task,
|
||
llm=self._get_llm(),
|
||
knowledge_base_path=self.knowledge_base_path,
|
||
headless=self.headless
|
||
)
|
||
|
||
print("\n🔄 Starting replay phase...")
|
||
start_time = time.time()
|
||
|
||
try:
|
||
result = await agent.run(max_steps=self.max_steps)
|
||
|
||
elapsed = time.time() - start_time
|
||
|
||
print("\n✅ Replay phase completed!")
|
||
print(f" 📊 Results:")
|
||
print(f" - Success: {'✓' if result['success'] else '✗'}")
|
||
print(f" - Execution time: {elapsed:.2f} seconds")
|
||
print(f" - Workflow reused: {'Yes' if result['replay_used'] else 'No'}")
|
||
|
||
if result['replay_used']:
|
||
# Calculate improvements
|
||
if hasattr(self, 'learning_metrics') and self.learning_metrics['success']:
|
||
speedup = self.learning_metrics['time'] / elapsed
|
||
calls_saved = self.learning_metrics['llm_calls']
|
||
|
||
print(f"\n 🎯 Performance Improvements:")
|
||
print(f" - Speed: {speedup:.1f}x faster")
|
||
print(f" - LLM calls saved: {calls_saved}")
|
||
print(f" - Time saved: {self.learning_metrics['time'] - elapsed:.1f} seconds")
|
||
else:
|
||
print(f" - LLM calls made: {result.get('llm_calls', 0)}")
|
||
print("\n ⚠️ Workflow was not reused. Task may be too different.")
|
||
|
||
# Store replay metrics
|
||
self.replay_metrics = {
|
||
'time': elapsed,
|
||
'replay_used': result['replay_used'],
|
||
'success': result['success']
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ Replay phase failed: {e}")
|
||
self.replay_metrics = {'time': 0, 'replay_used': False, 'success': False}
|
||
|
||
def show_statistics(self):
|
||
"""Show knowledge base statistics."""
|
||
|
||
print("\n" + "📊 KNOWLEDGE BASE STATISTICS ".ljust(70, "="))
|
||
|
||
from learning_agent import KnowledgeBase
|
||
kb = KnowledgeBase(self.knowledge_base_path)
|
||
stats = kb.get_statistics()
|
||
|
||
print("\n Current Knowledge Base Status:")
|
||
for key, value in stats.items():
|
||
formatted_key = key.replace('_', ' ').title()
|
||
print(f" - {formatted_key}: {value}")
|
||
|
||
# Show comparison if both phases completed
|
||
if hasattr(self, 'learning_metrics') and hasattr(self, 'replay_metrics'):
|
||
if self.learning_metrics['success'] and self.replay_metrics['replay_used']:
|
||
print("\n 📈 Performance Comparison:")
|
||
print(f" Phase 1 (Learning):")
|
||
print(f" - Time: {self.learning_metrics['time']:.2f}s")
|
||
print(f" - LLM Calls: {self.learning_metrics['llm_calls']}")
|
||
print(f" Phase 2 (Replay):")
|
||
print(f" - Time: {self.replay_metrics['time']:.2f}s")
|
||
print(f" - LLM Calls: 0")
|
||
|
||
improvement = (1 - self.replay_metrics['time'] / self.learning_metrics['time']) * 100
|
||
print(f"\n 🚀 Overall Improvement: {improvement:.0f}% faster with replay!")
|
||
|
||
print("\n" + "=" * 70)
|
||
print("DEMO COMPLETED SUCCESSFULLY")
|
||
print("=" * 70)
|
||
|
||
def save_results(self):
|
||
"""Persist learning/replay metrics and KB stats to a JSON file."""
|
||
from learning_agent import KnowledgeBase
|
||
|
||
kb = KnowledgeBase(self.knowledge_base_path)
|
||
payload = {
|
||
'model': self.llm_model,
|
||
'knowledge_base_path': self.knowledge_base_path,
|
||
'headless': self.headless,
|
||
'max_steps': self.max_steps,
|
||
'learning_task': self.learning_task.strip(),
|
||
'replay_task': self.replay_task.strip(),
|
||
'learning_metrics': getattr(self, 'learning_metrics', None),
|
||
'replay_metrics': getattr(self, 'replay_metrics', None),
|
||
'knowledge_base_stats': kb.get_statistics(),
|
||
}
|
||
|
||
with open(self.output_path, 'w', encoding='utf-8') as f:
|
||
json.dump(payload, f, ensure_ascii=False, indent=2)
|
||
|
||
print(f"\n💾 Results saved to: {self.output_path}")
|
||
|
||
def _get_llm(self):
|
||
"""Get LLM instance based on configuration (OpenAI 直连,缺 Key 时 OpenRouter 兜底)。"""
|
||
return make_llm(self.llm_model)
|
||
|
||
|
||
async def quick_test(model=DEFAULT_MODEL, headless=False, max_steps=15,
|
||
knowledge_base_path="./test_knowledge", task=None):
|
||
"""Quick test with a single simple task (no replay comparison)."""
|
||
print("\n🧪 QUICK TEST - Simple Email Task")
|
||
print("-" * 40)
|
||
|
||
task = task or "Go to ethereal.email and send a test email to demo@test.com"
|
||
|
||
llm = make_llm(model)
|
||
agent = LearningAgent(
|
||
task=task,
|
||
llm=llm,
|
||
knowledge_base_path=knowledge_base_path,
|
||
headless=headless
|
||
)
|
||
|
||
print(f"Task: {task}")
|
||
result = await agent.run(max_steps=max_steps)
|
||
|
||
print(f"\nResult: {'Success' if result['success'] else 'Failed'}")
|
||
print(f"Time: {result['execution_time']:.2f}s")
|
||
print(f"LLM calls: {result.get('llm_calls', 0)}")
|
||
|
||
|
||
def build_parser() -> argparse.ArgumentParser:
|
||
"""构建命令行参数解析器(RPA 邮件学习/回放演示)。"""
|
||
parser = argparse.ArgumentParser(
|
||
prog="demo_email.py",
|
||
description=(
|
||
"browser-use RPA 演示:学习一次「发送邮件」工作流,之后用不同参数高速回放。\n"
|
||
"第一阶段(学习)通过多模态大模型逐步探索并录制工作流;\n"
|
||
"第二阶段(回放)直接复用工作流、无需再调用大模型,对比耗时与调用次数。"
|
||
),
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog=(
|
||
"示例:\n"
|
||
" python demo_email.py # 运行完整的「学习→回放」对比演示\n"
|
||
" python demo_email.py --quick # 只跑一次简单任务,快速冒烟测试\n"
|
||
" python demo_email.py --model gemini-2.0-flash-exp --headless\n"
|
||
" python demo_email.py --task '给 a@b.com 发主题为\"报告\"的邮件' \\\n"
|
||
" --replay-task '给 c@d.com 发主题为\"周报\"的邮件' \\\n"
|
||
" --output results.json\n"
|
||
),
|
||
)
|
||
parser.add_argument(
|
||
"--task", default=None,
|
||
help="学习阶段的任务描述(默认:向 test@example.com 发送测试邮件)",
|
||
)
|
||
parser.add_argument(
|
||
"--replay-task", default=None,
|
||
help="回放阶段的任务描述,参数不同但流程相同(默认:向 another@example.com 发送邮件)",
|
||
)
|
||
parser.add_argument(
|
||
"--model", default=DEFAULT_MODEL,
|
||
help="使用的大模型,gpt-* 走 OpenAI(缺 Key 时走 OpenRouter 兜底),"
|
||
"gemini-* 走 Google(默认:gpt-5.6-luna)",
|
||
)
|
||
parser.add_argument(
|
||
"--headless", action="store_true",
|
||
help="以无界面(headless)模式运行浏览器(默认:显示浏览器窗口)",
|
||
)
|
||
parser.add_argument(
|
||
"--knowledge-base", default="./email_knowledge", metavar="PATH",
|
||
help="工作流知识库的存储目录(默认:./email_knowledge)",
|
||
)
|
||
parser.add_argument(
|
||
"--max-steps", type=int, default=20, metavar="N",
|
||
help="学习阶段允许的最大操作步数(默认:20)",
|
||
)
|
||
parser.add_argument(
|
||
"--output", default=None, metavar="PATH",
|
||
help="将学习/回放的指标对比与知识库统计写入指定 JSON 文件",
|
||
)
|
||
parser.add_argument(
|
||
"--quick", action="store_true",
|
||
help="快速冒烟测试:只运行一次简单任务,不做学习/回放对比",
|
||
)
|
||
return parser
|
||
|
||
|
||
def main():
|
||
"""Main entry point."""
|
||
args = build_parser().parse_args()
|
||
|
||
if args.quick:
|
||
# Run quick test (single task, no replay comparison)
|
||
asyncio.run(quick_test(
|
||
model=args.model,
|
||
headless=args.headless,
|
||
max_steps=args.max_steps,
|
||
knowledge_base_path=args.knowledge_base,
|
||
task=args.task,
|
||
))
|
||
else:
|
||
# Run full learning + replay demo
|
||
demo = EmailDemo(
|
||
llm_model=args.model,
|
||
knowledge_base_path=args.knowledge_base,
|
||
headless=args.headless,
|
||
max_steps=args.max_steps,
|
||
learning_task=args.task,
|
||
replay_task=args.replay_task,
|
||
output_path=args.output,
|
||
)
|
||
asyncio.run(demo.run_full_demo())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|