跳转至

browser-use-rpa

第8章 · Agent 的自我进化 · 配套项目 chapter8/browser-use-rpa

项目说明

Browser-Use RPA with Learning Capability

能操作电脑,并且越做越熟练的 Agent

项目概述

本项目实现了一个具有学习能力的浏览器自动化Agent。该Agent能够:

  1. 学习阶段:通过多模态大模型(GPT-4o, Claude, Gemini等)完成新任务,并捕获成功的操作流程
  2. 应用阶段:识别相似任务,直接回放已学习的工作流,无需再次调用大模型
  3. 持续改进:记录执行指标,不断优化知识库

架构设计

browser-use-rpa/
├── browser-use/          # Browser-use 核心库(未修改)
├── learning_agent/       # 学习Agent封装层
│   ├── agent.py         # 主Agent类,封装browser-use
│   ├── workflow.py      # 工作流数据结构
│   ├── knowledge_base.py # 知识库管理
│   └── replay.py        # 工作流回放器
├── demo_weather.py      # 天气查询演示
├── demo_email.py        # 邮件发送演示
└── knowledge_base/      # 存储学习到的工作流

核心组件

1. LearningAgent (agent.py)

  • 封装browser-use的Agent类
  • 拦截并记录每个操作步骤
  • 提取稳定的XPath选择器
  • 管理学习和回放模式

2. Workflow (workflow.py)

  • 定义工作流数据结构
  • 支持参数化(如不同的收件人、主题等)
  • 记录元素选择器和操作参数

3. KnowledgeBase (knowledge_base.py)

  • 持久化存储工作流
  • 意图匹配算法
  • 性能指标跟踪

4. WorkflowReplayer (replay.py)

  • 使用Playwright直接控制浏览器
  • 智能等待元素加载
  • 错误恢复机制

安装

# 1. 安装依赖
pip install -r requirements.txt

# 2. 安装Playwright浏览器
playwright install chromium

# 3. 配置环境变量
cp env.example .env
# 编辑.env文件,添加你的API密钥

使用示例

基本用法

from learning_agent import LearningAgent
from llm_factory import make_llm  # 包装层 LLM 工厂:OpenAI 直连,缺 Key 时 OpenRouter 兜底

# 创建学习Agent
agent = LearningAgent(
    task="发送邮件给test@example.com,主题是'测试',内容是'这是一封测试邮件'",
    llm=make_llm(),  # 默认 gpt-5.6-luna;无 OPENAI_API_KEY 时走 OpenRouter 兜底
    knowledge_base_path="./knowledge_base",
    headless=False  # 显示浏览器界面
)

# 执行任务
result = agent.run_sync(max_steps=20)

print(f"任务完成: {'成功' if result['success'] else '失败'}")
print(f"执行时间: {result['execution_time']:.2f}秒")
print(f"是否使用已学习的工作流: {result['replay_used']}")

运行演示

demo_email.py 是本实验的主入口,演示「学习一次工作流 → 用不同参数高速回放」这一核心思想。 它提供了完整的中文命令行接口,运行 python demo_email.py --help 可查看所有参数:

# 运行完整的「学习 → 回放」对比演示(默认行为,会打开浏览器)
python demo_email.py

# 快速冒烟测试:只跑一次简单任务,不做学习/回放对比
python demo_email.py --quick

# 无界面模式 + 使用 Gemini 模型
python demo_email.py --model gemini-2.0-flash-exp --headless

# 自定义两个阶段的任务,并把指标对比写入 JSON 文件
python demo_email.py \
    --task '给 a@b.com 发主题为"报告"的邮件' \
    --replay-task '给 c@d.com 发主题为"周报"的邮件' \
    --output results.json

# 天气查询演示(另一个更轻量的例子)
python demo_weather.py

命令行参数说明(demo_email.py):

参数 说明 默认值
--task 学习阶段的任务描述 向 test@example.com 发送测试邮件
--replay-task 回放阶段的任务描述(参数不同、流程相同) 向 another@example.com 发送邮件
--model 大模型;gpt-* 走 OpenAI(缺 Key 时 OpenRouter 兜底),gemini-* 走 Google gpt-5.6-luna
--headless 以无界面模式运行浏览器 显示窗口
--knowledge-base 工作流知识库存储目录 ./email_knowledge
--max-steps 学习阶段最大操作步数 20
--output 把学习/回放指标与知识库统计写入 JSON 文件 不写出
--quick 快速冒烟测试模式 关闭

预期结果: 第一次运行时,Agent 处于学习阶段,会通过多模态大模型逐步探索并录制「发送邮件」工作流, 耗时较长且产生多次 LLM 调用;随后回放阶段用新的收件人/主题参数复用同一工作流,几乎不再调用大模型, 耗时显著下降。演示结束会打印两个阶段的耗时、LLM 调用次数与知识库统计;若指定了 --output, 上述对比会以结构化 JSON 保存,便于复盘“学习一次、复用多次”带来的成本收益。

注意:完整运行需要有效的模型 API Key(见 .env)以及本地可用的浏览器(playwright install chromium)。 --help 与参数解析本身无需上述依赖即可查看。

验收标准测试

1. 首次任务执行(学习阶段)

运行 demo_email.py,观察第一阶段:

  • Agent通过"观察-思考-行动"循环完成任务
  • 每步操作都需要调用大模型
  • 成功后自动保存工作流到知识库
  • 显示执行时间和步骤数

示例输出:

📚 PHASE 1: LEARNING - First Email Task
Task: Send email to test@example.com
🚀 Starting learning phase...
✅ Learning phase completed!
   - Success: ✓
   - Execution time: 35.2 seconds
   - LLM calls made: 12
   - Workflow captured: Yes

2. 重复任务执行(应用阶段)

继续观察第二阶段:

  • Agent识别相似任务,匹配已学习的工作流
  • 直接回放操作步骤,无需调用大模型
  • 自动填充新的参数(收件人、主题等)
  • 执行速度显著提升

示例输出:

🚀 PHASE 2: REPLAY - Second Email Task
Task: Send email to another@example.com
🔄 Starting replay phase...
✅ Replay phase completed!
   - Success: ✓
   - Execution time: 8.5 seconds
   - Workflow reused: Yes

🎯 Performance Improvements:
   - Speed: 4.1x faster
   - LLM calls saved: 12
   - Time saved: 26.7 seconds

技术特点

1. 稳定的元素定位

  • XPath优先:捕获元素的完整XPath路径,对页面结构变化有较好的鲁棒性
  • 多重回退:XPath失败时尝试CSS选择器、属性选择器等
  • 智能等待:使用wait_for(state='visible')确保元素加载完成

2. 工作流捕获机制

# 从browser-use的内部状态提取元素信息
element = selector_map[index]
workflow_step = WorkflowStep(
    action_type=ActionType.CLICK,
    xpath=element.xpath,
    element_attributes={
        'id': element.attributes.get('id'),
        'class': element.attributes.get('class'),
        ...
    }
)

3. 意图匹配算法

  • 关键词匹配
  • 动词识别(send, write, check等)
  • 成功率加权
  • 置信度评分

性能对比

指标 学习阶段 回放阶段 提升
执行时间 30-40秒 5-10秒 3-5倍
LLM调用次数 10-15次 0次 100%
成功率 85% 95%+ 10%+

知识库管理

查看知识库统计:

from learning_agent import KnowledgeBase

kb = KnowledgeBase("./knowledge_base")
stats = kb.get_statistics()
print(stats)
# {
#     'total_workflows': 5,
#     'total_executions': 23,
#     'success_rate': '91.3%',
#     'total_model_calls_saved': 156
# }

清空知识库:

kb.clear_all()  # 谨慎使用

限制和注意事项

  1. 动态内容:对于高度动态的页面,XPath可能会变化
  2. 认证状态:不会保存登录状态,每次都从头开始
  3. 复杂交互:暂不支持拖拽、右键菜单等复杂操作
  4. 多标签页:回放模式简化了标签页处理

扩展建议

  1. 更智能的参数提取:使用NLP模型提取任务参数
  2. 工作流组合:将多个小工作流组合成复杂任务
  3. 错误恢复:增强回放失败时的恢复策略
  4. 分布式知识库:支持团队共享学习成果

贡献

欢迎提交Issue和Pull Request来改进本项目。

许可

本项目基于browser-use开发,遵循其开源许可协议。

源代码

demo_email.py

"""
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()

demo_weather.py

"""
Demo: Weather checking with learning capability

This demo shows how the agent learns to check weather and reuses the learned workflow.
"""

import asyncio
import logging
from dotenv import load_dotenv

from browser_use import ChatOpenAI
from learning_agent import LearningAgent
from llm_factory import make_llm


# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

load_dotenv()


async def demo_weather_learning():
    """Demonstrate weather checking with learning."""

    print("=" * 60)
    print("WEATHER CHECKING DEMO - LEARNING AGENT")
    print("=" * 60)

    # First task - agent will learn
    print("\n📚 PHASE 1: LEARNING - First weather check (Beijing)")
    print("-" * 40)

    task1 = "Check the weather in Beijing"

    agent1 = LearningAgent(
        task=task1,
        llm=make_llm(),
        knowledge_base_path="./weather_knowledge",
        headless=False  # Show browser for demo
    )

    print(f"Task: {task1}")
    print("The agent will use browser-use to complete this task from scratch...")

    result1 = await agent1.run(max_steps=10)

    print(f"\n✅ Task completed!")
    print(f"  - Success: {result1['success']}")
    print(f"  - Execution time: {result1['execution_time']:.2f}s")
    print(f"  - LLM calls made: {result1['llm_calls']}")
    print(f"  - Workflow learned: {'Yes' if result1['success'] else 'No'}")

    # Wait a bit before second task
    await asyncio.sleep(3)

    # Second task - agent should reuse learned workflow
    print("\n🚀 PHASE 2: REPLAY - Second weather check (Shanghai)")
    print("-" * 40)

    task2 = "Check the weather in Shanghai"

    agent2 = LearningAgent(
        task=task2,
        llm=make_llm(),
        knowledge_base_path="./weather_knowledge",
        headless=False
    )

    print(f"Task: {task2}")
    print("The agent will try to reuse the learned workflow...")

    result2 = await agent2.run(max_steps=10)

    print(f"\n✅ Task completed!")
    print(f"  - Success: {result2['success']}")
    print(f"  - Execution time: {result2['execution_time']:.2f}s")
    print(f"  - Replay used: {result2['replay_used']}")

    if result2['replay_used']:
        print(f"  - LLM calls saved: {result1['llm_calls']}")
        speedup = result1['execution_time'] / result2['execution_time']
        print(f"  - Speed improvement: {speedup:.1f}x faster")
    else:
        print(f"  - LLM calls made: {result2['llm_calls']}")

    # Show knowledge base statistics
    print("\n📊 KNOWLEDGE BASE STATISTICS")
    print("-" * 40)

    kb = agent2.knowledge_base
    stats = kb.get_statistics()

    for key, value in stats.items():
        print(f"  - {key.replace('_', ' ').title()}: {value}")

    print("\n" + "=" * 60)
    print("DEMO COMPLETED")
    print("=" * 60)


if __name__ == "__main__":
    asyncio.run(demo_weather_learning())

learning_agent/__init__.py

"""
Learning Agent for Browser-Use RPA

A wrapper system that adds learning and experience-replay capabilities to browser-use.
This agent can learn from successful task executions and replay learned workflows efficiently.
"""

from .agent import LearningAgent
from .knowledge_base import KnowledgeBase
from .workflow import Workflow, WorkflowStep

__all__ = ['LearningAgent', 'KnowledgeBase', 'Workflow', 'WorkflowStep']

learning_agent/agent.py

"""
Learning Agent that extends browser-use with experience-based learning.

This agent wraps the browser-use Agent to capture successful workflows
and replay them efficiently without LLM calls.
"""

import asyncio
import logging
from typing import Any, Dict, List, Optional
import time
from pathlib import Path
import sys
import os

# Add parent directory to path to import browser-use
sys.path.append(str(Path(__file__).parent.parent))

from browser_use import Agent, Browser
from browser_use.agent.views import AgentOutput, ActionResult
from browser_use.browser.views import BrowserStateSummary

from .workflow import Workflow, WorkflowStep, ActionType
from .knowledge_base import KnowledgeBase
from .replay import WorkflowReplayer


logger = logging.getLogger(__name__)


class LearningAgent:
    """
    An agent that learns from experience and can replay learned workflows.

    This agent:
    1. Attempts to match tasks to learned workflows
    2. Falls back to browser-use Agent for new tasks
    3. Captures successful executions as new workflows
    4. Improves over time by building a knowledge base
    """

    def __init__(self, 
                 task: str,
                 llm: Any,
                 browser: Optional[Browser] = None,
                 knowledge_base_path: str = "./knowledge_base",
                 headless: bool = False,
                 **agent_kwargs):
        """
        Initialize the learning agent.

        Args:
            task: The task to be performed
            llm: Language model to use (browser-use compatible)
            browser: Browser instance (optional)
            knowledge_base_path: Path to store learned workflows
            headless: Whether to run browser in headless mode for replay
            **agent_kwargs: Additional arguments for browser-use Agent
        """
        self.task = task
        self.llm = llm
        self.browser = browser
        self.headless = headless

        # Initialize knowledge base
        self.knowledge_base = KnowledgeBase(knowledge_base_path)

        # Initialize workflow replayer
        self.replayer = WorkflowReplayer(headless=headless)

        # Workflow capture state
        self.current_workflow: Optional[Workflow] = None
        self.is_learning = False
        self.captured_steps: List[Dict[str, Any]] = []

        # Browser-use agent (lazy initialization)
        self._agent: Optional[Agent] = None
        self._agent_kwargs = agent_kwargs

        # Metrics
        self.metrics = {
            "llm_calls": 0,
            "replay_used": False,
            "execution_time": 0,
            "success": False
        }

    @property
    def agent(self) -> Agent:
        """Lazy initialization of browser-use agent."""
        if self._agent is None:
            # Create agent with step callback for capturing
            self._agent = Agent(
                task=self.task,
                llm=self.llm,
                browser=self.browser,
                **self._agent_kwargs
            )

            # Store original step method
            self._original_step = self._agent.step

            # Monkey-patch the step method to capture actions
            self._agent.step = self._wrapped_step

        return self._agent

    async def _wrapped_step(self, step_info=None):
        """Wrapped step method that captures workflow information."""
        # Call original step
        await self._original_step(step_info)

        # Capture step information if learning
        if self.is_learning:
            await self._capture_step()

    async def _capture_step(self):
        """Capture the current step for workflow learning."""
        try:
            # Get the last action and result from agent state
            if self.agent.state.last_model_output and self.agent.state.last_result:
                model_output = self.agent.state.last_model_output
                results = self.agent.state.last_result

                # Get browser state for element information
                browser_state = await self.agent.browser_session.get_browser_state_summary()

                # Process each action in the step
                for i, (action, result) in enumerate(zip(model_output.action, results)):
                    if result and not result.error:
                        # Extract action details
                        action_data = self._extract_action_data(action, result, browser_state)
                        if action_data:
                            self.captured_steps.append(action_data)
                            logger.debug(f"Captured step: {action_data['type']}")

        except Exception as e:
            logger.error(f"Failed to capture step: {e}")

    def _extract_action_data(self, action: Any, result: ActionResult, browser_state: BrowserStateSummary) -> Optional[Dict[str, Any]]:
        """
        Extract action data for workflow capture.

        Args:
            action: The action object from browser-use
            result: The result of the action
            browser_state: Current browser state

        Returns:
            Dictionary containing action data, or None if extraction fails
        """
        try:
            action_dict = action.model_dump() if hasattr(action, 'model_dump') else {}

            # Determine action type
            action_type = None
            parameters = {}
            element_info = None

            # Parse different action types
            if 'go_to_url' in action_dict:
                action_type = ActionType.NAVIGATE
                parameters = {'url': action_dict['go_to_url'].get('url')}

            elif 'click_element' in action_dict:
                action_type = ActionType.CLICK
                click_data = action_dict['click_element']
                parameters = {
                    'while_holding_ctrl': click_data.get('while_holding_ctrl', False)
                }

                # Get element info from selector map
                index = click_data.get('index')
                if index and browser_state.dom_state.selector_map:
                    element_info = self._get_element_info(index, browser_state.dom_state.selector_map)

            elif 'input_text' in action_dict:
                action_type = ActionType.INPUT_TEXT
                input_data = action_dict['input_text']
                parameters = {
                    'text': input_data.get('text', ''),
                    'clear_existing': input_data.get('clear_existing', True)
                }

                # Get element info
                index = input_data.get('index')
                if index and browser_state.dom_state.selector_map:
                    element_info = self._get_element_info(index, browser_state.dom_state.selector_map)

            elif 'select_dropdown_option' in action_dict:
                action_type = ActionType.SELECT_OPTION
                select_data = action_dict['select_dropdown_option']
                parameters = {
                    'text': select_data.get('text', '')
                }

                index = select_data.get('index')
                if index and browser_state.dom_state.selector_map:
                    element_info = self._get_element_info(index, browser_state.dom_state.selector_map)

            elif 'scroll' in action_dict:
                action_type = ActionType.SCROLL
                scroll_data = action_dict['scroll']
                parameters = {
                    'down': scroll_data.get('down', True),
                    'num_pages': scroll_data.get('num_pages', 1)
                }

            elif 'upload_file' in action_dict:
                action_type = ActionType.UPLOAD_FILE
                upload_data = action_dict['upload_file']
                parameters = {
                    'path': upload_data.get('path', '')
                }

                index = upload_data.get('index')
                if index and browser_state.dom_state.selector_map:
                    element_info = self._get_element_info(index, browser_state.dom_state.selector_map)

            elif 'done' in action_dict:
                # Skip done action for workflow
                return None

            if action_type:
                return {
                    'type': action_type,
                    'parameters': parameters,
                    'element_info': element_info,
                    'url': browser_state.url
                }

        except Exception as e:
            logger.error(f"Failed to extract action data: {e}")

        return None

    def _get_element_info(self, index: int, selector_map: Dict) -> Optional[Dict[str, Any]]:
        """
        Get element information from selector map.

        Args:
            index: Element index
            selector_map: Browser-use selector map

        Returns:
            Dictionary containing element selectors and attributes
        """
        try:
            if index in selector_map:
                element = selector_map[index]

                # Extract stable selectors
                info = {
                    'xpath': getattr(element, 'xpath', None),
                    'attributes': {}
                }

                # Get important attributes
                if hasattr(element, 'attributes') and element.attributes:
                    for attr in ['id', 'name', 'class', 'type', 'role', 'aria-label', 'data-testid']:
                        if attr in element.attributes:
                            info['attributes'][attr] = element.attributes[attr]

                return info

        except Exception as e:
            logger.error(f"Failed to get element info: {e}")

        return None

    async def run(self, max_steps: int = 100) -> Dict[str, Any]:
        """
        Run the learning agent to complete the task.

        Args:
            max_steps: Maximum steps for browser-use agent

        Returns:
            Dictionary containing execution results and metrics
        """
        start_time = time.time()

        try:
            # Check if we have a learned workflow for this task
            match = self.knowledge_base.find_workflow_for_task(self.task)

            if match and match.confidence > 0.6:
                # Use learned workflow
                logger.info(f"Found matching workflow: '{match.workflow.intent}' "
                          f"(confidence: {match.confidence:.2f})")
                logger.info(f"Match reason: {match.match_reason}")

                result = await self._run_with_replay(match.workflow)

                # Update metrics
                self.knowledge_base.update_workflow_metrics(
                    match.workflow.workflow_id,
                    success=result['success'],
                    execution_time=result['execution_time'],
                    model_calls_saved=result['model_calls_saved']
                )

                self.metrics['replay_used'] = True
                self.metrics['success'] = result['success']

                # If replay failed, fall back to learning mode
                if not result['success']:
                    logger.warning("Replay failed, falling back to learning mode")
                    result = await self._run_with_learning(max_steps)

            else:
                # No matching workflow, run in learning mode
                logger.info("No matching workflow found, running in learning mode")
                result = await self._run_with_learning(max_steps)

        finally:
            self.metrics['execution_time'] = time.time() - start_time

            # Log performance comparison
            if self.metrics['replay_used']:
                logger.info(f"Task completed with replay in {self.metrics['execution_time']:.2f}s")
                logger.info(f"Model calls saved: {result.get('model_calls_saved', 0)}")
            else:
                logger.info(f"Task completed with learning in {self.metrics['execution_time']:.2f}s")
                logger.info(f"LLM calls made: {self.metrics['llm_calls']}")

        return self.metrics

    async def _run_with_replay(self, workflow: Workflow) -> Dict[str, Any]:
        """
        Run task using a learned workflow.

        Args:
            workflow: The workflow to replay

        Returns:
            Execution results
        """
        logger.info("Replaying learned workflow...")

        # Extract parameters from task if needed
        parameters = self._extract_task_parameters(self.task, workflow)

        # Setup replayer
        await self.replayer.setup()

        try:
            # Replay workflow
            result = await self.replayer.replay_workflow(
                workflow,
                parameters=parameters
            )

            logger.info(f"Replay completed: {result['steps_completed']}/{result['total_steps']} steps")

            return result

        finally:
            await self.replayer.cleanup()

    async def _run_with_learning(self, max_steps: int) -> Dict[str, Any]:
        """
        Run task with browser-use agent and capture workflow.

        Args:
            max_steps: Maximum steps for agent

        Returns:
            Execution results
        """
        logger.info("Running with browser-use agent (learning mode)...")

        # Enable learning mode
        self.is_learning = True
        self.captured_steps = []

        # Track LLM calls
        original_get_model_output = self.agent.get_model_output

        async def tracked_get_model_output(*args, **kwargs):
            self.metrics['llm_calls'] += 1
            return await original_get_model_output(*args, **kwargs)

        self.agent.get_model_output = tracked_get_model_output

        try:
            # Run the agent
            await self.agent.run(max_steps=max_steps)

            # Check if task was successful
            success = False
            if self.agent.state.last_result:
                for result in self.agent.state.last_result:
                    if result and hasattr(result, 'success') and result.success:
                        success = True
                        break

            self.metrics['success'] = success

            # If successful, save the workflow
            if success and self.captured_steps:
                await self._save_learned_workflow()

            return {
                'success': success,
                'steps_completed': len(self.captured_steps),
                'total_steps': len(self.captured_steps),
                'execution_time': self.metrics['execution_time'],
                'model_calls_saved': 0
            }

        finally:
            self.is_learning = False

    async def _save_learned_workflow(self):
        """Save the captured workflow to knowledge base."""
        try:
            # Create workflow from captured steps
            workflow = Workflow(
                workflow_id="",  # Will be generated
                intent=self.task,
                description=f"Learned workflow for: {self.task}",
                initial_url=self.captured_steps[0].get('url') if self.captured_steps else None
            )

            # Convert captured steps to workflow steps
            for step_data in self.captured_steps:
                step = WorkflowStep(
                    action_type=step_data['type'],
                    parameters=step_data['parameters']
                )

                # Add element info if available
                if step_data.get('element_info'):
                    element_info = step_data['element_info']
                    step.xpath = element_info.get('xpath')
                    step.element_attributes = element_info.get('attributes', {})

                workflow.add_step(step)

            # Save to knowledge base
            self.knowledge_base.save_workflow(workflow)

            logger.info(f"Saved learned workflow with {len(workflow.steps)} steps")

        except Exception as e:
            logger.error(f"Failed to save learned workflow: {e}")

    def _extract_task_parameters(self, task: str, workflow: Workflow) -> Dict[str, Any]:
        """
        Extract parameters from task description for workflow.

        Args:
            task: Task description
            workflow: Workflow that needs parameters

        Returns:
            Dictionary of extracted parameters
        """
        # This is a simplified parameter extraction
        # In production, you might use NLP or regex patterns
        parameters = {}

        # Example: Extract email addresses
        import re
        email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
        emails = re.findall(email_pattern, task)
        if emails:
            parameters['recipient'] = emails[0]

        # Extract quoted text as subject or content
        quoted = re.findall(r'"([^"]*)"', task)
        if quoted:
            if 'subject' in task.lower() or '主题' in task.lower():
                parameters['subject'] = quoted[0]
                if len(quoted) > 1:
                    parameters['content'] = quoted[1]
            else:
                parameters['content'] = quoted[0]

        return parameters

    def run_sync(self, max_steps: int = 100) -> Dict[str, Any]:
        """
        Synchronous wrapper for run method.

        Args:
            max_steps: Maximum steps for browser-use agent

        Returns:
            Execution results
        """
        return asyncio.run(self.run(max_steps))

learning_agent/knowledge_base.py

"""
Knowledge Base for storing and retrieving learned workflows.

This module provides persistent storage and intelligent retrieval of workflows,
including intent matching and workflow selection.
"""

import json
import os
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from datetime import datetime
import logging
from dataclasses import dataclass
import uuid

from .workflow import Workflow, WorkflowStep


logger = logging.getLogger(__name__)


@dataclass
class IntentMatch:
    """Represents a match between a task intent and a stored workflow"""
    workflow: Workflow
    confidence: float  # 0.0 to 1.0
    match_reason: str


class KnowledgeBase:
    """
    Manages storage and retrieval of learned workflows.

    The knowledge base provides:
    - Persistent storage of workflows
    - Intent matching to find relevant workflows
    - Performance tracking and optimization
    """

    def __init__(self, storage_path: str = "./knowledge_base"):
        """
        Initialize the knowledge base.

        Args:
            storage_path: Directory path for storing workflow data
        """
        self.storage_path = Path(storage_path)
        self.storage_path.mkdir(exist_ok=True)

        # In-memory cache of workflows
        self.workflows: Dict[str, Workflow] = {}

        # Intent index for fast matching
        self.intent_index: Dict[str, List[str]] = {}  # intent -> [workflow_ids]

        # Load existing workflows
        self.load_all_workflows()

    def save_workflow(self, workflow: Workflow) -> None:
        """
        Save a workflow to persistent storage.

        Args:
            workflow: The workflow to save
        """
        # Generate ID if not present
        if not workflow.workflow_id:
            workflow.workflow_id = str(uuid.uuid4())

        # Save to file
        workflow_file = self.storage_path / f"workflow_{workflow.workflow_id}.json"
        with open(workflow_file, 'w', encoding='utf-8') as f:
            f.write(workflow.to_json())

        # Update in-memory cache
        self.workflows[workflow.workflow_id] = workflow

        # Update intent index
        if workflow.intent not in self.intent_index:
            self.intent_index[workflow.intent] = []
        if workflow.workflow_id not in self.intent_index[workflow.intent]:
            self.intent_index[workflow.intent].append(workflow.workflow_id)

        logger.info(f"Saved workflow '{workflow.workflow_id}' for intent: {workflow.intent}")

    def load_all_workflows(self) -> None:
        """Load all workflows from storage into memory."""
        workflow_files = list(self.storage_path.glob("workflow_*.json"))

        for workflow_file in workflow_files:
            try:
                with open(workflow_file, 'r', encoding='utf-8') as f:
                    workflow_data = json.load(f)
                    workflow = Workflow.from_dict(workflow_data)

                    # Add to cache
                    self.workflows[workflow.workflow_id] = workflow

                    # Update intent index
                    if workflow.intent not in self.intent_index:
                        self.intent_index[workflow.intent] = []
                    self.intent_index[workflow.intent].append(workflow.workflow_id)

            except Exception as e:
                logger.error(f"Failed to load workflow from {workflow_file}: {e}")

        logger.info(f"Loaded {len(self.workflows)} workflows from storage")

    def find_workflow_for_task(self, task_description: str) -> Optional[IntentMatch]:
        """
        Find the best matching workflow for a given task.

        Args:
            task_description: Natural language description of the task

        Returns:
            The best matching workflow with confidence score, or None if no match
        """
        matches = self.find_matching_workflows(task_description)

        if matches:
            # Return the highest confidence match
            return max(matches, key=lambda m: m.confidence)

        return None

    def find_matching_workflows(self, task_description: str) -> List[IntentMatch]:
        """
        Find all workflows that might match the given task.

        Args:
            task_description: Natural language description of the task

        Returns:
            List of matching workflows sorted by confidence
        """
        matches = []

        # Normalize task description for matching
        task_lower = task_description.lower()

        for workflow in self.workflows.values():
            confidence, reason = self._calculate_match_confidence(task_lower, workflow)

            if confidence > 0.3:  # Minimum threshold
                matches.append(IntentMatch(
                    workflow=workflow,
                    confidence=confidence,
                    match_reason=reason
                ))

        # Sort by confidence (highest first)
        matches.sort(key=lambda m: m.confidence, reverse=True)

        return matches

    def _calculate_match_confidence(self, task: str, workflow: Workflow) -> Tuple[float, str]:
        """
        Calculate how well a workflow matches a task description.

        Args:
            task: Normalized task description
            workflow: Workflow to match against

        Returns:
            Tuple of (confidence_score, match_reason)
        """
        confidence = 0.0
        reasons = []

        # Check intent match
        intent_lower = workflow.intent.lower()

        # Exact intent match
        if intent_lower in task:
            confidence += 0.5
            reasons.append("exact intent match")

        # Keyword matching for common patterns
        intent_keywords = set(intent_lower.split())
        task_keywords = set(task.split())

        # Calculate keyword overlap
        common_keywords = intent_keywords & task_keywords
        if common_keywords:
            keyword_score = len(common_keywords) / len(intent_keywords)
            confidence += keyword_score * 0.3
            reasons.append(f"keyword match: {', '.join(common_keywords)}")

        # Check for action verbs (send, write, compose, create, etc.)
        action_verbs = {
            'send': ['send', 'email', 'mail', 'message'],
            'write': ['write', 'compose', 'draft', 'create'],
            'search': ['search', 'find', 'look', 'query'],
            'check': ['check', 'verify', 'view', 'see'],
            'login': ['login', 'signin', 'authenticate', 'log in', 'sign in'],
            'order': ['order', 'buy', 'purchase', 'checkout'],
            'book': ['book', 'reserve', 'schedule']
        }

        for action_group, verbs in action_verbs.items():
            if any(verb in intent_lower for verb in verbs) and any(verb in task for verb in verbs):
                confidence += 0.2
                reasons.append(f"action verb match: {action_group}")
                break

        # Boost confidence for recently successful workflows
        if workflow.success_count > workflow.failure_count:
            success_rate = workflow.success_count / (workflow.success_count + workflow.failure_count)
            confidence *= (1 + success_rate * 0.2)
            if success_rate > 0.8:
                reasons.append(f"high success rate: {success_rate:.0%}")

        # Compile reason string
        reason = "; ".join(reasons) if reasons else "partial match"

        return confidence, reason

    def update_workflow_metrics(self, 
                               workflow_id: str, 
                               success: bool,
                               execution_time: float,
                               model_calls_saved: int = 0) -> None:
        """
        Update performance metrics for a workflow after execution.

        Args:
            workflow_id: ID of the workflow that was executed
            success: Whether the execution was successful
            execution_time: Time taken to execute the workflow
            model_calls_saved: Number of LLM calls saved by using this workflow
        """
        if workflow_id in self.workflows:
            workflow = self.workflows[workflow_id]

            # Update counters
            if success:
                workflow.success_count += 1
            else:
                workflow.failure_count += 1

            # Update timing
            workflow.last_used_at = datetime.now()

            # Update average execution time
            total_executions = workflow.success_count + workflow.failure_count
            workflow.average_execution_time = (
                (workflow.average_execution_time * (total_executions - 1) + execution_time) 
                / total_executions
            )

            # Track model calls saved
            workflow.model_calls_saved += model_calls_saved

            # Save updated workflow
            self.save_workflow(workflow)

            logger.info(f"Updated metrics for workflow {workflow_id}: "
                       f"success={success}, time={execution_time:.2f}s, "
                       f"total_saved_calls={workflow.model_calls_saved}")

    def get_statistics(self) -> Dict[str, any]:
        """
        Get statistics about the knowledge base.

        Returns:
            Dictionary containing knowledge base statistics
        """
        total_workflows = len(self.workflows)
        total_executions = sum(w.success_count + w.failure_count for w in self.workflows.values())
        total_successes = sum(w.success_count for w in self.workflows.values())
        total_model_calls_saved = sum(w.model_calls_saved for w in self.workflows.values())

        success_rate = (total_successes / total_executions * 100) if total_executions > 0 else 0

        return {
            "total_workflows": total_workflows,
            "total_executions": total_executions,
            "total_successes": total_successes,
            "success_rate": f"{success_rate:.1f}%",
            "total_model_calls_saved": total_model_calls_saved,
            "unique_intents": len(self.intent_index)
        }

    def clear_all(self) -> None:
        """Clear all workflows from the knowledge base (use with caution)."""
        # Clear files
        for workflow_file in self.storage_path.glob("workflow_*.json"):
            workflow_file.unlink()

        # Clear memory
        self.workflows.clear()
        self.intent_index.clear()

        logger.info("Cleared all workflows from knowledge base")

learning_agent/replay.py

"""
Workflow replay functionality using Playwright.

This module provides reliable replay of learned workflows by directly
controlling the browser through Playwright, bypassing the need for LLM calls.
"""

import asyncio
import logging
from typing import Any, Dict, Optional
from playwright.async_api import Page, Browser, async_playwright, Locator
import time

from .workflow import Workflow, WorkflowStep, ActionType


logger = logging.getLogger(__name__)


class WorkflowReplayer:
    """
    Replays learned workflows using Playwright for direct browser control.

    This replayer:
    - Executes workflows without LLM calls
    - Handles dynamic page loading with smart waits
    - Provides robust error recovery
    - Tracks execution metrics
    """

    def __init__(self, headless: bool = False):
        """
        Initialize the workflow replayer.

        Args:
            headless: Whether to run browser in headless mode
        """
        self.headless = headless
        self.browser: Optional[Browser] = None
        self.page: Optional[Page] = None
        self.context = None
        self.playwright = None

    async def setup(self):
        """Initialize Playwright and browser."""
        self.playwright = await async_playwright().start()
        self.browser = await self.playwright.chromium.launch(
            headless=self.headless,
            args=['--disable-blink-features=AutomationControlled']
        )
        self.context = await self.browser.new_context(
            viewport={'width': 1280, 'height': 720},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        )
        self.page = await self.context.new_page()

        # Set default timeout
        self.page.set_default_timeout(30000)

        logger.info("Workflow replayer initialized")

    async def cleanup(self):
        """Clean up browser resources."""
        if self.page:
            await self.page.close()
        if self.context:
            await self.context.close()
        if self.browser:
            await self.browser.close()
        if self.playwright:
            await self.playwright.stop()

    async def replay_workflow(self, 
                             workflow: Workflow, 
                             parameters: Optional[Dict[str, Any]] = None,
                             initial_url: Optional[str] = None) -> Dict[str, Any]:
        """
        Replay a workflow with the given parameters.

        Args:
            workflow: The workflow to replay
            parameters: Parameters to apply to the workflow
            initial_url: Starting URL (uses workflow's initial_url if not provided)

        Returns:
            Dictionary containing execution results and metrics
        """
        start_time = time.time()
        results = {
            "success": False,
            "steps_completed": 0,
            "total_steps": len(workflow.steps),
            "errors": [],
            "execution_time": 0,
            "model_calls_saved": len(workflow.steps)  # Each step would require an LLM call
        }

        try:
            # Apply parameters if provided
            if parameters:
                workflow = workflow.parameterize(parameters)

            # Navigate to initial URL
            start_url = initial_url or workflow.initial_url
            if start_url:
                logger.info(f"Navigating to initial URL: {start_url}")
                await self.page.goto(start_url, wait_until='domcontentloaded')
                await self.page.wait_for_load_state('networkidle', timeout=10000)

            # Execute each step
            for i, step in enumerate(workflow.steps):
                logger.info(f"Executing step {i+1}/{len(workflow.steps)}: {step.action_type.value}")

                try:
                    await self._execute_step(step)
                    results["steps_completed"] += 1

                    # Small delay between actions for stability
                    await asyncio.sleep(0.5)

                except Exception as e:
                    error_msg = f"Step {i+1} failed: {str(e)}"
                    logger.error(error_msg)
                    results["errors"].append(error_msg)

                    # Try to recover if it's not a critical error
                    if step.action_type not in [ActionType.NAVIGATE, ActionType.CLICK]:
                        continue
                    else:
                        break

            # Check if workflow completed successfully
            results["success"] = (results["steps_completed"] == results["total_steps"])

        except Exception as e:
            logger.error(f"Workflow replay failed: {e}")
            results["errors"].append(str(e))

        finally:
            results["execution_time"] = time.time() - start_time
            logger.info(f"Workflow replay completed in {results['execution_time']:.2f}s")

        return results

    async def _execute_step(self, step: WorkflowStep) -> None:
        """
        Execute a single workflow step.

        Args:
            step: The step to execute
        """
        # Wait before action if specified
        if step.wait_before > 0:
            await asyncio.sleep(step.wait_before)

        # Execute based on action type
        if step.action_type == ActionType.NAVIGATE:
            await self._execute_navigate(step)
        elif step.action_type == ActionType.CLICK:
            await self._execute_click(step)
        elif step.action_type == ActionType.INPUT_TEXT:
            await self._execute_input(step)
        elif step.action_type == ActionType.SELECT_OPTION:
            await self._execute_select(step)
        elif step.action_type == ActionType.SCROLL:
            await self._execute_scroll(step)
        elif step.action_type == ActionType.WAIT:
            await self._execute_wait(step)
        elif step.action_type == ActionType.SWITCH_TAB:
            await self._execute_switch_tab(step)
        elif step.action_type == ActionType.UPLOAD_FILE:
            await self._execute_upload(step)
        else:
            logger.warning(f"Unsupported action type: {step.action_type}")

    async def _get_element(self, step: WorkflowStep) -> Locator:
        """
        Get element using stable selectors with fallback.

        Args:
            step: The step containing selector information

        Returns:
            Playwright Locator for the element
        """
        locator = None

        # Try XPath first (most stable)
        if step.xpath:
            try:
                locator = self.page.locator(f"xpath={step.xpath}")
                # Check if element exists
                if await locator.count() > 0:
                    # Wait for element to be ready
                    await locator.wait_for(state='visible', timeout=step.timeout * 1000)
                    return locator
            except Exception as e:
                logger.debug(f"XPath locator failed: {e}")

        # Try CSS selector as fallback
        if step.css_selector:
            try:
                locator = self.page.locator(step.css_selector)
                if await locator.count() > 0:
                    await locator.wait_for(state='visible', timeout=step.timeout * 1000)
                    return locator
            except Exception as e:
                logger.debug(f"CSS selector failed: {e}")

        # Try to build selector from attributes
        if step.element_attributes:
            selector_parts = []

            # Use ID if available
            if 'id' in step.element_attributes:
                return self.page.locator(f"#{step.element_attributes['id']}")

            # Build attribute selector
            for attr, value in step.element_attributes.items():
                if attr in ['name', 'type', 'role', 'aria-label', 'data-testid']:
                    selector_parts.append(f"[{attr}='{value}']")

            if selector_parts:
                selector = ''.join(selector_parts)
                try:
                    locator = self.page.locator(selector)
                    if await locator.count() > 0:
                        await locator.wait_for(state='visible', timeout=step.timeout * 1000)
                        return locator
                except Exception as e:
                    logger.debug(f"Attribute selector failed: {e}")

        # Try text content as last resort
        if 'text' in step.parameters:
            text = step.parameters['text']
            locator = self.page.get_by_text(text)
            if await locator.count() > 0:
                return locator

        raise Exception(f"Could not find element for step: {step.description or step.action_type}")

    async def _execute_navigate(self, step: WorkflowStep) -> None:
        """Execute navigation action."""
        url = step.parameters.get('url')
        if url:
            logger.debug(f"Navigating to: {url}")
            await self.page.goto(url, wait_until='domcontentloaded')
            await self.page.wait_for_load_state('networkidle', timeout=10000)

    async def _execute_click(self, step: WorkflowStep) -> None:
        """Execute click action with smart waiting."""
        element = await self._get_element(step)

        # Ensure element is clickable
        await element.wait_for(state='visible', timeout=step.timeout * 1000)
        await element.scroll_into_view_if_needed()

        # Check for ctrl/cmd modifier
        if step.parameters.get('while_holding_ctrl'):
            await element.click(modifiers=['Control'])
        else:
            await element.click()

        # Wait for potential navigation or dynamic updates
        try:
            await self.page.wait_for_load_state('networkidle', timeout=5000)
        except:
            pass  # Page might not navigate

    async def _execute_input(self, step: WorkflowStep) -> None:
        """Execute text input action."""
        element = await self._get_element(step)
        text = step.parameters.get('text', '')
        clear_existing = step.parameters.get('clear_existing', True)

        # Click to focus
        await element.click()

        # Clear existing text if needed
        if clear_existing:
            await element.fill('')

        # Type the text
        await element.type(text, delay=50)  # Small delay for more human-like typing

    async def _execute_select(self, step: WorkflowStep) -> None:
        """Execute select option action."""
        element = await self._get_element(step)
        option_text = step.parameters.get('text', '')

        # Try to select by visible text
        await element.select_option(label=option_text)

    async def _execute_scroll(self, step: WorkflowStep) -> None:
        """Execute scroll action."""
        direction = 'down' if step.parameters.get('down', True) else 'up'
        pages = step.parameters.get('num_pages', 1)

        # Calculate scroll amount
        viewport_height = await self.page.evaluate('window.innerHeight')
        scroll_amount = viewport_height * pages

        if direction == 'down':
            await self.page.evaluate(f'window.scrollBy(0, {scroll_amount})')
        else:
            await self.page.evaluate(f'window.scrollBy(0, -{scroll_amount})')

        # Wait for any lazy-loaded content
        await asyncio.sleep(0.5)

    async def _execute_wait(self, step: WorkflowStep) -> None:
        """Execute wait action."""
        wait_time = step.parameters.get('seconds', 1)
        await asyncio.sleep(wait_time)

    async def _execute_switch_tab(self, step: WorkflowStep) -> None:
        """Execute tab switching (simplified for single tab replay)."""
        # In replay mode, we typically work with a single tab
        # This is a placeholder for multi-tab support
        logger.debug("Tab switching in replay mode - continuing in current tab")

    async def _execute_upload(self, step: WorkflowStep) -> None:
        """Execute file upload action."""
        element = await self._get_element(step)
        file_path = step.parameters.get('path', '')

        # Set the file(s) to upload
        await element.set_input_files(file_path)

learning_agent/workflow.py

"""
Workflow data structures for capturing and storing browser action sequences.

This module defines the structures used to represent learned workflows,
including individual steps and complete action sequences.
"""

from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional
from enum import Enum
import json


class ActionType(Enum):
    """Types of actions that can be recorded in a workflow"""
    NAVIGATE = "navigate"
    CLICK = "click"
    INPUT_TEXT = "input_text"
    SELECT_OPTION = "select_option"
    SCROLL = "scroll"
    WAIT = "wait"
    SWITCH_TAB = "switch_tab"
    CLOSE_TAB = "close_tab"
    UPLOAD_FILE = "upload_file"


@dataclass
class WorkflowStep:
    """Represents a single step in a workflow"""

    action_type: ActionType

    # Stable selectors for element identification
    xpath: Optional[str] = None
    css_selector: Optional[str] = None

    # Action parameters
    parameters: Dict[str, Any] = field(default_factory=dict)

    # Additional context
    element_attributes: Dict[str, str] = field(default_factory=dict)
    description: str = ""

    # Timing information
    wait_before: float = 0.0  # Seconds to wait before executing this step
    timeout: float = 15.0  # Maximum time to wait for element to be ready

    # Validation
    expected_outcome: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert step to dictionary for serialization"""
        return {
            "action_type": self.action_type.value,
            "xpath": self.xpath,
            "css_selector": self.css_selector,
            "parameters": self.parameters,
            "element_attributes": self.element_attributes,
            "description": self.description,
            "wait_before": self.wait_before,
            "timeout": self.timeout,
            "expected_outcome": self.expected_outcome
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'WorkflowStep':
        """Create step from dictionary"""
        data = data.copy()
        data['action_type'] = ActionType(data['action_type'])
        return cls(**data)

    @classmethod
    def from_browser_action(cls, action_type: str, element: Optional[Any] = None, **params) -> 'WorkflowStep':
        """Create a workflow step from browser-use action and element info"""
        step = cls(
            action_type=ActionType(action_type.lower()),
            parameters=params
        )

        if element:
            # Extract stable selectors from DOMInteractedElement
            if hasattr(element, 'x_path'):
                step.xpath = element.x_path

            # Store relevant attributes for fallback identification
            if hasattr(element, 'attributes') and element.attributes:
                step.element_attributes = {
                    k: v for k, v in element.attributes.items()
                    if k in ['id', 'name', 'class', 'type', 'role', 'aria-label', 'data-testid']
                }

        return step


@dataclass
class Workflow:
    """Represents a complete workflow that can be learned and replayed"""

    # Identification
    workflow_id: str
    intent: str  # The task intent this workflow accomplishes

    # Steps
    steps: List[WorkflowStep] = field(default_factory=list)

    # Metadata
    created_at: datetime = field(default_factory=datetime.now)
    last_used_at: Optional[datetime] = None
    success_count: int = 0
    failure_count: int = 0

    # Learning context
    initial_url: Optional[str] = None
    example_parameters: Dict[str, Any] = field(default_factory=dict)
    description: str = ""

    # Performance metrics
    average_execution_time: float = 0.0
    model_calls_saved: int = 0

    def add_step(self, step: WorkflowStep) -> None:
        """Add a step to the workflow"""
        self.steps.append(step)

    def to_dict(self) -> Dict[str, Any]:
        """Convert workflow to dictionary for serialization"""
        return {
            "workflow_id": self.workflow_id,
            "intent": self.intent,
            "steps": [step.to_dict() for step in self.steps],
            "created_at": self.created_at.isoformat(),
            "last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
            "success_count": self.success_count,
            "failure_count": self.failure_count,
            "initial_url": self.initial_url,
            "example_parameters": self.example_parameters,
            "description": self.description,
            "average_execution_time": self.average_execution_time,
            "model_calls_saved": self.model_calls_saved
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'Workflow':
        """Create workflow from dictionary"""
        data = data.copy()
        data['steps'] = [WorkflowStep.from_dict(s) for s in data.get('steps', [])]
        data['created_at'] = datetime.fromisoformat(data['created_at'])
        if data.get('last_used_at'):
            data['last_used_at'] = datetime.fromisoformat(data['last_used_at'])
        return cls(**data)

    def to_json(self) -> str:
        """Serialize workflow to JSON string"""
        return json.dumps(self.to_dict(), indent=2, default=str)

    @classmethod
    def from_json(cls, json_str: str) -> 'Workflow':
        """Deserialize workflow from JSON string"""
        data = json.loads(json_str)
        return cls.from_dict(data)

    def parameterize(self, parameters: Dict[str, Any]) -> 'Workflow':
        """
        Create a parameterized copy of this workflow with specific values.

        Args:
            parameters: Dictionary mapping parameter names to values

        Returns:
            A new Workflow instance with parameters applied
        """
        import copy
        parameterized = copy.deepcopy(self)

        # Apply parameters to each step
        for step in parameterized.steps:
            for param_key, param_value in parameters.items():
                # Replace placeholders in step parameters
                for key, value in step.parameters.items():
                    if isinstance(value, str) and f"{{{param_key}}}" in value:
                        step.parameters[key] = value.replace(f"{{{param_key}}}", str(param_value))

        return parameterized

llm_factory.py

"""包装层 LLM 工厂:为 browser-use 示例统一提供带 OpenRouter 兜底的模型客户端。

本文件位于 RPA 包装层,**不修改** 上游 browser-use fork。它按环境变量决定用哪家:
- gemini-* 模型            -> ChatGoogle(走 GOOGLE_API_KEY)
- 有 OPENAI_API_KEY        -> 直连 OpenAI
- 否则有 OPENROUTER_API_KEY -> 走 OpenRouter,并把模型名映射到 OpenRouter 命名:
      gpt-*    -> openai/gpt-*
      claude-* -> anthropic/claude-opus-4.8
      含 "/"   -> 原样透传
      其它     -> openai/gpt-5.6-luna

默认模型 gpt-5.6-luna。
"""

import os

from browser_use import ChatOpenAI, ChatGoogle

DEFAULT_MODEL = "gpt-5.6-luna"
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"


def to_openrouter_model(model: str) -> str:
    if not model:
        return "openai/gpt-5.6-luna"
    if "/" in model:
        return model
    if model.startswith("gpt-"):
        return "openai/" + model
    if model.startswith("claude-"):
        return "anthropic/claude-opus-4.8"
    return "openai/gpt-5.6-luna"


def make_llm(model: str = None):
    """按环境变量构造 LLM 客户端(OpenAI 直连优先,缺 Key 时 OpenRouter 兜底)。"""
    model = model or DEFAULT_MODEL
    if model.startswith("gemini"):
        return ChatGoogle(model=model)
    if os.getenv("OPENAI_API_KEY"):
        return ChatOpenAI(model=model)
    if os.getenv("OPENROUTER_API_KEY"):
        return ChatOpenAI(
            model=to_openrouter_model(model),
            api_key=os.getenv("OPENROUTER_API_KEY"),
            base_url=OPENROUTER_BASE_URL,
        )
    raise RuntimeError(
        "未检测到 OPENAI_API_KEY 或 OPENROUTER_API_KEY,请在 .env 中配置其一"
        "(OpenRouter 可作为统一兜底)。"
    )

quickstart.py

"""
Quick start script for the Learning Agent.

This script provides a simple example of how to use the learning agent
for common tasks.
"""

import asyncio
from dotenv import load_dotenv
from browser_use import ChatOpenAI, ChatGoogle
from learning_agent import LearningAgent
from llm_factory import make_llm

# Load environment variables
load_dotenv()


async def example_search():
    """Example: Search on Google."""
    print("\n🔍 Example 1: Google Search")
    print("-" * 40)

    agent = LearningAgent(
        task="Go to Google and search for 'browser automation with AI'",
        llm=make_llm(),
        knowledge_base_path="./my_knowledge",
        headless=False  # Show browser
    )

    result = await agent.run(max_steps=10)
    print(f"✅ Completed in {result['execution_time']:.2f}s")
    print(f"   LLM calls: {result.get('llm_calls', 0)}")
    print(f"   Workflow reused: {result.get('replay_used', False)}")


async def example_weather():
    """Example: Check weather."""
    print("\n☀️ Example 2: Weather Check")
    print("-" * 40)

    agent = LearningAgent(
        task="Check the weather forecast for Tokyo",
        llm=ChatGoogle(model="gemini-3.5-flash"),  # You can use different LLMs
        knowledge_base_path="./my_knowledge",
        headless=False
    )

    result = await agent.run(max_steps=15)
    print(f"✅ Completed in {result['execution_time']:.2f}s")

    # Run again with different city - should be faster!
    print("\n   Running again for New York...")
    agent2 = LearningAgent(
        task="Check the weather forecast for New York",
        llm=ChatGoogle(model="gemini-3.5-flash"),
        knowledge_base_path="./my_knowledge",
        headless=False
    )

    result2 = await agent2.run(max_steps=15)
    print(f"✅ Completed in {result2['execution_time']:.2f}s")

    if result2.get('replay_used'):
        speedup = result['execution_time'] / result2['execution_time']
        print(f"   🚀 {speedup:.1f}x faster with learned workflow!")


async def example_custom_task():
    """Example: Custom task from user input."""
    print("\n💡 Example 3: Custom Task")
    print("-" * 40)

    task = input("Enter your task: ")

    if not task:
        task = "Go to Wikipedia and search for 'artificial intelligence'"

    print(f"\nTask: {task}")

    agent = LearningAgent(
        task=task,
        llm=make_llm(),
        knowledge_base_path="./my_knowledge",
        headless=False
    )

    result = await agent.run(max_steps=20)

    print(f"\n✅ Task completed!")
    print(f"   Success: {result['success']}")
    print(f"   Time: {result['execution_time']:.2f}s")
    print(f"   Workflow reused: {result.get('replay_used', False)}")

    if not result.get('replay_used'):
        print("\n💡 Tip: Try the same task again - it will be much faster!")


def show_knowledge_stats():
    """Show knowledge base statistics."""
    from learning_agent import KnowledgeBase

    print("\n📊 Knowledge Base Statistics")
    print("-" * 40)

    kb = KnowledgeBase("./my_knowledge")
    stats = kb.get_statistics()

    if stats['total_workflows'] == 0:
        print("No workflows learned yet. Run some tasks first!")
    else:
        for key, value in stats.items():
            formatted_key = key.replace('_', ' ').title()
            print(f"   {formatted_key}: {value}")

        print("\n   Learned workflows:")
        for workflow in kb.workflows.values():
            print(f"      • {workflow.intent}")
            if workflow.success_count > 0:
                print(f"        (used {workflow.success_count} times)")


async def main():
    """Main menu."""
    print("=" * 60)
    print("LEARNING AGENT - QUICK START")
    print("=" * 60)

    while True:
        print("\nOptions:")
        print("1. Google Search Example")
        print("2. Weather Check Example")
        print("3. Custom Task")
        print("4. Show Knowledge Base Stats")
        print("5. Exit")

        choice = input("\nSelect option (1-5): ")

        if choice == "1":
            await example_search()
        elif choice == "2":
            await example_weather()
        elif choice == "3":
            await example_custom_task()
        elif choice == "4":
            show_knowledge_stats()
        elif choice == "5":
            print("\nGoodbye! 👋")
            break
        else:
            print("Invalid option, please try again.")


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n\nInterrupted by user. Goodbye! 👋")

test_validation.py

"""
Comprehensive validation test for the Learning Agent system.

This script validates all components of the learning agent:
1. Workflow capture and storage
2. Intent matching
3. Workflow replay
4. Performance improvements
"""

import asyncio
import json
import logging
from pathlib import Path
import time
from typing import Dict, Any

from dotenv import load_dotenv
from browser_use import ChatOpenAI
from llm_factory import make_llm
from learning_agent import LearningAgent, KnowledgeBase, Workflow, WorkflowStep
from learning_agent.workflow import ActionType


# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

load_dotenv()


class ValidationTest:
    """Comprehensive validation test suite."""

    def __init__(self):
        self.test_results = {
            "workflow_capture": False,
            "knowledge_base": False,
            "intent_matching": False,
            "workflow_replay": False,
            "performance_improvement": False
        }
        self.kb_path = "./test_validation_kb"

    async def run_all_tests(self):
        """Run all validation tests."""
        print("=" * 80)
        print("LEARNING AGENT VALIDATION TEST SUITE")
        print("=" * 80)

        # Clean up any existing test data
        self.cleanup_test_data()

        # Run tests
        await self.test_workflow_capture()
        await self.test_knowledge_base()
        await self.test_intent_matching()
        await self.test_workflow_replay()
        await self.test_performance_improvement()

        # Show results
        self.show_results()

        # Clean up
        self.cleanup_test_data()

    def cleanup_test_data(self):
        """Clean up test knowledge base."""
        import shutil
        if Path(self.kb_path).exists():
            shutil.rmtree(self.kb_path)

    async def test_workflow_capture(self):
        """Test 1: Workflow capture functionality."""
        print("\n" + "TEST 1: WORKFLOW CAPTURE ".ljust(70, "-"))

        try:
            # Create a simple workflow manually
            workflow = Workflow(
                workflow_id="test_workflow_1",
                intent="Navigate to Google and search for weather",
                initial_url="https://www.google.com"
            )

            # Add steps
            workflow.add_step(WorkflowStep(
                action_type=ActionType.NAVIGATE,
                parameters={"url": "https://www.google.com"}
            ))

            workflow.add_step(WorkflowStep(
                action_type=ActionType.INPUT_TEXT,
                xpath="//input[@name='q']",
                parameters={"text": "weather", "clear_existing": True}
            ))

            workflow.add_step(WorkflowStep(
                action_type=ActionType.CLICK,
                xpath="//input[@value='Google Search']",
                parameters={}
            ))

            # Test serialization
            json_str = workflow.to_json()
            restored = Workflow.from_json(json_str)

            # Verify
            assert restored.workflow_id == workflow.workflow_id
            assert len(restored.steps) == 3
            assert restored.steps[0].action_type == ActionType.NAVIGATE

            print("✅ Workflow capture and serialization: PASSED")
            self.test_results["workflow_capture"] = True

        except Exception as e:
            print(f"❌ Workflow capture test failed: {e}")

    async def test_knowledge_base(self):
        """Test 2: Knowledge base storage and retrieval."""
        print("\n" + "TEST 2: KNOWLEDGE BASE ".ljust(70, "-"))

        try:
            # Create knowledge base
            kb = KnowledgeBase(self.kb_path)

            # Create and save workflows
            workflow1 = Workflow(
                workflow_id="test_email",
                intent="send email to recipient",
                description="Email sending workflow"
            )

            workflow2 = Workflow(
                workflow_id="test_weather",
                intent="check weather in city",
                description="Weather checking workflow"
            )

            kb.save_workflow(workflow1)
            kb.save_workflow(workflow2)

            # Test loading
            kb2 = KnowledgeBase(self.kb_path)

            # Verify
            assert len(kb2.workflows) == 2
            assert "test_email" in kb2.workflows
            assert "test_weather" in kb2.workflows

            print(f"✅ Knowledge base operations: PASSED")
            print(f"   - Saved workflows: {len(kb2.workflows)}")
            print(f"   - Unique intents: {len(kb2.intent_index)}")

            self.test_results["knowledge_base"] = True

        except Exception as e:
            print(f"❌ Knowledge base test failed: {e}")

    async def test_intent_matching(self):
        """Test 3: Intent matching algorithm."""
        print("\n" + "TEST 3: INTENT MATCHING ".ljust(70, "-"))

        try:
            kb = KnowledgeBase(self.kb_path)

            # Create workflows with different intents
            workflows = [
                Workflow(workflow_id="1", intent="send email to recipient"),
                Workflow(workflow_id="2", intent="check weather in Beijing"),
                Workflow(workflow_id="3", intent="search for product on Amazon"),
                Workflow(workflow_id="4", intent="login to Gmail account"),
            ]

            for w in workflows:
                kb.save_workflow(w)

            # Test matching
            test_cases = [
                ("Send an email to john@example.com", "1", 0.5),
                ("What's the weather in Shanghai?", "2", 0.4),
                ("I want to buy a laptop on Amazon", "3", 0.4),
                ("Sign in to my Gmail", "4", 0.4),
            ]

            passed = 0
            for task, expected_id, min_confidence in test_cases:
                match = kb.find_workflow_for_task(task)
                if match and match.workflow.workflow_id == expected_id and match.confidence >= min_confidence:
                    passed += 1
                    print(f"   ✓ '{task[:30]}...' → Workflow {expected_id} ({match.confidence:.2f})")
                else:
                    print(f"   ✗ '{task[:30]}...' → Failed to match correctly")

            if passed == len(test_cases):
                print(f"✅ Intent matching: PASSED ({passed}/{len(test_cases)} tests)")
                self.test_results["intent_matching"] = True
            else:
                print(f"⚠️ Intent matching: PARTIAL ({passed}/{len(test_cases)} tests)")

        except Exception as e:
            print(f"❌ Intent matching test failed: {e}")

    async def test_workflow_replay(self):
        """Test 4: Workflow replay mechanism."""
        print("\n" + "TEST 4: WORKFLOW REPLAY ".ljust(70, "-"))

        try:
            from learning_agent.replay import WorkflowReplayer

            # Create a simple workflow
            workflow = Workflow(
                workflow_id="test_replay",
                intent="search on Google",
                initial_url="https://www.google.com"
            )

            workflow.add_step(WorkflowStep(
                action_type=ActionType.NAVIGATE,
                parameters={"url": "https://www.google.com"}
            ))

            # Initialize replayer
            replayer = WorkflowReplayer(headless=True)
            await replayer.setup()

            # Test replay
            result = await replayer.replay_workflow(
                workflow,
                parameters={},
                initial_url="https://www.google.com"
            )

            await replayer.cleanup()

            # Verify
            if result["steps_completed"] > 0:
                print(f"✅ Workflow replay: PASSED")
                print(f"   - Steps completed: {result['steps_completed']}/{result['total_steps']}")
                print(f"   - Execution time: {result['execution_time']:.2f}s")
                self.test_results["workflow_replay"] = True
            else:
                print(f"⚠️ Workflow replay: No steps completed")

        except Exception as e:
            print(f"❌ Workflow replay test failed: {e}")

    async def test_performance_improvement(self):
        """Test 5: Performance improvement validation."""
        print("\n" + "TEST 5: PERFORMANCE IMPROVEMENT ".ljust(70, "-"))

        try:
            # Simulate learning phase metrics
            learning_time = 30.5
            learning_llm_calls = 12

            # Simulate replay phase metrics
            replay_time = 7.8
            replay_llm_calls = 0

            # Calculate improvements
            speedup = learning_time / replay_time
            calls_saved = learning_llm_calls - replay_llm_calls
            time_saved = learning_time - replay_time

            print(f"   📊 Simulated Performance Metrics:")
            print(f"      Learning phase: {learning_time:.1f}s, {learning_llm_calls} LLM calls")
            print(f"      Replay phase: {replay_time:.1f}s, {replay_llm_calls} LLM calls")
            print(f"   🚀 Improvements:")
            print(f"      Speed: {speedup:.1f}x faster")
            print(f"      LLM calls saved: {calls_saved}")
            print(f"      Time saved: {time_saved:.1f}s")

            if speedup > 3 and calls_saved > 10:
                print(f"✅ Performance improvement: VALIDATED")
                self.test_results["performance_improvement"] = True
            else:
                print(f"⚠️ Performance improvement: Below expectations")

        except Exception as e:
            print(f"❌ Performance test failed: {e}")

    def show_results(self):
        """Show test results summary."""
        print("\n" + "=" * 80)
        print("TEST RESULTS SUMMARY")
        print("=" * 80)

        total_tests = len(self.test_results)
        passed_tests = sum(1 for v in self.test_results.values() if v)

        for test_name, passed in self.test_results.items():
            status = "✅ PASSED" if passed else "❌ FAILED"
            formatted_name = test_name.replace("_", " ").title()
            print(f"   {formatted_name:.<40} {status}")

        print("-" * 80)
        print(f"   Overall: {passed_tests}/{total_tests} tests passed")

        if passed_tests == total_tests:
            print("\n🎉 ALL VALIDATION TESTS PASSED!")
            print("The Learning Agent system is working correctly.")
        elif passed_tests >= total_tests * 0.8:
            print("\n✅ MOST TESTS PASSED")
            print("The system is mostly functional with minor issues.")
        else:
            print("\n⚠️ VALIDATION INCOMPLETE")
            print("Please review failed tests and fix issues.")


async def quick_integration_test():
    """Quick integration test with real browser-use."""
    print("\n" + "QUICK INTEGRATION TEST ".ljust(70, "="))

    try:
        # Simple task
        task = "Go to https://www.example.com and find the heading"

        agent = LearningAgent(
            task=task,
            llm=make_llm(),
            knowledge_base_path="./quick_test_kb",
            headless=True
        )

        print(f"Task: {task}")
        print("Running learning agent...")

        result = await agent.run(max_steps=5)

        print(f"\nResult:")
        print(f"  - Success: {result['success']}")
        print(f"  - Time: {result['execution_time']:.2f}s")
        print(f"  - LLM calls: {result.get('llm_calls', 0)}")

        # Clean up
        import shutil
        if Path("./quick_test_kb").exists():
            shutil.rmtree("./quick_test_kb")

        return result['success']

    except Exception as e:
        print(f"Integration test error: {e}")
        return False


async def main():
    """Main entry point."""
    import sys

    if "--quick" in sys.argv:
        # Run quick integration test
        success = await quick_integration_test()
        sys.exit(0 if success else 1)
    else:
        # Run full validation suite
        validator = ValidationTest()
        await validator.run_all_tests()


if __name__ == "__main__":
    asyncio.run(main())