ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
Experience Learning Agent for GAIA
|
||||
This module extends the AWorld Agent with learning from experience capabilities.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
|
||||
from AWorld.aworld.agents.llm_agent import Agent
|
||||
from AWorld.aworld.config.conf import AgentConfig
|
||||
from AWorld.aworld.core.task import Task, TaskResponse
|
||||
from AWorld.aworld.runner import Runners
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExperienceAgent(Agent):
|
||||
"""
|
||||
Extended Agent that can learn from successful trajectories and apply learned experiences.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conf: AgentConfig,
|
||||
name: str = "experience_agent",
|
||||
system_prompt: str = "",
|
||||
learning_mode: bool = False,
|
||||
apply_experience: bool = False,
|
||||
experience_db_path: str = "./experience_db.json",
|
||||
knowledge_base: Optional['KnowledgeBase'] = None,
|
||||
summarizer: Optional['TrajectorySummarizer'] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize the Experience Agent.
|
||||
|
||||
Args:
|
||||
conf: Agent configuration
|
||||
name: Agent name
|
||||
system_prompt: Base system prompt
|
||||
learning_mode: Whether to capture and learn from successful trajectories
|
||||
apply_experience: Whether to apply learned experiences to new tasks
|
||||
experience_db_path: Path to store learned experiences
|
||||
knowledge_base: Knowledge base for retrieval
|
||||
summarizer: Trajectory summarizer instance
|
||||
**kwargs: Additional arguments for base Agent
|
||||
"""
|
||||
super().__init__(conf=conf, name=name, system_prompt=system_prompt, **kwargs)
|
||||
|
||||
self.learning_mode = learning_mode
|
||||
self.apply_experience = apply_experience
|
||||
self.experience_db_path = experience_db_path
|
||||
self.knowledge_base = knowledge_base
|
||||
self.summarizer = summarizer
|
||||
self.base_system_prompt = system_prompt
|
||||
|
||||
# Load existing experiences if available
|
||||
self.experiences = self._load_experiences()
|
||||
|
||||
# Track current task trajectory
|
||||
self.current_trajectory = []
|
||||
|
||||
def _load_experiences(self) -> Dict[str, Any]:
|
||||
"""Load existing experiences from file."""
|
||||
if os.path.exists(self.experience_db_path):
|
||||
try:
|
||||
with open(self.experience_db_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load experiences: {e}")
|
||||
return {}
|
||||
|
||||
def _save_experiences(self):
|
||||
"""Save experiences to file."""
|
||||
try:
|
||||
with open(self.experience_db_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.experiences, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save experiences: {e}")
|
||||
|
||||
def _get_task_hash(self, question: str) -> str:
|
||||
"""Generate a hash for a question to use as key."""
|
||||
return hashlib.md5(question.encode()).hexdigest()
|
||||
|
||||
async def execute_task(self, task: Task) -> TaskResponse:
|
||||
"""
|
||||
Execute a task with experience learning/application.
|
||||
|
||||
Args:
|
||||
task: The task to execute
|
||||
|
||||
Returns:
|
||||
TaskResponse with result
|
||||
"""
|
||||
question = task.input
|
||||
original_prompt = self.system_prompt
|
||||
|
||||
# Apply experience if enabled
|
||||
if self.apply_experience:
|
||||
relevant_experiences = self._get_relevant_experiences(question)
|
||||
if relevant_experiences:
|
||||
experience_text = self._format_experiences(relevant_experiences)
|
||||
self.system_prompt = f"{self.base_system_prompt}\n\n# Relevant Past Experiences:\n{experience_text}"
|
||||
logger.info(f"Applied {len(relevant_experiences)} relevant experiences to prompt")
|
||||
|
||||
# Reset the manual-capture buffer for the new task. The real trajectory
|
||||
# is captured by AWorld and read back from the TaskResponse below.
|
||||
self.current_trajectory = []
|
||||
|
||||
try:
|
||||
# Execute the task
|
||||
result = await Runners.run_task(task)
|
||||
task_response = result.get(task.id)
|
||||
|
||||
# Recover the actual execution trajectory produced by AWorld's replay
|
||||
# buffer (TaskResponse.trajectory). Falls back to manually captured
|
||||
# actions when the framework did not record a trajectory.
|
||||
trajectory = self._extract_trajectory(task_response)
|
||||
|
||||
# Process result for learning if enabled
|
||||
if self.learning_mode and task_response and self._is_successful(task_response, task):
|
||||
await self._learn_from_success(question, task_response, trajectory)
|
||||
|
||||
return task_response
|
||||
finally:
|
||||
# Restore in a finally: if run_task raises and the caller moves on
|
||||
# to the next task, the injected experiences of THIS task must not
|
||||
# leak into later tasks that have no matching experiences of
|
||||
# their own.
|
||||
self.system_prompt = original_prompt
|
||||
|
||||
def _extract_trajectory(self, task_response: Optional[TaskResponse]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Normalize the trajectory recorded by AWorld into the step format the
|
||||
TrajectorySummarizer expects: ``{'action': {'tool_name', 'action_name',
|
||||
'params'}}``.
|
||||
|
||||
AWorld stores each step as a serialized replay-buffer ``DataRow`` whose
|
||||
``exp_data.actions`` field holds the ``ActionModel`` objects taken at
|
||||
that step. When no framework trajectory is available (e.g. the runner
|
||||
did not populate it), we fall back to any actions that were captured
|
||||
manually via :meth:`capture_action`.
|
||||
|
||||
Args:
|
||||
task_response: The response returned by ``Runners.run_task``.
|
||||
|
||||
Returns:
|
||||
A list of normalized trajectory steps.
|
||||
"""
|
||||
raw = getattr(task_response, "trajectory", None) if task_response else None
|
||||
if not raw:
|
||||
return list(self.current_trajectory)
|
||||
|
||||
steps: List[Dict[str, Any]] = []
|
||||
for row in raw:
|
||||
actions = []
|
||||
if isinstance(row, dict):
|
||||
exp_data = row.get("exp_data") or {}
|
||||
if isinstance(exp_data, dict):
|
||||
actions = exp_data.get("actions") or []
|
||||
for action in actions:
|
||||
if not isinstance(action, dict):
|
||||
continue
|
||||
steps.append({
|
||||
"action": {
|
||||
"tool_name": action.get("tool_name") or "unknown",
|
||||
"action_name": action.get("action_name") or "",
|
||||
"params": action.get("params") or {},
|
||||
}
|
||||
})
|
||||
|
||||
# If normalization yielded nothing usable, keep the raw rows so the
|
||||
# summarizer at least reflects the correct step count.
|
||||
return steps or list(self.current_trajectory) or list(raw)
|
||||
|
||||
def _get_relevant_experiences(self, question: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve relevant experiences for a given question.
|
||||
|
||||
Args:
|
||||
question: The current question
|
||||
|
||||
Returns:
|
||||
List of relevant experiences
|
||||
"""
|
||||
relevant = []
|
||||
|
||||
# First check if knowledge base has preloaded experiences
|
||||
if self.knowledge_base:
|
||||
kb_experiences = self.knowledge_base.search(question, top_k=3)
|
||||
relevant.extend(kb_experiences)
|
||||
|
||||
# Then check learned experiences
|
||||
if self.experiences:
|
||||
# Simple similarity check - can be enhanced with embeddings
|
||||
for exp_id, exp_data in self.experiences.items():
|
||||
if self._is_similar(question, exp_data.get('question', '')):
|
||||
relevant.append(exp_data)
|
||||
if len(relevant) >= 5: # Limit to top 5 experiences
|
||||
break
|
||||
|
||||
return relevant
|
||||
|
||||
def _is_similar(self, q1: str, q2: str) -> bool:
|
||||
"""
|
||||
Simple similarity check between questions.
|
||||
Can be enhanced with semantic similarity using embeddings.
|
||||
|
||||
Args:
|
||||
q1: First question
|
||||
q2: Second question
|
||||
|
||||
Returns:
|
||||
True if questions are similar
|
||||
"""
|
||||
# Simple keyword overlap for now
|
||||
if q1 is None or q2 is None:
|
||||
return False
|
||||
q1_words = set(q1.lower().split())
|
||||
q2_words = set(q2.lower().split())
|
||||
|
||||
overlap = len(q1_words & q2_words)
|
||||
total = len(q1_words | q2_words)
|
||||
|
||||
if total == 0:
|
||||
return False
|
||||
|
||||
similarity = overlap / total
|
||||
return similarity > 0.3 # Threshold for similarity
|
||||
|
||||
def _format_experiences(self, experiences: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
Format experiences for inclusion in system prompt.
|
||||
|
||||
Args:
|
||||
experiences: List of experience dictionaries
|
||||
|
||||
Returns:
|
||||
Formatted experience text
|
||||
"""
|
||||
formatted = []
|
||||
for i, exp in enumerate(experiences, 1):
|
||||
exp_text = f"## Experience {i}:\n"
|
||||
|
||||
if 'question' in exp:
|
||||
exp_text += f"- Similar Question: {exp['question']}\n"
|
||||
|
||||
if 'summary' in exp:
|
||||
exp_text += f"- Key Insights: {exp['summary']}\n"
|
||||
|
||||
if 'approach' in exp:
|
||||
exp_text += f"- Approach: {exp['approach']}\n"
|
||||
|
||||
if 'tools_used' in exp:
|
||||
exp_text += f"- Tools Used: {', '.join(exp['tools_used'])}\n"
|
||||
|
||||
formatted.append(exp_text)
|
||||
|
||||
return "\n".join(formatted)
|
||||
|
||||
def _is_successful(self, response: TaskResponse, task: Task) -> bool:
|
||||
"""
|
||||
Determine if a task execution was successful.
|
||||
|
||||
Args:
|
||||
response: The task response
|
||||
task: The original task
|
||||
|
||||
Returns:
|
||||
True if the task was successful
|
||||
"""
|
||||
# Check if answer exists and is not empty
|
||||
if not response or not response.answer:
|
||||
return False
|
||||
|
||||
# Honor AWorld's own success signal when the framework provides one.
|
||||
success_flag = getattr(response, "success", None)
|
||||
status = getattr(response, "status", None)
|
||||
if success_flag is False:
|
||||
return False
|
||||
if status and str(status).lower() in {"failed", "cancelled"}:
|
||||
return False
|
||||
|
||||
# Additional success criteria can be added here
|
||||
# For GAIA, we might check against known answers if available
|
||||
|
||||
return True
|
||||
|
||||
async def _learn_from_success(self, question: str, response: TaskResponse, trajectory: List[Dict[str, Any]]):
|
||||
"""
|
||||
Learn from a successful task execution.
|
||||
|
||||
Args:
|
||||
question: The original question
|
||||
response: The successful response
|
||||
trajectory: The execution trajectory
|
||||
"""
|
||||
if not self.summarizer:
|
||||
logger.warning("No summarizer configured, skipping learning")
|
||||
return
|
||||
|
||||
try:
|
||||
# Summarize the trajectory
|
||||
summary = await self.summarizer.summarize(question, response, trajectory)
|
||||
|
||||
# Store the experience
|
||||
exp_id = self._get_task_hash(question)
|
||||
self.experiences[exp_id] = {
|
||||
'question': question,
|
||||
'answer': response.answer,
|
||||
'summary': summary.get('summary', ''),
|
||||
'approach': summary.get('approach', ''),
|
||||
'tools_used': summary.get('tools_used', []),
|
||||
'key_insights': summary.get('key_insights', []),
|
||||
'general_strategy': summary.get('general_strategy', ''),
|
||||
'num_steps': len(trajectory),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'success': True
|
||||
}
|
||||
|
||||
# Save to disk
|
||||
self._save_experiences()
|
||||
|
||||
logger.info(f"Learned from successful execution: {exp_id[:8]}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to learn from success: {e}")
|
||||
|
||||
def capture_action(self, action: Dict[str, Any]):
|
||||
"""
|
||||
Capture an action in the current trajectory.
|
||||
|
||||
Args:
|
||||
action: The action to capture
|
||||
"""
|
||||
if self.learning_mode:
|
||||
self.current_trajectory.append({
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'action': action
|
||||
})
|
||||
Reference in New Issue
Block a user