Skip to content

Commit c4f0f74

Browse files
committed
examples: add gpt-oss browser example
1 parent 53ff3cd commit c4f0f74

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ See [ollama/docs/api.md](https://github.com/ollama/ollama/blob/main/docs/api.md)
3030
#### gpt-oss
3131
- [gpt-oss-tools.py](gpt-oss-tools.py) - Using tools with gpt-oss
3232
- [gpt-oss-tools-stream.py](gpt-oss-tools-stream.py) - Using tools with gpt-oss, with streaming enabled
33+
- [gpt-oss-tools-browser.py](gpt-oss-tools-browser.py) - Using browser tools with gpt-oss
3334

3435

3536
### Multimodal with Images - Chat with a multimodal (image chat) model

examples/gpt-oss-tools-browser.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# /// script
2+
# requires-python = ">=3.11"
3+
# dependencies = [
4+
# "gpt-oss",
5+
# "ollama",
6+
# ]
7+
# ///
8+
9+
import asyncio
10+
import json
11+
from typing import Optional
12+
13+
from gpt_oss.tools.simple_browser import ExaBackend, SimpleBrowserTool
14+
from openai_harmony import Author, Role, TextContent
15+
from openai_harmony import Message as HarmonyMessage
16+
17+
from ollama import Client
18+
19+
_backend = ExaBackend(source='web')
20+
_browser_tool = SimpleBrowserTool(backend=_backend)
21+
22+
23+
def heading(text):
24+
print(text)
25+
print('=' * (len(text) +3))
26+
27+
28+
async def _browser_search_async(query: str, topn: int = 10, source: str | None = None) -> str:
29+
# map Ollama message to Harmony format
30+
harmony_message = HarmonyMessage(
31+
author=Author(role=Role.USER),
32+
content=[TextContent(text=json.dumps({'query': query, 'topn': topn}))],
33+
recipient='browser.search',
34+
)
35+
36+
result_text: str = ''
37+
async for response in _browser_tool._process(harmony_message):
38+
if response.content:
39+
for content in response.content:
40+
if isinstance(content, TextContent):
41+
result_text += content.text
42+
return result_text or f'No results for query: {query}'
43+
44+
45+
async def _browser_open_async(id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1, *, view_source: bool = False, source: str | None = None) -> str:
46+
payload = {
47+
'id': id,
48+
'cursor': cursor,
49+
'loc': loc,
50+
'num_lines': num_lines,
51+
'view_source': view_source,
52+
'source': source
53+
}
54+
55+
harmony_message = HarmonyMessage(
56+
author=Author(role=Role.USER),
57+
content=[TextContent(text=json.dumps(payload))],
58+
recipient='browser.open',
59+
)
60+
61+
result_text: str = ''
62+
async for response in _browser_tool._process(harmony_message):
63+
if response.content:
64+
for content in response.content:
65+
if isinstance(content, TextContent):
66+
result_text += content.text
67+
return result_text or f'Could not open: {id}'
68+
69+
70+
async def _browser_find_async(pattern: str, cursor: int = -1) -> str:
71+
payload = {'pattern': pattern, 'cursor': cursor}
72+
73+
harmony_message = HarmonyMessage(
74+
author=Author(role=Role.USER),
75+
content=[TextContent(text=json.dumps(payload))],
76+
recipient='browser.find',
77+
)
78+
79+
result_text: str = ''
80+
async for response in _browser_tool._process(harmony_message):
81+
if response.content:
82+
for content in response.content:
83+
if isinstance(content, TextContent):
84+
result_text += content.text
85+
return result_text or f'Pattern not found: {pattern}'
86+
87+
88+
def browser_search(query: str, topn: int = 10, source: Optional[str] = None) -> str:
89+
return asyncio.run(_browser_search_async(query=query, topn=topn, source=source))
90+
91+
92+
def browser_open(id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1, *, view_source: bool = False, source: Optional[str] = None) -> str:
93+
return asyncio.run(_browser_open_async(id=id, cursor=cursor, loc=loc, num_lines=num_lines, view_source=view_source, source=source))
94+
95+
96+
def browser_find(pattern: str, cursor: int = -1) -> str:
97+
return asyncio.run(_browser_find_async(pattern=pattern, cursor=cursor))
98+
99+
100+
model = 'gpt-oss:20b'
101+
102+
# Schema definitions for each browser tool
103+
browser_search_schema = {
104+
'type': 'function',
105+
'function': {
106+
'name': 'browser.search',
107+
},
108+
}
109+
110+
browser_open_schema = {
111+
'type': 'function',
112+
'function': {
113+
'name': 'browser.open',
114+
},
115+
}
116+
117+
browser_find_schema = {
118+
'type': 'function',
119+
'function': {
120+
'name': 'browser.find',
121+
},
122+
}
123+
124+
available_tools = {
125+
'browser.search': browser_search,
126+
'browser.open': browser_open,
127+
'browser.find': browser_find,
128+
}
129+
130+
131+
prompt = 'What is Ollama?'
132+
print('You: ', prompt, '\n')
133+
messages = [{'role': 'user', 'content': prompt}]
134+
135+
client = Client()
136+
while True:
137+
response = client.chat(
138+
model=model,
139+
messages=messages,
140+
tools=[browser_search_schema, browser_open_schema, browser_find_schema],
141+
options={'num_ctx': 8192} # 8192 is the recommended lower limit for the context window
142+
)
143+
144+
if hasattr(response.message, 'thinking') and response.message.thinking:
145+
heading('Thinking')
146+
print(response.message.thinking.strip() + '\n')
147+
148+
if hasattr(response.message, 'content') and response.message.content:
149+
heading('Assistant')
150+
print(response.message.content.strip() + '\n')
151+
152+
# add message to chat history
153+
messages.append(response.message)
154+
155+
if response.message.tool_calls:
156+
for tool_call in response.message.tool_calls:
157+
tool_name = tool_call.function.name
158+
args = tool_call.function.arguments or {}
159+
function_to_call = available_tools.get(tool_name)
160+
if not function_to_call:
161+
print(f'Unknown tool: {tool_name}')
162+
continue
163+
164+
try:
165+
result = function_to_call(**args)
166+
heading(f'Tool: {tool_name}')
167+
if args:
168+
print(f'Arguments: {args}')
169+
print(result[:200])
170+
if len(result) > 200:
171+
print('... [truncated]')
172+
print()
173+
messages.append({'role': 'tool', 'content': result, 'tool_name': tool_name})
174+
except Exception as e:
175+
err = f'Error from {tool_name}: {e}'
176+
print(err)
177+
messages.append({'role': 'tool', 'content': err, 'tool_name': tool_name})
178+
else:
179+
# break on no more tool calls
180+
break

0 commit comments

Comments
 (0)