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,27 @@
|
||||
# VERL Adapter (AWorld Train)
|
||||
|
||||
This module hosts the VERL integration for AWorld training workflows.
|
||||
|
||||
- aworld_agent_loop.py: Base class bridging VERL AgentLoop with AWorld agents.
|
||||
- common.py:
|
||||
- Utilities for converting trajectories/messages to VERL AgentLoopOutput.
|
||||
- Utilities for getting MCP server configuration.
|
||||
|
||||
## Usage
|
||||
Import adapter entrypoints from your example code:
|
||||
|
||||
```python
|
||||
from train.adapter.verl.aworld_agent_loop import AworldAgentLoop
|
||||
```
|
||||
Then implement your example-specific loop:
|
||||
```python
|
||||
class MyLoop(AworldAgentLoop):
|
||||
def build_agents(self):
|
||||
...
|
||||
```
|
||||
|
||||
## Adding New Features
|
||||
- Avoid putting example-specific code here; that belongs in train/examples/.
|
||||
|
||||
## Notes
|
||||
- Prefer small, composable utilities and explicit public APIs.
|
||||
@@ -0,0 +1,179 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, List, Dict, Union
|
||||
|
||||
from aworld.agents.llm_agent import Agent
|
||||
from aworld.config.agent_loader import _load_yaml
|
||||
from aworld.core.agent.swarm import Swarm
|
||||
from aworld.runner import Runners
|
||||
from aworld.logs.util import logger
|
||||
|
||||
from verl.experimental.agent_loop.agent_loop import AgentLoopBase, AgentLoopOutput
|
||||
|
||||
from train.adapter.verl.common import to_agent_loop_output
|
||||
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.propagate = False
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
class AworldAgentLoop(AgentLoopBase):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
async def build_agents(self) -> Union[Agent, Swarm]:
|
||||
"""Build single- or multi-agent"""
|
||||
|
||||
async def get_llm_server_address(self, server_name: str = None) -> str:
|
||||
server = self.server_manager._choose_server(server_name or uuid.uuid4().hex)
|
||||
base_url = await server.get_server_address.remote()
|
||||
base_url = f"http://{base_url}/v1"
|
||||
logger.info(f"get_server_address#base_url: {base_url}")
|
||||
return base_url
|
||||
|
||||
async def get_llm_server_model_name(self):
|
||||
model_name = "/".join(self.config.actor_rollout_ref.model.path.split("/")[-2:])
|
||||
logger.info(f"get_server_model_name#model_name: {model_name}")
|
||||
return model_name
|
||||
|
||||
# main branch
|
||||
# async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
|
||||
# messages = list(kwargs["raw_prompt"])
|
||||
|
||||
# release 0.5.0
|
||||
async def run(self, messages: list, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
|
||||
agent = await self.build_agents()
|
||||
|
||||
self.agent = agent
|
||||
|
||||
result = await self.run_agents(messages[0], agent)
|
||||
res = result.trajectory
|
||||
|
||||
# build agent loop output
|
||||
output = await self.convert_agent_output(trajectory=res,
|
||||
response_length=self.config.actor_rollout_ref.rollout.response_length)
|
||||
return output
|
||||
|
||||
async def run_agents(self, input, agent):
|
||||
if isinstance(input, dict):
|
||||
input = input.get("content", "")
|
||||
# collect trajectory
|
||||
if isinstance(agent, Swarm):
|
||||
result = Runners.sync_run(input=input, swarm=agent)
|
||||
else:
|
||||
result = Runners.sync_run(input=input, agent=agent)
|
||||
|
||||
return result
|
||||
|
||||
async def get_agent_tool_config(self, config_path: str) -> Dict[str, Any]:
|
||||
"""Load tool configuration, preferring YAML with simple fields.
|
||||
|
||||
Priority:
|
||||
1) agent_tools.yaml (simple user config with url, Authorization, MCP_SERVERS)
|
||||
2) mcp.json (legacy full config)
|
||||
"""
|
||||
|
||||
# 1) Try YAML (simple schema)
|
||||
try:
|
||||
import yaml # Local import to avoid hard dependency at import time
|
||||
if os.path.exists(config_path):
|
||||
src = _load_yaml(config_path)
|
||||
|
||||
url = src.get('url', '')
|
||||
authorization = src.get('Authorization', '')
|
||||
mcp_servers_value = src.get('MCP_SERVERS', '')
|
||||
|
||||
# Normalize servers to comma-separated string for header and list for internal
|
||||
if isinstance(mcp_servers_value, list):
|
||||
mcp_servers_str = ','.join([str(s).strip() for s in mcp_servers_value if str(s).strip()])
|
||||
else:
|
||||
mcp_servers_str = str(mcp_servers_value or '').strip()
|
||||
|
||||
# Build internal full mcp_config
|
||||
server_name = src.get('server_name', 'aworld-mcp')
|
||||
server_type = src.get('type', 'streamable-http')
|
||||
timeout = src.get('timeout', 600)
|
||||
sse_read_timeout = src.get('sse_read_timeout', 600)
|
||||
client_session_timeout_seconds = src.get('client_session_timeout_seconds', 600)
|
||||
|
||||
if url:
|
||||
mcp_config = {
|
||||
"mcpServers": {
|
||||
server_name: {
|
||||
"type": server_type,
|
||||
"url": url,
|
||||
"headers": {
|
||||
"Authorization": authorization,
|
||||
"MCP_SERVERS": mcp_servers_str,
|
||||
},
|
||||
"timeout": timeout,
|
||||
"sse_read_timeout": sse_read_timeout,
|
||||
"client_session_timeout_seconds": client_session_timeout_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
return mcp_config
|
||||
except Exception as err:
|
||||
print(f"Error loading YAML tool config err: {err}")
|
||||
|
||||
# 2) Fallback to legacy JSON
|
||||
try:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as err:
|
||||
print(f"Error loading tool config[{config_path}] err is : {err}")
|
||||
|
||||
def get_num_turns(self, trajectory: List[Dict[str, Any]]):
|
||||
return len(trajectory)
|
||||
|
||||
async def convert_agent_output(self, trajectory: List[Dict[str, Any]], response_length: int) -> AgentLoopOutput:
|
||||
"""Convert trajectory to AgentLoopOutput.
|
||||
|
||||
Args:
|
||||
trajectory (List[Dict[str, Any]]): List of agent execution trajectory.
|
||||
response_length (int): Max length of response.
|
||||
|
||||
Returns:
|
||||
AgentLoopOutput: agent loop output trajectory used for training.
|
||||
"""
|
||||
if not trajectory:
|
||||
raise Exception("Trajectory is empty")
|
||||
|
||||
num_turns = self.get_num_turns(trajectory)
|
||||
messages = trajectory[-1].get("exp_data", {}).get("messages", [])
|
||||
if not messages:
|
||||
return AgentLoopOutput(
|
||||
prompt_ids=[],
|
||||
response_ids=[],
|
||||
response_mask=[],
|
||||
num_turns=num_turns,
|
||||
metrics={},
|
||||
)
|
||||
if messages[-1].get("role") != "assistant":
|
||||
logger.warning(f"Found last message with role '{messages[-1].get('role')}', but expected 'assistant'. Truncating trailing 'tool' messages.")
|
||||
last_non_tool_index = -1
|
||||
for i in range(len(messages) - 1, -1, -1):
|
||||
if messages[i].get("role") != "tool":
|
||||
last_non_tool_index = i
|
||||
break
|
||||
if last_non_tool_index != -1:
|
||||
messages = messages[:last_non_tool_index + 1]
|
||||
else:
|
||||
messages = []
|
||||
|
||||
output = await to_agent_loop_output(tokenizer=self.tokenizer,
|
||||
messages=messages,
|
||||
response_length=response_length,
|
||||
tools=self.agent.tools)
|
||||
return output
|
||||
@@ -0,0 +1,202 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Any
|
||||
from transformers import AutoTokenizer
|
||||
from verl.experimental.agent_loop.agent_loop import AgentLoopBase, AgentLoopOutput, AgentLoopMetrics
|
||||
|
||||
|
||||
async def to_agent_loop_output(tokenizer: AutoTokenizer,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_length: int,
|
||||
tools: Dict[str, Any] = None) -> AgentLoopOutput:
|
||||
"""Convert messages to AgentLoopOutput.
|
||||
|
||||
Args:
|
||||
tokenizer (AutoTokenizer): Tokenizer for tokenize messages.
|
||||
messages (List[Dict[str, Any]]): List of messages in OpenAI request format.
|
||||
response_length (int): Max length of response.
|
||||
tools: Tool list used by the agent.
|
||||
|
||||
Returns:
|
||||
AgentLoopOutput: agent loop output trajectory used for training.
|
||||
"""
|
||||
# Ensure tools is iterable for chat templates that iterate over tools
|
||||
if tools is None:
|
||||
tools = []
|
||||
|
||||
# Normalize messages to satisfy chat templates expectations
|
||||
def _normalize_message(msg: Dict[str, Any]) -> Dict[str, Any]:
|
||||
normalized = dict(msg)
|
||||
# content may be None when assistant only returns tool_calls; make it empty string
|
||||
if normalized.get("content") is None:
|
||||
normalized["content"] = ""
|
||||
# Ensure tool_calls.function.arguments is a string (many templates expect str)
|
||||
if isinstance(normalized.get("tool_calls"), list):
|
||||
fixed_calls = []
|
||||
for call in normalized["tool_calls"]:
|
||||
call_copy = dict(call)
|
||||
func = call_copy.get("function")
|
||||
if isinstance(func, dict):
|
||||
func_copy = dict(func)
|
||||
args_val = func_copy.get("arguments")
|
||||
if not isinstance(args_val, (str, bytes)):
|
||||
try:
|
||||
func_copy["arguments"] = json.dumps(args_val, ensure_ascii=False)
|
||||
except Exception:
|
||||
func_copy["arguments"] = str(args_val)
|
||||
call_copy["function"] = func_copy
|
||||
fixed_calls.append(call_copy)
|
||||
normalized["tool_calls"] = fixed_calls
|
||||
return normalized
|
||||
|
||||
if not messages:
|
||||
return AgentLoopOutput(
|
||||
prompt_ids=[],
|
||||
response_ids=[],
|
||||
response_mask=[],
|
||||
num_turns=0,
|
||||
metrics={},
|
||||
)
|
||||
|
||||
messages = [_normalize_message(m) for m in messages]
|
||||
num_turns = 0
|
||||
for i in range(len(messages)):
|
||||
if messages[i].get("role") == "system":
|
||||
continue
|
||||
# parallel tool calls are in single turn
|
||||
if i == 0 or messages[i].get("role") != messages[i - 1].get("role"):
|
||||
num_turns += 1
|
||||
|
||||
prompt_ids = []
|
||||
response_ids = []
|
||||
response_mask = []
|
||||
chat_list = []
|
||||
loop = asyncio.get_running_loop()
|
||||
# system_prompt_prefix_ids = self.tokenizer.apply_chat_template([{}], add_generation_prompt=False, tokenize=True)
|
||||
i = 0
|
||||
try:
|
||||
while i < len(messages):
|
||||
if messages[i].get("role") == "system":
|
||||
chat_list.append(messages[i])
|
||||
i += 1
|
||||
continue
|
||||
# initial chat completion
|
||||
if messages[i].get("role") == "user":
|
||||
if i == 0 or messages[i - 1].get("role") == "system":
|
||||
chat_list.append(messages[i])
|
||||
prompt_ids = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: tokenizer.apply_chat_template(
|
||||
chat_list,
|
||||
tools=tools,
|
||||
add_generation_prompt=True,
|
||||
tokenize=True,
|
||||
),
|
||||
)
|
||||
else:
|
||||
chat_list.append(messages[i])
|
||||
cur_response_ids = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: tokenizer.apply_chat_template(
|
||||
chat_list,
|
||||
add_generation_prompt=False,
|
||||
tokenize=True,
|
||||
),
|
||||
)
|
||||
response_ids += cur_response_ids
|
||||
response_mask += [0] * len(cur_response_ids)
|
||||
chat_list = []
|
||||
i += 1
|
||||
continue
|
||||
# assistant message
|
||||
if messages[i].get("role") == "assistant":
|
||||
chat_list.append(messages[i])
|
||||
cur_response_ids = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: tokenizer.apply_chat_template(
|
||||
chat_list,
|
||||
add_generation_prompt=False,
|
||||
tokenize=True,
|
||||
),
|
||||
)
|
||||
chat_list = []
|
||||
response_ids += cur_response_ids
|
||||
response_mask += [1] * len(cur_response_ids)
|
||||
i += 1
|
||||
continue
|
||||
# follow up chat completion with tool response:
|
||||
if messages[i].get("role") == "tool":
|
||||
last_assistant_message = messages[i - 1]
|
||||
chat_list.append(last_assistant_message)
|
||||
token_assistant = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: tokenizer.apply_chat_template(
|
||||
chat_list,
|
||||
add_generation_prompt=False,
|
||||
tokenize=True,
|
||||
),
|
||||
)
|
||||
while i < len(messages) and messages[i].get("role") == "tool":
|
||||
chat_list.append(messages[i])
|
||||
i += 1
|
||||
token_assistant_tool = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: tokenizer.apply_chat_template(
|
||||
chat_list,
|
||||
add_generation_prompt=False,
|
||||
tokenize=True,
|
||||
),
|
||||
)
|
||||
tool_response_ids = token_assistant_tool[len(token_assistant):]
|
||||
chat_list = []
|
||||
response_ids += tool_response_ids
|
||||
response_mask += [0] * len(tool_response_ids)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to convert messages to agentloop_output: {messages}.Exception is: {e}")
|
||||
|
||||
max_response_length = min(response_length, len(response_ids))
|
||||
output = AgentLoopOutput(
|
||||
prompt_ids=prompt_ids,
|
||||
response_ids=response_ids[:max_response_length],
|
||||
response_mask=response_mask[:max_response_length],
|
||||
num_turns=num_turns,
|
||||
metrics={},
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def get_agent_tool_env_and_servers(tool_config: Dict[str, Any] = None) -> tuple[Dict[str, Any], List[str]]:
|
||||
if not tool_config or not tool_config.get("url") or not tool_config.get("authorization"):
|
||||
tool_config["url"] = os.getenv("MCP_SERVER_URL")
|
||||
tool_config["authorization"] = f"Bearer {os.getenv('MCP_SERVER_TOKEN')}"
|
||||
url = tool_config.get("url")
|
||||
authorization = tool_config.get("authorization")
|
||||
mcp_servers_str = tool_config.get("mcp_servers", "")
|
||||
if not url or not authorization:
|
||||
raise ValueError("url, Authorization are required. Please set MCP_SERVER_URL and MCP_SERVER_TOKEN environment variable \
|
||||
or provide them in tool_config parameter.")
|
||||
server_name = tool_config.get('server_name', 'aworld-mcp')
|
||||
server_type = tool_config.get('type', 'streamable-http')
|
||||
timeout = tool_config.get('timeout', 600)
|
||||
sse_read_timeout = tool_config.get('sse_read_timeout', 600)
|
||||
client_session_timeout_seconds = tool_config.get('client_session_timeout_seconds', 600)
|
||||
mcp_config = {
|
||||
"mcpServers": {
|
||||
server_name: {
|
||||
"type": server_type,
|
||||
"url": url,
|
||||
"headers": {
|
||||
"Authorization": authorization,
|
||||
"MCP_SERVERS": mcp_servers_str,
|
||||
},
|
||||
"timeout": timeout,
|
||||
"sse_read_timeout": sse_read_timeout,
|
||||
"client_session_timeout_seconds": client_session_timeout_seconds,
|
||||
}
|
||||
}
|
||||
}
|
||||
servers = list(server_name for server_name in mcp_config.get("mcpServers", {}).keys())
|
||||
return mcp_config, servers
|
||||
Reference in New Issue
Block a user