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,13 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
|
||||
"""
|
||||
aworld.config public exports
|
||||
|
||||
To avoid circular imports, do not import agent_loader here.
|
||||
If you need YAML helpers, import them explicitly:
|
||||
from aworld.config.agent_loader import load_agents_from_yaml, load_swarm_from_yaml
|
||||
"""
|
||||
|
||||
from aworld.config.conf import *
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
"""
|
||||
Agent/Squad configuration loader from YAML.
|
||||
|
||||
Goals:
|
||||
- Allow users to define agents and (optionally) a swarm topology in a single YAML file
|
||||
- One function to load and construct Agents/Swarm
|
||||
- Use existing config models (AgentConfig, ModelConfig, etc.) and utilities
|
||||
- Support ${ENV_VAR} substitution in YAML values
|
||||
|
||||
YAML schema (minimal):
|
||||
|
||||
agents:
|
||||
researcher:
|
||||
system_prompt: "You specialize at researching."
|
||||
llm_config:
|
||||
llm_provider: openai
|
||||
llm_model_name: gpt-4o
|
||||
llm_api_key: ${OPENAI_API_KEY}
|
||||
llm_temperature: 0.1
|
||||
summarizer:
|
||||
system_prompt: "You specialize at summarizing."
|
||||
llm_config:
|
||||
llm_provider: openai
|
||||
llm_model_name: google/gemini-2.5-pro
|
||||
llm_api_key: ${OPENROUTER_API_KEY}
|
||||
llm_base_url: https://openrouter.ai/api/v1
|
||||
llm_temperature: 0.1
|
||||
|
||||
# Optional Swarm definition (choose one of the patterns below)
|
||||
swarm:
|
||||
type: workflow # or handoff, team
|
||||
order: [researcher, summarizer] # for workflow
|
||||
# edges: [[researcher, summarizer]] # for handoff
|
||||
# root: researcher # for team
|
||||
# members: [summarizer]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, Tuple, List, Any, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from aworld.agents.llm_agent import Agent
|
||||
from aworld.config.conf import AgentConfig
|
||||
from aworld.core.agent.swarm import Swarm, GraphBuildType
|
||||
from aworld.logs.util import logger
|
||||
from aworld.utils.common import replace_env_variables
|
||||
|
||||
|
||||
def _replace_internal_vars(data: Any, vars_map: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Replace placeholders of the form ${vars.KEY} using values from vars_map.
|
||||
- If the ENTIRE string is exactly "${vars.KEY}", return the raw value (preserve type, e.g., float/bool/int)
|
||||
- If used inside a longer string, perform string substitution
|
||||
Works recursively for dicts/lists/strings.
|
||||
"""
|
||||
if not vars_map:
|
||||
return data
|
||||
|
||||
pattern = re.compile(r"\$\{vars\.([A-Za-z0-9_]+)\}")
|
||||
full_pattern = re.compile(r"^\$\{vars\.([A-Za-z0-9_]+)\}$")
|
||||
|
||||
def _recurse(obj: Any) -> Any:
|
||||
if isinstance(obj, dict):
|
||||
return {k: _recurse(v) for k, v in obj.items()}
|
||||
if isinstance(obj, list):
|
||||
return [_recurse(v) for v in obj]
|
||||
if isinstance(obj, str):
|
||||
# Full match: preserve original type from vars_map
|
||||
m = full_pattern.match(obj)
|
||||
if m:
|
||||
key = m.group(1)
|
||||
if key in vars_map:
|
||||
return vars_map[key]
|
||||
logger.warning(f"YAML vars: '${{vars.{key}}}' not found in top-level 'vars'")
|
||||
return obj
|
||||
|
||||
# Partial substitution within a larger string -> stringify replacement
|
||||
def _sub(match: re.Match) -> str:
|
||||
key = match.group(1)
|
||||
if key in vars_map:
|
||||
return str(vars_map[key])
|
||||
logger.warning(f"YAML vars: '${{vars.{key}}}' not found in top-level 'vars'")
|
||||
return match.group(0)
|
||||
return pattern.sub(_sub, obj)
|
||||
return obj
|
||||
|
||||
return _recurse(data)
|
||||
|
||||
|
||||
def _load_yaml(path: str) -> Dict[str, Any]:
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"Config YAML not found: {path}")
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
# 1) Replace ${ENV} placeholders from OS environment
|
||||
data = replace_env_variables(data)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Top-level YAML must be a mapping (dict)")
|
||||
# 2) Replace ${vars.KEY} placeholders from YAML top-level 'vars'
|
||||
data = _replace_internal_vars(data, data.get("vars", {}))
|
||||
return data
|
||||
|
||||
|
||||
def load_agents_from_yaml(path: str) -> Dict[str, Agent]:
|
||||
"""
|
||||
Load agents defined in YAML and construct Agent instances.
|
||||
|
||||
Returns a dict mapping agent names to Agent instances.
|
||||
Does not build a Swarm; use load_swarm_from_yaml for that.
|
||||
"""
|
||||
data = _load_yaml(path)
|
||||
agents_conf = data.get("agents", {})
|
||||
if not isinstance(agents_conf, dict):
|
||||
raise ValueError("`agents` must be a mapping of name -> config")
|
||||
|
||||
agents: Dict[str, Agent] = {}
|
||||
for name, conf_dict in agents_conf.items():
|
||||
if not isinstance(conf_dict, dict):
|
||||
raise ValueError(f"Agent `{name}` config must be a mapping")
|
||||
try:
|
||||
# Pydantic will parse nested llm_config, memory_config, etc.
|
||||
agent_conf = AgentConfig(**conf_dict)
|
||||
agent = Agent(name=name, conf=agent_conf)
|
||||
agents[name] = agent
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load agent `{name}` from YAML: {e}")
|
||||
raise
|
||||
return agents
|
||||
|
||||
|
||||
def load_swarm_from_yaml(path: str) -> Tuple[Swarm, Dict[str, Agent]]:
|
||||
"""
|
||||
Load agents and an optional swarm topology from YAML.
|
||||
|
||||
Returns (swarm, agents_dict).
|
||||
If `swarm` section is missing, builds a default workflow in the order of YAML `agents` keys.
|
||||
"""
|
||||
data = _load_yaml(path)
|
||||
agents = load_agents_from_yaml(path)
|
||||
|
||||
swarm_conf: Optional[Dict[str, Any]] = data.get("swarm")
|
||||
if not swarm_conf:
|
||||
# Default: simple workflow in the order of agents declaration
|
||||
ordered = [agents[name] for name in data.get("agents", {}).keys()]
|
||||
if not ordered:
|
||||
raise ValueError("No agents defined to build a swarm")
|
||||
return Swarm(*ordered), agents
|
||||
|
||||
stype = (swarm_conf.get("type") or GraphBuildType.WORKFLOW.value).lower()
|
||||
if stype not in {GraphBuildType.WORKFLOW.value, GraphBuildType.HANDOFF.value, GraphBuildType.TEAM.value}:
|
||||
raise ValueError(f"Unsupported swarm.type: {stype}")
|
||||
|
||||
if stype == GraphBuildType.WORKFLOW.value:
|
||||
order: List[str] = swarm_conf.get("order") or list(data.get("agents", {}).keys())
|
||||
if not isinstance(order, list) or not order:
|
||||
raise ValueError("For workflow swarm, `order` must be a non-empty list of agent names")
|
||||
ordered_agents = [agents[name] for name in order]
|
||||
return Swarm(*ordered_agents), agents
|
||||
|
||||
if stype == GraphBuildType.HANDOFF.value:
|
||||
edges: List[List[str]] = swarm_conf.get("edges") or []
|
||||
if not edges:
|
||||
raise ValueError("For handoff swarm, `edges` must be provided as [[left, right], ...]")
|
||||
pairs = []
|
||||
for a, b in edges:
|
||||
pairs.append((agents[a], agents[b]))
|
||||
return Swarm(*pairs, build_type=GraphBuildType.HANDOFF), agents
|
||||
|
||||
# TEAM
|
||||
root: str = swarm_conf.get("root")
|
||||
members: List[str] = swarm_conf.get("members") or []
|
||||
if not root:
|
||||
# If root not specified, default to the first defined agent
|
||||
root = next(iter(data.get("agents", {}).keys()), None)
|
||||
if not root:
|
||||
raise ValueError("For team swarm, `root` or at least one agent must be defined")
|
||||
ordered = [agents[root]] + [agents[m] for m in members if m != root]
|
||||
return Swarm(*ordered, build_type=GraphBuildType.TEAM), agents
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,271 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import os
|
||||
import traceback
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from aworld.logs.util import logger
|
||||
|
||||
|
||||
def load_config(file_name: str, dir_name: str = None) -> Dict[str, Any]:
|
||||
"""Dynamically load config file form current path.
|
||||
|
||||
Args:
|
||||
file_name: Config file name.
|
||||
dir_name: Config file directory.
|
||||
|
||||
Returns:
|
||||
Config dict.
|
||||
"""
|
||||
|
||||
if dir_name:
|
||||
file_path = os.path.join(dir_name, file_name)
|
||||
else:
|
||||
# load conf form current path
|
||||
current_dir = Path(__file__).parent.absolute()
|
||||
file_path = os.path.join(current_dir, file_name)
|
||||
if not os.path.exists(file_path):
|
||||
logger.debug(f"{file_path} not exists, please check it.")
|
||||
|
||||
configs = dict()
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
yaml_data = yaml.safe_load(file)
|
||||
configs.update(yaml_data)
|
||||
except FileNotFoundError:
|
||||
logger.debug(f"Can not find the file: {file_path}")
|
||||
except Exception:
|
||||
logger.warning(f"{file_name} read fail.\n", traceback.format_exc())
|
||||
return configs
|
||||
|
||||
|
||||
def wipe_secret_info(config: Dict[str, Any], keys: List[str]) -> Dict[str, Any]:
|
||||
"""Return a deep copy of this config as a plain Dict as well ass wipe up secret info, used to log."""
|
||||
|
||||
def _wipe_secret(conf):
|
||||
def _wipe_secret_plain_value(v):
|
||||
if isinstance(v, List):
|
||||
return [_wipe_secret_plain_value(e) for e in v]
|
||||
elif isinstance(v, Dict):
|
||||
return _wipe_secret(v)
|
||||
else:
|
||||
return v
|
||||
|
||||
key_list = []
|
||||
for key in conf.keys():
|
||||
key_list.append(key)
|
||||
for key in key_list:
|
||||
if key.strip('"') in keys:
|
||||
conf[key] = '-^_^-'
|
||||
else:
|
||||
_wipe_secret_plain_value(conf[key])
|
||||
return conf
|
||||
|
||||
if not config:
|
||||
return config
|
||||
return _wipe_secret(config)
|
||||
|
||||
|
||||
class ClientType(Enum):
|
||||
SDK = "sdk"
|
||||
HTTP = "http"
|
||||
|
||||
|
||||
class ConfigDict(dict):
|
||||
"""Object mode operates dict, can read non-existent attributes through `get` method."""
|
||||
__setattr__ = dict.__setitem__
|
||||
__getattr__ = dict.__getitem__
|
||||
|
||||
def __init__(self, seq: dict = None, **kwargs):
|
||||
if seq is None:
|
||||
seq = OrderedDict()
|
||||
super(ConfigDict, self).__init__(seq, **kwargs)
|
||||
self.nested(self)
|
||||
|
||||
def nested(self, seq: dict):
|
||||
"""Nested recursive processing dict.
|
||||
|
||||
Args:
|
||||
seq: Python original format dict
|
||||
"""
|
||||
for k, v in seq.items():
|
||||
if isinstance(v, dict):
|
||||
seq[k] = ConfigDict(v)
|
||||
self.nested(v)
|
||||
|
||||
|
||||
class BaseConfig(BaseModel):
|
||||
def to_dict(self) -> ConfigDict:
|
||||
return ConfigDict(self.model_dump())
|
||||
|
||||
|
||||
class ModelConfig(BaseConfig):
|
||||
llm_provider: str = "openai"
|
||||
llm_model_name: str = None
|
||||
llm_temperature: float = 1.
|
||||
llm_base_url: str = None
|
||||
llm_api_key: str = None
|
||||
llm_client_type: ClientType = ClientType.SDK
|
||||
llm_sync_enabled: bool = True
|
||||
llm_async_enabled: bool = True
|
||||
max_retries: int = 3
|
||||
max_model_len: Optional[int] = None # Maximum model context length
|
||||
model_type: Optional[str] = 'qwen' # Model type determines tokenizer and maximum length
|
||||
params: Optional[Dict[str, Any]] = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
# init max_model_len
|
||||
if self.max_model_len is None:
|
||||
# qwen or other default model_type
|
||||
self.max_model_len = 128000 if self.model_type != 'claude' else 200000
|
||||
|
||||
|
||||
class LlmCompressionConfig(BaseConfig):
|
||||
enabled: bool = False
|
||||
compress_type: str = 'llm' # llm, llmlingua
|
||||
trigger_compress_token_length: int = 10000 # Trigger compression when exceeding this length
|
||||
compress_model: Optional[ModelConfig] = Field(default=None, description="Compression model configuration")
|
||||
|
||||
|
||||
class OptimizationConfig(BaseConfig):
|
||||
enabled: bool = False
|
||||
max_token_budget_ratio: float = 0.5 # Maximum context length ratio
|
||||
|
||||
|
||||
class ContextRuleConfig(BaseConfig):
|
||||
"""Context interference rule configuration"""
|
||||
|
||||
# ===== Performance optimization configuration =====
|
||||
optimization_config: OptimizationConfig = OptimizationConfig()
|
||||
|
||||
# ===== LLM conversation compression configuration =====
|
||||
llm_compression_config: LlmCompressionConfig = LlmCompressionConfig()
|
||||
|
||||
|
||||
class AgentMemoryConfig(BaseConfig):
|
||||
"""Configuration for procedural memory."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, validate_default=True, revalidate_instances='always', validate_assignment=True,
|
||||
arbitrary_types_allowed=True
|
||||
)
|
||||
# short-term config
|
||||
history_rounds: int = Field(default=100,
|
||||
description="rounds of message msg; when the number of messages is greater than the history_rounds, the memory will be trimmed")
|
||||
enable_summary: bool = Field(default=False,
|
||||
description="enable_summary use llm to create summary short-term memory")
|
||||
summary_model: Optional[str] = Field(default=None, description="short-term summary model")
|
||||
summary_rounds: Optional[int] = Field(default=5,
|
||||
description="rounds of message msg; when the number of messages is greater than the summary_rounds, the summary will be created")
|
||||
summary_context_length: Optional[int] = Field(default=40960,
|
||||
description=" when the content length is greater than the summary_context_length, the summary will be created")
|
||||
# summary_prompt: str = Field(default=SUMMARY_PROMPT, description="summary prompt")
|
||||
|
||||
# Long-term memory config
|
||||
enable_long_term: bool = Field(default=False, description="enable_long_term use to store long-term memory")
|
||||
long_term_model: Optional[str] = Field(default=None, description="long-term extract model")
|
||||
# LongTermConfig
|
||||
long_term_config: Optional[BaseModel] = Field(default=None, description="long_term_config")
|
||||
|
||||
|
||||
class AgentConfig(BaseConfig):
|
||||
llm_config: ModelConfig = ModelConfig()
|
||||
memory_config: AgentMemoryConfig = AgentMemoryConfig()
|
||||
context_rule: ContextRuleConfig = ContextRuleConfig()
|
||||
|
||||
# default reset init in first
|
||||
need_reset: bool = True
|
||||
# use vision model
|
||||
use_vision: bool = True
|
||||
max_steps: int = 10
|
||||
max_input_tokens: int = 128000
|
||||
max_actions_per_step: int = 10
|
||||
system_prompt: Optional[str] = None
|
||||
system_prompt_template: Optional[str] = None
|
||||
agent_prompt: Optional[str] = None
|
||||
working_dir: Optional[str] = None
|
||||
enable_recording: bool = False
|
||||
use_tools_in_prompt: bool = False
|
||||
exit_on_failure: bool = False
|
||||
ext: dict = {}
|
||||
human_tools: List[str] = []
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# Initialize llm_config with relevant kwargs
|
||||
llm_config_kwargs = {k: v for k, v in kwargs.items() if k in ModelConfig.model_fields}
|
||||
# Reassignment if it has llm config args
|
||||
if llm_config_kwargs or not self.llm_config:
|
||||
self.llm_config = ModelConfig(**llm_config_kwargs)
|
||||
|
||||
@property
|
||||
def llm_model_name(self) -> str:
|
||||
return self.llm_config.llm_model_name
|
||||
|
||||
@property
|
||||
def llm_provider(self) -> str:
|
||||
return self.llm_config.llm_provider
|
||||
|
||||
|
||||
class TaskConfig(BaseConfig):
|
||||
task_id: str = str(uuid.uuid4())
|
||||
task_name: str | None = None
|
||||
max_steps: int = 100
|
||||
stream: bool = False
|
||||
resp_carry_context: bool = True
|
||||
exit_on_failure: bool = False
|
||||
ext: dict = {}
|
||||
|
||||
|
||||
class ToolConfig(BaseConfig):
|
||||
name: str = None
|
||||
custom_executor: bool = False
|
||||
enable_recording: bool = False
|
||||
working_dir: str = ""
|
||||
max_retry: int = 3
|
||||
llm_config: ModelConfig = None
|
||||
reuse: bool = False
|
||||
use_async: bool = False
|
||||
exit_on_failure: bool = False
|
||||
ext: dict = {}
|
||||
|
||||
|
||||
class EngineName:
|
||||
# Use asyncio or MultiProcess run in local
|
||||
LOCAL = "local"
|
||||
# Stateless(task) run in ray. Ray actor will use a new name
|
||||
RAY = "ray"
|
||||
SPARK = "spark"
|
||||
|
||||
|
||||
class RunConfig(BaseConfig):
|
||||
job_name: str = "aworld_job"
|
||||
engine_name: str = EngineName.LOCAL
|
||||
worker_num: int = 1
|
||||
# engine whether to run in local
|
||||
in_local: bool = True
|
||||
# run in local whether to use the same process
|
||||
reuse_process: bool = True
|
||||
# Is the task sequence dependent
|
||||
sequence_dependent: bool = False
|
||||
# The custom implement of RuntimeEngine
|
||||
cls: Optional[str] = None
|
||||
event_bus: Optional[Dict[str, Any]] = None
|
||||
tracer: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class EvaluationConfig(BaseConfig):
|
||||
work_dir: Optional[str] = None
|
||||
run_times: int = 1
|
||||
Reference in New Issue
Block a user