Add fetch_url tool for reading webpage content

Bot can now fetch and read the text content of URLs shared in chat.
Uses httpx for async HTTP and BeautifulSoup to strip HTML noise.
Output is capped at 8000 chars to avoid blowing the context window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Suppai_NekoandClaude Sonnet 4.6 committed 2026-03-25 16:58:29 -07:00
1 parent c823423c8b
commit 4431376ad5
2 files changed
+51 -1

No files matched your search

+49 -1
View File
@@ -133,6 +133,28 @@ FETCH_CHANNEL_HISTORY_TOOL = {
}
FETCH_URL_TOOL = {
"type": "function",
"function": {
"name": "fetch_url",
"description": (
"Fetch and read the text content of a webpage given its URL. "
"Use this when someone shares a link and wants you to read, summarise, or discuss its contents."
),
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to fetch.",
}
},
"required": ["url"],
},
},
}
def load_personality():
global personality_notes
if PERSONALITY_FILE.exists():
@@ -557,6 +579,30 @@ async def call_mcp_tool(name: str, args: dict) -> str:
return "\n".join(parts)
async def fetch_url(url: str, max_chars: int = 8000) -> str:
import httpx
from bs4 import BeautifulSoup
try:
async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client:
r = await client.get(url, headers={"User-Agent": "Mozilla/5.0"})
r.raise_for_status()
ct = r.headers.get("content-type", "")
if "html" in ct:
soup = BeautifulSoup(r.text, "html.parser")
for tag in soup(["script", "style", "nav", "footer", "header"]):
tag.decompose()
text = soup.get_text(separator="\n", strip=True)
else:
text = r.text
lines = [l for l in text.splitlines() if l.strip()]
result = "\n".join(lines)
if len(result) > max_chars:
result = result[:max_chars] + "\n\n[truncated]"
return result or "Page returned no readable text."
except Exception as exc:
return f"Failed to fetch URL: {exc}"
def _ensure_user_tail(messages: list) -> None:
"""Qwen's chat template requires the last message to be from the user. Inject a continuation prompt if needed."""
if messages and messages[-1]["role"] not in ("user", "system"):
@@ -582,7 +628,7 @@ async def fetch_discord_history(channel: discord.abc.Messageable, limit: int = 5
async def run_agent(messages: list, channel: discord.abc.Messageable | None = None) -> str:
"""Call the LLM in a loop, executing tool calls until a final reply is produced."""
all_tools = openai_tools + [PERSONALITY_TOOL, UPDATE_REFLECTION_NOTES_TOOL, READ_LIFE_HISTORY_TOOL]
all_tools = openai_tools + [PERSONALITY_TOOL, UPDATE_REFLECTION_NOTES_TOOL, READ_LIFE_HISTORY_TOOL, FETCH_URL_TOOL]
if channel is not None:
all_tools = all_tools + [FETCH_CHANNEL_HISTORY_TOOL]
kwargs = {"model": MODEL, "messages": list(messages), "tools": all_tools, "tool_choice": "required"}
@@ -633,6 +679,8 @@ async def run_agent(messages: list, channel: discord.abc.Messageable | None = No
result = await fetch_discord_history(channel, limit)
else:
result = "Channel history is not available in this context."
elif tc.function.name == "fetch_url":
result = await fetch_url(args["url"])
else:
result = await call_mcp_tool(tc.function.name, args)
+2
View File
@@ -2,3 +2,5 @@ discord.py>=2.3
openai>=1.0
python-dotenv>=1.0
mcp>=1.0
httpx>=0.27
beautifulsoup4>=4.12