renamed + added functionality

This commit is contained in:
nicole pardal 2025-09-23 17:44:18 -07:00
parent 1c6afe4316
commit ae333084b9
2 changed files with 99 additions and 66 deletions

View File

@ -3,19 +3,65 @@ from __future__ import annotations
import os import os
from typing import Any, Dict, List from typing import Any, Dict, List
from browser_tool_helpers import Browser from gpt_oss_browser_tool_helper import Browser
from ollama import Client from ollama import Client
def main() -> None: def main() -> None:
client = Client(headers={'Authorization': os.getenv('OLLAMA_API_KEY')}) api_key = os.getenv('OLLAMA_API_KEY')
if api_key:
client = Client(headers={'Authorization': f'Bearer {api_key}'})
else:
client = Client()
browser = Browser(initial_state=None, client=client) browser = Browser(initial_state=None, client=client)
# Minimal tool schemas # Tool schemas
browser_search_schema = {'type': 'function', 'function': {'name': 'browser.search'}} browser_search_schema = {
browser_open_schema = {'type': 'function', 'function': {'name': 'browser.open'}} 'type': 'function',
browser_find_schema = {'type': 'function', 'function': {'name': 'browser.find'}} 'function': {
'name': 'browser.search',
'parameters': {
'type': 'object',
'properties': {
'query': {'type': 'string'},
'topn': {'type': 'integer'},
},
'required': ['query'],
},
},
}
browser_open_schema = {
'type': 'function',
'function': {
'name': 'browser.open',
'parameters': {
'type': 'object',
'properties': {
'id': {'anyOf': [{'type': 'integer'}, {'type': 'string'}]},
'cursor': {'type': 'integer'},
'loc': {'type': 'integer'},
'num_lines': {'type': 'integer'},
},
},
},
}
browser_find_schema = {
'type': 'function',
'function': {
'name': 'browser.find',
'parameters': {
'type': 'object',
'properties': {
'pattern': {'type': 'string'},
'cursor': {'type': 'integer'},
},
'required': ['pattern'],
},
},
}
def browser_search(query: str, topn: int = 10) -> str: def browser_search(query: str, topn: int = 10) -> str:
return browser.search(query=query, topn=topn)['pageText'] return browser.search(query=query, topn=topn)['pageText']
@ -23,7 +69,7 @@ def main() -> None:
def browser_open(id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1) -> str: def browser_open(id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1) -> str:
return browser.open(id=id, cursor=cursor, loc=loc, num_lines=num_lines)['pageText'] return browser.open(id=id, cursor=cursor, loc=loc, num_lines=num_lines)['pageText']
def browser_find(pattern: str, cursor: int = -1) -> str: def browser_find(pattern: str, cursor: int = -1, **_: Any) -> str:
return browser.find(pattern=pattern, cursor=cursor)['pageText'] return browser.find(pattern=pattern, cursor=cursor)['pageText']
available_tools = { available_tools = {
@ -32,7 +78,7 @@ def main() -> None:
'browser.find': browser_find, 'browser.find': browser_find,
} }
messages: List[Dict[str, Any]] = [{'role': 'user', 'content': 'What is Ollama?'}] messages: List[Dict[str, Any]] = [{'role': 'user', 'content': 'When did Ollama announce the new engine?'}]
print('----- Prompt:', messages[0]['content'], '\n') print('----- Prompt:', messages[0]['content'], '\n')
while True: while True:

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Protocol, Tuple from typing import Any, Dict, List, Optional, Protocol, Tuple, Union
from urllib.parse import urlparse from urllib.parse import urlparse
from ollama import Client from ollama import Client
@ -30,7 +30,7 @@ class BrowserStateData:
class WebSearchResult: class WebSearchResult:
title: str title: str
url: str url: str
content: Dict[str, str] # {"fullText": str} content: Dict[str, str]
class SearchClient(Protocol): class SearchClient(Protocol):
@ -94,7 +94,6 @@ class Browser:
self.state = BrowserState(initial_state) self.state = BrowserState(initial_state)
self._client: Optional[Client] = client self._client: Optional[Client] = client
# parity with TS: one setter that accepts both
def set_client(self, client: Client) -> None: def set_client(self, client: Client) -> None:
self._client = client self._client = client
@ -160,10 +159,9 @@ class Browser:
links: Dict[int, str] = {} links: Dict[int, str] = {}
link_id = 0 link_id = 0
# collapse [text]\n(url) -> [text](url)
multiline_pattern = re.compile(r'\[([^\]]+)\]\s*\n\s*\(([^)]+)\)') multiline_pattern = re.compile(r'\[([^\]]+)\]\s*\n\s*\(([^)]+)\)')
text = multiline_pattern.sub(lambda m: f'[{m.group(1)}]({m.group(2)})', text) text = multiline_pattern.sub(lambda m: f'[{m.group(1)}]({m.group(2)})', text)
text = re.sub(r'\s+', ' ', text) # mild cleanup from the above text = re.sub(r'\s+', ' ', text)
link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)') link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
@ -185,7 +183,6 @@ class Browser:
txt = self._join_lines_with_numbers(lines[loc:]) txt = self._join_lines_with_numbers(lines[loc:])
data = self.state.get_data() data = self.state.get_data()
if len(txt) > data.view_tokens: if len(txt) > data.view_tokens:
# approximate char-per-token heuristic (keep identical to TS flow)
max_chars_per_token = 128 max_chars_per_token = 128
upper_bound = min((data.view_tokens + 1) * max_chars_per_token, len(txt)) upper_bound = min((data.view_tokens + 1) * max_chars_per_token, len(txt))
segment = txt[:upper_bound] segment = txt[:upper_bound]
@ -242,10 +239,10 @@ class Browser:
) )
tb = [] tb = []
tb.append('') # L0 blank tb.append('')
tb.append('URL: ') # L1 "URL: " tb.append('URL: ')
tb.append('# Search Results') # L2 tb.append('# Search Results')
tb.append('') # L3 blank tb.append('')
link_idx = 0 link_idx = 0
for query_results in results.get('results', {}).values(): for query_results in results.get('results', {}).values():
@ -276,7 +273,6 @@ class Browser:
fetched_at=datetime.utcnow(), fetched_at=datetime.utcnow(),
) )
# preview block (when no full text)
link_fmt = f'{link_idx}{result.title}\n' link_fmt = f'{link_idx}{result.title}\n'
preview = link_fmt + f'URL: {result.url}\n' preview = link_fmt + f'URL: {result.url}\n'
full_text = result.content.get('fullText', '') if result.content else '' full_text = result.content.get('fullText', '') if result.content else ''
@ -296,7 +292,7 @@ class Browser:
page.lines = self._wrap_lines(page.text, 80) page.lines = self._wrap_lines(page.text, 80)
return page return page
def _build_page_from_crawl(self, requested_url: str, crawl_response: Dict[str, Any]) -> Page: def _build_page_from_fetch(self, requested_url: str, fetch_response: Dict[str, Any]) -> Page:
page = Page( page = Page(
url=requested_url, url=requested_url,
title=requested_url, title=requested_url,
@ -306,7 +302,7 @@ class Browser:
fetched_at=datetime.utcnow(), fetched_at=datetime.utcnow(),
) )
for url, url_results in crawl_response.get('results', {}).items(): for url, url_results in fetch_response.get('results', {}).items():
if url_results: if url_results:
r0 = url_results[0] r0 = url_results[0]
if r0.get('content'): if r0.get('content'):
@ -372,22 +368,20 @@ class Browser:
if not self._client: if not self._client:
raise RuntimeError('Client not provided') raise RuntimeError('Client not provided')
resp = self._client.web_search([query], max_results=topn) resp = self._client.web_search(query, max_results=topn)
# Normalize to dict shape used by page builders
normalized: Dict[str, Any] = {'results': {}} normalized: Dict[str, Any] = {'results': {}}
for q, items in resp.results.items(): rows: List[Dict[str, str]] = []
rows: List[Dict[str, str]] = [] for item in resp.results:
for item in items: content = item.content or ''
content = item.content or '' rows.append(
rows.append( {
{ 'title': item.title,
'title': item.title, 'url': item.url,
'url': item.url, 'content': content,
'content': content, }
} )
) normalized['results'][query] = rows
normalized['results'][q] = rows
search_page = self._build_search_results_page_collection(query, normalized) search_page = self._build_search_results_page_collection(query, normalized)
self._save_page(search_page) self._save_page(search_page)
@ -430,7 +424,6 @@ class Browser:
if state.page_stack: if state.page_stack:
page = self._page_from_stack(state.page_stack[-1]) page = self._page_from_stack(state.page_stack[-1])
# Open by URL (string id)
if isinstance(id, str): if isinstance(id, str):
url = id url = id
if url in state.url_to_page: if url in state.url_to_page:
@ -439,35 +432,30 @@ class Browser:
page_text = self._display_page(state.url_to_page[url], cursor, loc, num_lines) page_text = self._display_page(state.url_to_page[url], cursor, loc, num_lines)
return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)} return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}
crawl_response = self._client.web_crawl([url]) fetch_response = self._client.web_fetch(url)
# Normalize to dict shape used by page builders normalized: Dict[str, Any] = {
normalized: Dict[str, Any] = {'results': {}} 'results': {
for u, items in crawl_response.results.items(): url: [
rows: List[Dict[str, str]] = []
for item in items:
content = item.content or ''
rows.append(
{ {
'title': item.title, 'title': fetch_response.title or url,
'url': item.url, 'url': url,
'content': content, 'content': fetch_response.content or '',
} }
) ]
normalized['results'][u] = rows }
new_page = self._build_page_from_crawl(url, normalized) }
new_page = self._build_page_from_fetch(url, normalized)
self._save_page(new_page) self._save_page(new_page)
cursor = len(self.get_state().page_stack) - 1 cursor = len(self.get_state().page_stack) - 1
page_text = self._display_page(new_page, cursor, loc, num_lines) page_text = self._display_page(new_page, cursor, loc, num_lines)
return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)} return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}
# Open by link id (int) from current page
if isinstance(id, int): if isinstance(id, int):
if not page: if not page:
raise RuntimeError('No current page to resolve link from') raise RuntimeError('No current page to resolve link from')
link_url = page.links.get(id) link_url = page.links.get(id)
if not link_url: if not link_url:
# build an error page like TS
err = Page( err = Page(
url=f'invalid_link_{id}', url=f'invalid_link_{id}',
title=f'No link with id {id} on `{page.title}`', title=f'No link with id {id} on `{page.title}`',
@ -497,28 +485,25 @@ class Browser:
new_page = state.url_to_page.get(link_url) new_page = state.url_to_page.get(link_url)
if not new_page: if not new_page:
crawl_response = self._client.web_crawl([link_url]) fetch_response = self._client.web_fetch(link_url)
normalized: Dict[str, Any] = {'results': {}} normalized: Dict[str, Any] = {
for u, items in crawl_response.results.items(): 'results': {
rows: List[Dict[str, str]] = [] link_url: [
for item in items:
content = item.content or ''
rows.append(
{ {
'title': item.title, 'title': fetch_response.title or link_url,
'url': item.url, 'url': link_url,
'content': content, 'content': fetch_response.content or '',
} }
) ]
normalized['results'][u] = rows }
new_page = self._build_page_from_crawl(link_url, normalized) }
new_page = self._build_page_from_fetch(link_url, normalized)
self._save_page(new_page) self._save_page(new_page)
cursor = len(self.get_state().page_stack) - 1 cursor = len(self.get_state().page_stack) - 1
page_text = self._display_page(new_page, cursor, loc, num_lines) page_text = self._display_page(new_page, cursor, loc, num_lines)
return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)} return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}
# No id: just re-display the current page and advance stack
if not page: if not page:
raise RuntimeError('No current page to display') raise RuntimeError('No current page to display')
@ -547,3 +532,5 @@ class Browser:
page_text = self._display_page(find_page, new_cursor, 0, -1) page_text = self._display_page(find_page, new_cursor, 0, -1)
return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)} return {'state': self.get_state(), 'pageText': cap_tool_content(page_text)}