mirror of
https://github.com/browser-use/browser-use.git
synced 2026-03-13 07:52:54 +08:00
remove read long content action
This commit is contained in:
@@ -648,7 +648,7 @@ class FileSystem:
|
||||
truncation_note = (
|
||||
f'\n\n[Showing {len(pages_included)} of {num_pages} pages. '
|
||||
f'Skipped pages: {skipped[:10]}{"..." if len(skipped) > 10 else ""}. '
|
||||
f'Use read_long_content with a specific goal to find relevant sections.]'
|
||||
f'Use extract with start_from_char to read further into the file.]'
|
||||
)
|
||||
else:
|
||||
truncation_note = ''
|
||||
|
||||
@@ -47,7 +47,6 @@ from browser_use.tools.views import (
|
||||
InputTextAction,
|
||||
NavigateAction,
|
||||
NoParamsAction,
|
||||
ReadContentAction,
|
||||
SaveAsPdfAction,
|
||||
ScreenshotAction,
|
||||
ScrollAction,
|
||||
@@ -1696,275 +1695,6 @@ You will be given a query and the markdown of a webpage that has been filtered t
|
||||
include_extracted_content_only_once=True,
|
||||
)
|
||||
|
||||
# Intelligent content reading
|
||||
|
||||
@self.registry.action(
|
||||
'Intelligently read long content to find specific information. Works on current page (source="page") or files. For large content, uses search to identify relevant sections. Best for long articles, documents, or any content where you know what you are looking for.',
|
||||
param_model=ReadContentAction,
|
||||
)
|
||||
async def read_long_content(
|
||||
params: ReadContentAction,
|
||||
browser_session: BrowserSession,
|
||||
page_extraction_llm: BaseChatModel,
|
||||
available_file_paths: list[str],
|
||||
):
|
||||
import re
|
||||
|
||||
from browser_use.llm.messages import UserMessage
|
||||
|
||||
goal = params.goal
|
||||
context = params.context
|
||||
source = params.source
|
||||
max_chars = 50000
|
||||
|
||||
async def extract_search_terms(goal: str, context: str) -> list[str]:
|
||||
"""Use LLM to extract search terms from goal."""
|
||||
prompt = f"""Extract 3-5 key search terms from this goal that would help find relevant sections.
|
||||
Return only the terms, one per line, no numbering or bullets.
|
||||
|
||||
Goal: {goal}
|
||||
|
||||
Context: {context}"""
|
||||
response = await page_extraction_llm.ainvoke([UserMessage(content=prompt)])
|
||||
return [term.strip() for term in response.completion.strip().split('\n') if term.strip()][:5]
|
||||
|
||||
def search_text(content: str, pattern: str, context_chars: int = 100) -> list[dict]:
|
||||
"""Search content for pattern, return matches with positions."""
|
||||
try:
|
||||
regex = re.compile(pattern, re.IGNORECASE)
|
||||
except re.error:
|
||||
regex = re.compile(re.escape(pattern), re.IGNORECASE)
|
||||
|
||||
matches = []
|
||||
for match in regex.finditer(content):
|
||||
start = max(0, match.start() - context_chars)
|
||||
end = min(len(content), match.end() + context_chars)
|
||||
matches.append(
|
||||
{
|
||||
'position': match.start(),
|
||||
'snippet': content[start:end],
|
||||
}
|
||||
)
|
||||
return matches
|
||||
|
||||
def chunk_content(content: str, chunk_size: int = 2000) -> list[dict]:
|
||||
"""Split content into chunks with positions."""
|
||||
chunks = []
|
||||
for i in range(0, len(content), chunk_size):
|
||||
chunks.append(
|
||||
{
|
||||
'start': i,
|
||||
'end': min(i + chunk_size, len(content)),
|
||||
'text': content[i : i + chunk_size],
|
||||
}
|
||||
)
|
||||
return chunks
|
||||
|
||||
try:
|
||||
if source.lower() == 'page':
|
||||
# Read from current webpage
|
||||
from browser_use.dom.markdown_extractor import extract_clean_markdown
|
||||
|
||||
# Clear DOM cache and wait for page to settle before extracting
|
||||
if browser_session._dom_watchdog:
|
||||
browser_session._dom_watchdog.clear_cache()
|
||||
|
||||
wait_time = browser_session.browser_profile.wait_for_network_idle_page_load_time
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
content, _ = await extract_clean_markdown(browser_session=browser_session, extract_links=False)
|
||||
source_name = 'current page'
|
||||
|
||||
if not content:
|
||||
return ActionResult(
|
||||
extracted_content='Error: No page content available',
|
||||
long_term_memory='Failed to read page: no content',
|
||||
)
|
||||
|
||||
else:
|
||||
# Read from file
|
||||
file_path = source
|
||||
|
||||
# Validate file path against whitelist (available_file_paths + downloaded files)
|
||||
allowed_paths = set(available_file_paths or [])
|
||||
allowed_paths.update(browser_session.downloaded_files)
|
||||
if file_path not in allowed_paths:
|
||||
return ActionResult(
|
||||
extracted_content=f'Error: File path not in available_file_paths: {file_path}. '
|
||||
f'The user must add this path to available_file_paths when creating the Agent.',
|
||||
long_term_memory=f'Failed to read: file path not allowed: {file_path}',
|
||||
)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return ActionResult(
|
||||
extracted_content=f'Error: File not found: {file_path}',
|
||||
long_term_memory='Failed to read: file not found',
|
||||
)
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
source_name = os.path.basename(file_path)
|
||||
|
||||
if ext == '.pdf':
|
||||
# Read PDF directly using pypdf
|
||||
import pypdf
|
||||
|
||||
reader = pypdf.PdfReader(file_path)
|
||||
num_pages = len(reader.pages)
|
||||
|
||||
# Extract all page text
|
||||
page_texts: list[str] = []
|
||||
total_chars = 0
|
||||
for page in reader.pages:
|
||||
text = page.extract_text() or ''
|
||||
page_texts.append(text)
|
||||
total_chars += len(text)
|
||||
|
||||
# If PDF is small enough, return it all
|
||||
if total_chars <= max_chars:
|
||||
content_parts = []
|
||||
for i, text in enumerate(page_texts, 1):
|
||||
if text.strip():
|
||||
content_parts.append(f'--- Page {i} ---\n{text}')
|
||||
content = '\n\n'.join(content_parts)
|
||||
|
||||
memory = f'Read {source_name} ({num_pages} pages, {total_chars:,} chars) for goal: {goal[:50]}'
|
||||
logger.info(f'📄 {memory}')
|
||||
return ActionResult(
|
||||
extracted_content=f'PDF: {source_name} ({num_pages} pages)\n\n{content}',
|
||||
long_term_memory=memory,
|
||||
include_extracted_content_only_once=True,
|
||||
)
|
||||
|
||||
# PDF too large - use intelligent extraction
|
||||
logger.info(f'PDF has {total_chars:,} chars across {num_pages} pages, using intelligent extraction')
|
||||
|
||||
# Extract search terms from goal
|
||||
search_terms = await extract_search_terms(goal, context)
|
||||
|
||||
# Search and score pages by relevance
|
||||
page_scores: dict[int, int] = {} # 1-indexed page -> score
|
||||
for term in search_terms:
|
||||
try:
|
||||
term_pattern = re.compile(re.escape(term), re.IGNORECASE)
|
||||
except re.error:
|
||||
continue
|
||||
for i, text in enumerate(page_texts, 1):
|
||||
if term_pattern.search(text):
|
||||
page_scores[i] = page_scores.get(i, 0) + 1
|
||||
|
||||
# Select pages: always include page 1, then most relevant
|
||||
pages_to_read = [1]
|
||||
sorted_pages = sorted(page_scores.items(), key=lambda x: -x[1])
|
||||
for page_num, _ in sorted_pages:
|
||||
if page_num not in pages_to_read:
|
||||
pages_to_read.append(page_num)
|
||||
|
||||
# Build result respecting char limit, truncating pages if needed
|
||||
content_parts = []
|
||||
chars_used = 0
|
||||
pages_included = []
|
||||
for page_num in sorted(set(pages_to_read)):
|
||||
text = page_texts[page_num - 1]
|
||||
page_header = f'--- Page {page_num} ---\n'
|
||||
remaining = max_chars - chars_used
|
||||
if remaining < len(page_header) + 50:
|
||||
break # no room for meaningful content
|
||||
page_content = page_header + text
|
||||
if len(page_content) > remaining:
|
||||
page_content = page_content[: remaining - len('\n[...truncated]')] + '\n[...truncated]'
|
||||
content_parts.append(page_content)
|
||||
chars_used += len(page_content)
|
||||
pages_included.append(page_num)
|
||||
|
||||
content = '\n\n'.join(content_parts)
|
||||
memory = f'Read {source_name} ({len(pages_included)} relevant pages of {num_pages}) for goal: {goal[:50]}'
|
||||
logger.info(f'📄 {memory}')
|
||||
return ActionResult(
|
||||
extracted_content=f'PDF: {source_name} ({num_pages} pages, showing {len(pages_included)} relevant)\n\n{content}',
|
||||
long_term_memory=memory,
|
||||
include_extracted_content_only_once=True,
|
||||
)
|
||||
|
||||
else:
|
||||
# Text file
|
||||
async with await anyio.open_file(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = await f.read()
|
||||
|
||||
# Check if content fits in budget
|
||||
if len(content) <= max_chars:
|
||||
memory = f'Read {source_name} ({len(content):,} chars) for goal: {goal[:50]}'
|
||||
logger.info(f'📄 {memory}')
|
||||
return ActionResult(
|
||||
extracted_content=f'Content from {source_name} ({len(content):,} chars):\n\n{content}',
|
||||
long_term_memory=memory,
|
||||
include_extracted_content_only_once=True,
|
||||
)
|
||||
|
||||
# Content too large - use intelligent extraction
|
||||
logger.info(f'Content has {len(content):,} chars, using intelligent extraction')
|
||||
|
||||
# Extract search terms from goal
|
||||
search_terms = await extract_search_terms(goal, context)
|
||||
|
||||
# Search for each term and score chunks
|
||||
chunks = chunk_content(content, chunk_size=2000)
|
||||
chunk_scores: dict[int, int] = {} # chunk index -> relevance score
|
||||
|
||||
for term in search_terms:
|
||||
matches = search_text(content, term)
|
||||
for match in matches:
|
||||
# Find which chunk this match belongs to
|
||||
for i, chunk in enumerate(chunks):
|
||||
if chunk['start'] <= match['position'] < chunk['end']:
|
||||
chunk_scores[i] = chunk_scores.get(i, 0) + 1
|
||||
break
|
||||
|
||||
if not chunk_scores:
|
||||
# No matches - return first max_chars
|
||||
truncated = content[:max_chars]
|
||||
memory = f'Read {source_name} (truncated to {max_chars:,} chars, no matches for search terms)'
|
||||
logger.info(f'📄 {memory}')
|
||||
return ActionResult(
|
||||
extracted_content=f'Content from {source_name} (first {max_chars:,} of {len(content):,} chars):\n\n{truncated}',
|
||||
long_term_memory=memory,
|
||||
include_extracted_content_only_once=True,
|
||||
)
|
||||
|
||||
# Sort chunks by relevance and collect most relevant ones
|
||||
sorted_chunks = sorted(chunk_scores.items(), key=lambda x: -x[1])
|
||||
|
||||
# Always include first chunk for context
|
||||
selected_indices = {0} # Start with first chunk
|
||||
for chunk_idx, _ in sorted_chunks:
|
||||
selected_indices.add(chunk_idx)
|
||||
|
||||
# Build result from selected chunks in order
|
||||
result_parts = []
|
||||
total_chars = 0
|
||||
for i in sorted(selected_indices):
|
||||
chunk = chunks[i]
|
||||
if total_chars + len(chunk['text']) > max_chars:
|
||||
break
|
||||
if i > 0 and (i - 1) not in selected_indices:
|
||||
result_parts.append('\n[...]\n') # Indicate gap
|
||||
result_parts.append(chunk['text'])
|
||||
total_chars += len(chunk['text'])
|
||||
|
||||
result_content = ''.join(result_parts)
|
||||
memory = f'Read {source_name} ({len(selected_indices)} relevant sections of {len(chunks)}) for goal: {goal[:50]}'
|
||||
logger.info(f'📄 {memory}')
|
||||
|
||||
return ActionResult(
|
||||
extracted_content=f'Content from {source_name} (relevant sections, {total_chars:,} of {len(content):,} chars):\n\n{result_content}',
|
||||
long_term_memory=memory,
|
||||
include_extracted_content_only_once=True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'Error reading content: {str(e)}'
|
||||
logger.error(error_msg)
|
||||
return ActionResult(extracted_content=error_msg, long_term_memory=error_msg)
|
||||
|
||||
@self.registry.action(
|
||||
"""Execute browser JavaScript. Best practice: wrap in IIFE (function(){...})() with try-catch for safety. Use ONLY browser APIs (document, window, DOM). NO Node.js APIs (fs, require, process). Example: (function(){try{const el=document.querySelector('#id');return el?el.value:'not found'}catch(e){return 'Error: '+e.message}})() Avoid comments. Use for hover, drag, zoom, custom selectors, extract/filter links, or analysing page structure. IMPORTANT: Shadow DOM elements with [index] markers can be clicked directly with click(index) — do NOT use evaluate() to click them. Only use evaluate for shadow DOM elements that are NOT indexed. Limit output size.""",
|
||||
terminates_sequence=True,
|
||||
|
||||
@@ -159,17 +159,6 @@ class SaveAsPdfAction(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ReadContentAction(BaseModel):
|
||||
"""Action for intelligent reading of long content."""
|
||||
|
||||
goal: str = Field(description='What to look for or extract from the content')
|
||||
source: str = Field(
|
||||
default='page',
|
||||
description='What to read: "page" for current webpage, or a file path',
|
||||
)
|
||||
context: str = Field(default='', description='Additional context about the task')
|
||||
|
||||
|
||||
class GetDropdownOptionsAction(BaseModel):
|
||||
index: int
|
||||
|
||||
|
||||
Reference in New Issue
Block a user