Skip to content

Commit a0731b3

Browse files
jasinluoRook1ex
authored andcommitted
feature: 新增 web_search 工具,支持 duckduckgo 与 google
新增 web_fetch 工具 新增 相关示例和使用文档
1 parent 0c9f4e1 commit a0731b3

21 files changed

Lines changed: 5246 additions & 0 deletions

File tree

docs/mkdocs/en/tool.md

Lines changed: 583 additions & 0 deletions
Large diffs are not rendered by default.

docs/mkdocs/zh/tool.md

Lines changed: 585 additions & 0 deletions
Large diffs are not rendered by default.

examples/webfetch_tool/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Set TRPC_AGENT_API_KEY、TRPC_AGENT_BASE_URL、TRPC_AGENT_MODEL_NAME
2+
TRPC_AGENT_API_KEY=your_api_key
3+
TRPC_AGENT_BASE_URL=your_base_url
4+
TRPC_AGENT_MODEL_NAME=your_model_name

examples/webfetch_tool/README.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# WebFetchTool 联网抓取示例
2+
3+
本示例演示如何使用框架内置的 `WebFetchTool`,让 LLM Agent 通过公网 HTTP GET 抓取单个 URL,并按照"白名单 / 黑名单域名 + SSRF 防护 + 内置 LRU 缓存"的规则生成可追溯的回答。
4+
5+
## 关键特性
6+
7+
- **单次 HTTP GET + 文本化**:对指定 URL 发一次无鉴权 GET;HTML 被转换为 Markdown-ish 纯文本,其它 `text/*` / `application/json` 等文本型 MIME 按原样返回;二进制响应以结构化 `UNSUPPORTED_CONTENT_TYPE` 错误拒收
8+
- **域名白名单/黑名单**:通过构造参数 `allowed_domains` / `blocked_domains` 实现工具级 site 过滤(**LLM 无法覆盖**);子域感知匹配(`www.` 前缀剥离,`python.org` 同时匹配 `docs.python.org`),黑名单优先于白名单
9+
- **SSRF 防护**`block_private_network=True`(默认)会在每次请求及**每一跳重定向**后解析主机,拒绝回环 / 私网 / 链路本地(含 `169.254.169.254` 云元数据端点)/ 保留 / 组播 / 未指定地址
10+
- **结构化输出**`FetchResult` 统一返回 `url / status_code / status_text / content_type / content / bytes / duration_ms / cached / error`,便于 LLM 引用与降级
11+
- **内容与字节双重裁剪**`max_content_length`(字符)与 `max_response_bytes`(字节)分别控制返回文本长度与**线缆上实际读取的原始字节**;LLM 还可通过调用参数 `max_length` 进一步按需收紧
12+
- **内置 LRU 缓存**`enable_cache=True` 时启用进程内 URL → `FetchResult` LRU;`cache_ttl_seconds` / `cache_max_bytes` 控制新鲜度与预算,命中时响应上 `cached=true`
13+
14+
## Agent 层级结构说明
15+
16+
本例提供四个独立 Agent,按场景顺序驱动;`root_agent` 默认指向 `default_fetch_agent`
17+
18+
```text
19+
default_fetch_agent (LlmAgent) # 基线:HTTP 形态 + SSRF 默认项
20+
├── model: OpenAIModel
21+
├── tools:
22+
│ └── WebFetchTool(
23+
│ timeout=10.0,
24+
│ user_agent="trpc-agent-python-webfetch-example/1.0",
25+
│ max_content_length=4000,
26+
│ max_response_bytes=1 MiB,
27+
│ follow_redirects=True,
28+
│ max_redirects=3,
29+
│ block_private_network=True,
30+
│ )
31+
└── session: InMemorySessionService
32+
33+
cached_fetch_agent (LlmAgent) # 进程内 LRU 缓存
34+
├── model: OpenAIModel
35+
├── tools:
36+
│ └── WebFetchTool(
37+
│ enable_cache=True,
38+
│ cache_ttl_seconds=120.0,
39+
│ cache_max_bytes=2 MiB,
40+
│ )
41+
└── session: InMemorySessionService
42+
43+
whitelist_fetch_agent (LlmAgent) # 域名白名单
44+
├── model: OpenAIModel
45+
├── tools:
46+
│ └── WebFetchTool(allowed_domains=["python.org"])
47+
└── session: InMemorySessionService
48+
49+
blocklist_fetch_agent (LlmAgent) # 域名黑名单
50+
├── model: OpenAIModel
51+
├── tools:
52+
│ └── WebFetchTool(blocked_domains=["example.com"])
53+
└── session: InMemorySessionService
54+
```
55+
56+
关键文件:
57+
58+
- [examples/webfetch_tool/agent/agent.py](./agent/agent.py):构建四个 `LlmAgent`,分别覆盖 HTTP 形态默认项、LRU 缓存、白名单、黑名单
59+
- [examples/webfetch_tool/agent/prompts.py](./agent/prompts.py):网页阅读助手提示词,要求引用 URL、复述 `BLOCKED_URL` / `HTTP_STATUS` 错误、并在命中缓存时告知用户
60+
- [examples/webfetch_tool/agent/config.py](./agent/config.py):环境变量读取(LLM 凭据)
61+
- [examples/webfetch_tool/run_agent.py](./run_agent.py):测试入口,依次执行基线抓取、`max_length` 按需裁剪、缓存命中、白名单拒绝、黑名单拒绝等场景
62+
63+
## 关键代码解释
64+
65+
这一节用于快速定位"HTTP 形态默认项、LRU 缓存、域名过滤、SSRF 防护"四条核心链路。
66+
67+
### 1) HTTP 形态默认项(`agent/agent.py::create_default_fetch_agent`
68+
69+
- `timeout`:HTTP 读写超时,示例压到 10s 便于快速暴露网络异常
70+
- `user_agent`:线上日志可由此识别示例流量
71+
- `max_content_length`:返回 `content` 字符上限(示例 4000,便于屏显);LLM 调用参数 `max_length` 可进一步**按调用覆盖**
72+
- `max_response_bytes`:线缆上允许读取的**原始字节**上限(示例 1 MiB)。流式读取遇到该上限即终止下载,避免大文件吃满解码/内存预算
73+
- `follow_redirects` / `max_redirects`:手动重定向循环,上限示例 3 跳
74+
- `block_private_network`:默认开启的 SSRF 边界,**每一跳**都会重新做 DNS 解析校验
75+
76+
### 2) LRU 缓存(`agent/agent.py::create_cached_fetch_agent`
77+
78+
- `enable_cache=True`:打开进程内 URL → `FetchResult` LRU(默认关闭,需显式 opt-in)
79+
- `cache_ttl_seconds`:单条新鲜度,超时后下一次读触发穿透 + 淘汰
80+
- `cache_max_bytes`:总字节预算,满后按 LRU 淘汰;**单条体积超过预算时会被静默跳过**
81+
- 缓存键会做 URL 归一化(统一 scheme 大小写、剥离 `www.`、忽略默认端口、忽略尾 `/`),`https://example.com``https://www.example.com/` 共享同一条缓存项
82+
- 命中时 `FetchResult.cached = True`,便于下游判断新鲜度
83+
84+
### 3) 域名白/黑名单(`agent/agent.py::create_whitelist_fetch_agent` / `create_blocklist_fetch_agent`
85+
86+
- `allowed_domains` / `blocked_domains`**工具级**配置,**LLM 无法在调用参数里覆盖**;这是与 `WebSearchTool`(LLM 可逐调用传入名单)的主要差别
87+
- 匹配规则:`www.` 前缀剥离,子域感知(`python.org` 同时匹配 `docs.python.org`
88+
- **黑名单优先**:同一主机同时命中两张名单时仍被拒绝,返回错误 `BLOCKED_URL: '<host>' is not permitted by the tool's domain policy`
89+
- 每一跳重定向都会重新套一遍名单校验,防止"合法首跳 → 跳到被禁主机"的绕过
90+
91+
### 4) SSRF 防护(`block_private_network=True`
92+
93+
- 在发起请求**和每一跳重定向**时对目标主机做 DNS 解析,命中以下任一即拒绝:
94+
- 私网(RFC 1918 / ULA)、回环(`127.0.0.0/8`)、链路本地(含 `169.254.169.254` 云元数据端点)
95+
- 保留 / 组播 / 未指定地址
96+
- 仅当调用方已用外部白名单限定目标、并确信输入可信(如内网集群)时才考虑关闭该开关
97+
98+
## 环境与运行
99+
100+
### 环境要求
101+
102+
- Python 3.12
103+
104+
### 安装步骤
105+
106+
```bash
107+
git clone https://github.com/trpc-group/trpc-agent-python.git
108+
cd trpc-agent-python
109+
python3 -m venv .venv
110+
source .venv/bin/activate
111+
pip3 install -e .
112+
```
113+
114+
### 环境变量要求
115+
116+
[examples/webfetch_tool/.env](./.env) 中配置(或通过 `export`):
117+
118+
必填(LLM 凭据):
119+
120+
- `TRPC_AGENT_API_KEY`
121+
- `TRPC_AGENT_BASE_URL`
122+
- `TRPC_AGENT_MODEL_NAME`
123+
124+
### 运行命令
125+
126+
```bash
127+
cd examples/webfetch_tool
128+
python3 run_agent.py
129+
```
130+
131+
## 运行结果
132+
133+
```text
134+
========== Default · plain fetch ==========
135+
🆔 Session ID: d422c00b...
136+
📝 User: Fetch https://example.com and summarise the page in one short paragraph.
137+
🤖 Assistant:
138+
🔧 [Invoke Tool: webfetch({'url': 'https://example.com', 'max_length': 500})]
139+
📊 [Tool Result: url='https://example.com' status=200 content_type='text/html' bytes=183 cached=False duration_ms=87 preview='Example Domain # Example Domain This domain is for use in documentation examples without needing permission. Avoid ...']
140+
The page at **Example Domain** is a placeholder used for documentation purposes. It states that the domain is intended for examples and should not be used in actual operations. A link is provided to learn more about such domains.
141+
142+
Source: [https://example.com](https://example.com)
143+
----------------------------------------
144+
145+
========== Default · per-call max_length override ==========
146+
🆔 Session ID: 47afb4e2...
147+
📝 User: Fetch https://example.com but only return the first ~200 characters of the body. Use max_length=200 on the tool call.
148+
🤖 Assistant:
149+
🔧 [Invoke Tool: webfetch({'url': 'https://example.com', 'max_length': 200})]
150+
📊 [Tool Result: url='https://example.com' status=200 content_type='text/html' bytes=183 cached=False duration_ms=75 preview='Example Domain # Example Domain This domain is for use in documentation examples without needing permission. Avoid ...']
151+
Here is the first ~200 characters of the content from https://example.com:
152+
153+
Example Domain
154+
155+
# Example Domain
156+
157+
This domain is for use in documentation examples without needing permission. Avoid use in operations.
158+
159+
[Learn more](https://iana.org/domains/example)
160+
161+
Source: [https://example.com](https://example.com)
162+
----------------------------------------
163+
164+
========== Cache · first fetch (network) ==========
165+
🆔 Session ID: 64dbc485...
166+
📝 User: Fetch https://example.com and summarise the page in one short paragraph.
167+
🤖 Assistant:
168+
🔧 [Invoke Tool: webfetch({'url': 'https://example.com', 'max_length': 500})]
169+
[INFO] WebFetchTool put result for https://example.com in LRU cache
170+
📊 [Tool Result: url='https://example.com' status=200 content_type='text/html' bytes=183 cached=False duration_ms=79 preview='Example Domain # Example Domain This domain is for use in documentation examples without needing permission. Avoid ...']
171+
The page at **Example Domain** is a placeholder used for documentation purposes. It states that this domain is intended for examples and should not be used in actual operations. A link is provided to learn more about such domains.
172+
173+
Source: [https://example.com](https://example.com)
174+
----------------------------------------
175+
176+
========== Cache · second fetch (cache hit) ==========
177+
🆔 Session ID: 857e415e...
178+
📝 User: Fetch https://example.com again and summarise the page in one short paragraph. Tell me whether the tool served the response from the cache.
179+
🤖 Assistant:
180+
🔧 [Invoke Tool: webfetch({'url': 'https://example.com', 'max_length': 500})]
181+
[INFO] WebFetchTool got cached result for https://example.com in LRU cache
182+
📊 [Tool Result: url='https://example.com' status=200 content_type='text/html' bytes=183 cached=True duration_ms=79 preview='Example Domain # Example Domain This domain is for use in documentation examples without needing permission. Avoid ...']
183+
The page at **Example Domain** is a placeholder used for documentation purposes, advising users to avoid operational use. It provides a link to learn more about such domains.
184+
185+
The response was served from the in-process cache, so the content may be slightly stale (up to the tool's configured TTL).
186+
187+
Source: [https://example.com](https://example.com)
188+
----------------------------------------
189+
190+
========== Whitelist · allowed host (python.org) ==========
191+
🆔 Session ID: d56de0e1...
192+
📝 User: Fetch https://www.python.org/ and summarise the landing page in one short paragraph.
193+
🤖 Assistant:
194+
🔧 [Invoke Tool: webfetch({'url': 'https://www.python.org/', 'max_length': 500})]
195+
📊 [Tool Result: url='https://www.python.org/' status=200 content_type='text/html' bytes=505 cached=False duration_ms=413 preview='Welcome to Python.org **Notice:** This page displays a fallback because interactive scripts did not run. Possible ca...']
196+
The Python.org landing page welcomes visitors to the official site of the Python programming language. It provides quick links to essential resources like the Python Software Foundation (PSF), documentation, the Python Package Index (PyPI), and more. The page also includes navigation options for exploring Python further.
197+
198+
Source: [Python.org](https://www.python.org/)
199+
----------------------------------------
200+
201+
========== Whitelist · rejected host (example.com) ==========
202+
🆔 Session ID: 980ff5ad...
203+
📝 User: Fetch https://example.com and summarise the page. If the tool refuses the URL, explain why.
204+
🤖 Assistant:
205+
🔧 [Invoke Tool: webfetch({'url': 'https://example.com'})]
206+
📊 [Tool Result: error="BLOCKED_URL: 'example.com' is not permitted by the tool's domain policy" url='https://example.com' status=None]
207+
The tool refused to fetch the URL `https://example.com` because the domain `example.com` is not permitted by the tool's domain policy. This means the tool has a blocklist or allowlist in place that restricts access to certain domains, and `example.com` is one of them.
208+
209+
If you need information from a different domain, please provide a URL that is allowed by the tool's policy.
210+
----------------------------------------
211+
212+
========== Blacklist · rejected host (example.com) ==========
213+
🆔 Session ID: a58386ba...
214+
📝 User: Fetch https://example.com and summarise the page. If the tool refuses the URL, explain why.
215+
🤖 Assistant:
216+
🔧 [Invoke Tool: webfetch({'url': 'https://example.com'})]
217+
📊 [Tool Result: error="BLOCKED_URL: 'example.com' is not permitted by the tool's domain policy" url='https://example.com' status=None]
218+
The tool refused to fetch the URL `https://example.com` because the domain `example.com` is blocked by the tool's domain policy. This means the tool is configured to disallow requests to this specific domain.
219+
220+
If you need information from this domain, you may need to use an alternative method or tool to access it.
221+
----------------------------------------
222+
```
223+
224+
## 适用场景建议
225+
226+
- 需要 LLM 阅读 **指定网页并引用 URL** 的场景:适合直接复用本示例 `WebFetchTool` + 提示词约定
227+
-**公网 SSRF 风险敏感**(例如 Agent 跑在云实例、能访问内网元数据端点):保留 `block_private_network=True` 默认值
228+
- 存在 **热点页面反复读取**(文档、changelog、status page):打开 `enable_cache=True` 并按请求体量调整 `cache_max_bytes`
229+
- 需要把抓取工具限定在一小组可信站点(白名单)或屏蔽已知噪声站点(黑名单):通过 `allowed_domains` / `blocked_domains` 配置,**这两个参数 LLM 无法覆盖**
230+
- 需要在工具调用前后插入日志、审计、参数校验:参考 [examples/filter_with_tool](../filter_with_tool),把 `WebFetchTool(filters_name=[...])``before_tool_callback` / `after_tool_callback` 组合使用
231+
- 需要自定义 HTTP 形态(公司代理、mTLS、连接池复用):通过构造参数 `proxy``http_client` 注入,本示例未涉及
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
""" Agent module for WebFetchTool"""
7+
8+
from trpc_agent_sdk.agents import LlmAgent
9+
from trpc_agent_sdk.models import LLMModel
10+
from trpc_agent_sdk.models import OpenAIModel
11+
from trpc_agent_sdk.tools import WebFetchTool
12+
13+
from .config import get_model_config
14+
from .prompts import INSTRUCTION
15+
16+
17+
def _create_model() -> LLMModel:
18+
"""Create the LLM model used by every demo agent."""
19+
api_key, url, model_name = get_model_config()
20+
return OpenAIModel(model_name=model_name, api_key=api_key, base_url=url)
21+
22+
23+
def create_default_fetch_agent() -> LlmAgent:
24+
"""Build the baseline webfetch agent.
25+
26+
The construction below pins every HTTP-shape knob explicitly so the
27+
example doubles as inline documentation:
28+
29+
- ``timeout`` — lower than the 30s default so transient network
30+
hiccups surface quickly in the demo.
31+
- ``user_agent`` — identifies the demo on the wire so downstream
32+
logs can tell example traffic apart from real deployments.
33+
- ``max_content_length`` — cap returned ``content`` text so the
34+
demo output stays readable even for long pages.
35+
- ``max_response_bytes`` — hard byte budget on the raw wire read
36+
(~1 MiB here) that protects decode / memory budgets before the
37+
char cap kicks in.
38+
- ``follow_redirects`` / ``max_redirects`` — keep the redirect
39+
loop bounded while still handling ``http`` → ``https`` hops.
40+
- ``block_private_network`` — default-on SSRF boundary; pinned
41+
here to make the intent explicit.
42+
"""
43+
web_fetch = WebFetchTool(
44+
timeout=10.0,
45+
user_agent="trpc-agent-python-webfetch-example/1.0",
46+
max_content_length=4000,
47+
max_response_bytes=1 * 1024 * 1024,
48+
follow_redirects=True,
49+
max_redirects=3,
50+
block_private_network=True,
51+
)
52+
return LlmAgent(
53+
name="default_webfetch_assistant",
54+
description="Web-reading assistant that fetches a single URL and summarises its textual content.",
55+
model=_create_model(),
56+
instruction=INSTRUCTION,
57+
tools=[web_fetch],
58+
)
59+
60+
61+
def create_cached_fetch_agent() -> LlmAgent:
62+
"""Build a webfetch agent that enables the in-process LRU cache.
63+
64+
Cache knobs covered:
65+
66+
- ``enable_cache=True`` — opt in to the URL → ``FetchResult`` LRU.
67+
- ``cache_ttl_seconds`` — how long an entry is considered fresh.
68+
- ``cache_max_bytes`` — total byte budget for the cache; entries
69+
larger than this limit are silently skipped.
70+
71+
The demo fetches the same URL twice in a row so the second call
72+
comes back with ``cached=true`` on the tool response.
73+
"""
74+
web_fetch = WebFetchTool(
75+
timeout=10.0,
76+
user_agent="trpc-agent-python-webfetch-example/1.0",
77+
max_content_length=4000,
78+
max_response_bytes=1 * 1024 * 1024,
79+
enable_cache=True,
80+
cache_ttl_seconds=120.0,
81+
cache_max_bytes=2 * 1024 * 1024,
82+
)
83+
return LlmAgent(
84+
name="cached_webfetch_assistant",
85+
description=("Web-reading assistant with an in-process LRU cache so repeated "
86+
"fetches of the same URL skip the network."),
87+
model=_create_model(),
88+
instruction=INSTRUCTION,
89+
tools=[web_fetch],
90+
)
91+
92+
93+
def create_whitelist_fetch_agent() -> LlmAgent:
94+
"""Build a webfetch agent that only permits a closed set of hosts.
95+
96+
Configures ``allowed_domains`` so every non-whitelisted URL is
97+
rejected with ``BLOCKED_URL`` before the HTTP GET is issued. The
98+
matching is subdomain-aware (``www.`` stripped), so ``python.org``
99+
also lets ``docs.python.org`` through.
100+
"""
101+
web_fetch = WebFetchTool(
102+
timeout=10.0,
103+
user_agent="trpc-agent-python-webfetch-example/1.0",
104+
max_content_length=4000,
105+
allowed_domains=["python.org"],
106+
)
107+
return LlmAgent(
108+
name="whitelist_webfetch_assistant",
109+
description=("Web-reading assistant restricted to a domain whitelist; "
110+
"off-list URLs are rejected with BLOCKED_URL."),
111+
model=_create_model(),
112+
instruction=INSTRUCTION,
113+
tools=[web_fetch],
114+
)
115+
116+
117+
def create_blocklist_fetch_agent() -> LlmAgent:
118+
"""Build a webfetch agent that rejects a named set of hosts.
119+
120+
Configures ``blocked_domains`` so any URL whose host matches
121+
(subdomain-aware, ``www.`` stripped) is rejected with
122+
``BLOCKED_URL``. Blocks are evaluated *before* the allow list, so a
123+
host present in both lists is still rejected.
124+
"""
125+
web_fetch = WebFetchTool(
126+
timeout=10.0,
127+
user_agent="trpc-agent-python-webfetch-example/1.0",
128+
max_content_length=4000,
129+
blocked_domains=["example.com"],
130+
)
131+
return LlmAgent(
132+
name="blocklist_webfetch_assistant",
133+
description=("Web-reading assistant with a domain blacklist; "
134+
"blacklisted URLs are rejected with BLOCKED_URL."),
135+
model=_create_model(),
136+
instruction=INSTRUCTION,
137+
tools=[web_fetch],
138+
)
139+
140+
141+
default_fetch_agent = create_default_fetch_agent()
142+
cached_fetch_agent = create_cached_fetch_agent()
143+
whitelist_fetch_agent = create_whitelist_fetch_agent()
144+
blocklist_fetch_agent = create_blocklist_fetch_agent()
145+
root_agent = default_fetch_agent

0 commit comments

Comments
 (0)