mirror of
https://github.com/ollama/ollama-python.git
synced 2026-06-17 21:53:23 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c919192116 |
+2
-1
@@ -36,6 +36,8 @@ See [ollama/docs/api.md](https://github.com/ollama/ollama/blob/main/docs/api.md)
|
||||
|
||||
- [gpt-oss-tools.py](gpt-oss-tools.py)
|
||||
- [gpt-oss-tools-stream.py](gpt-oss-tools-stream.py)
|
||||
- [gpt-oss-tools-browser.py](gpt-oss-tools-browser.py) - Using browser research tools with gpt-oss
|
||||
- [gpt-oss-tools-browser-stream.py](gpt-oss-tools-browser-stream.py) - Using browser research tools with gpt-oss, with streaming enabled
|
||||
|
||||
### Web search
|
||||
|
||||
@@ -46,7 +48,6 @@ export OLLAMA_API_KEY="your_api_key_here"
|
||||
```
|
||||
|
||||
- [web-search.py](web-search.py)
|
||||
- [web-search-gpt-oss.py](web-search-gpt-oss.py) - Using browser research tools with gpt-oss
|
||||
|
||||
#### MCP server
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "gpt-oss",
|
||||
# "ollama",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Iterator, Optional
|
||||
|
||||
from gpt_oss.tools.simple_browser import ExaBackend, SimpleBrowserTool
|
||||
from openai_harmony import Author, Role, TextContent
|
||||
from openai_harmony import Message as HarmonyMessage
|
||||
from rich import print
|
||||
|
||||
from ollama import Client
|
||||
from ollama._types import ChatResponse
|
||||
|
||||
_backend = ExaBackend(source='web')
|
||||
_browser_tool = SimpleBrowserTool(backend=_backend)
|
||||
|
||||
|
||||
def heading(text):
|
||||
print(text)
|
||||
print('=' * (len(text) + 3))
|
||||
|
||||
|
||||
async def _browser_search_async(query: str, topn: int = 10, source: str | None = None) -> str:
|
||||
# map Ollama message to Harmony format
|
||||
harmony_message = HarmonyMessage(
|
||||
author=Author(role=Role.USER),
|
||||
content=[TextContent(text=json.dumps({'query': query, 'topn': topn}))],
|
||||
recipient='browser.search',
|
||||
)
|
||||
|
||||
result_text: str = ''
|
||||
async for response in _browser_tool._process(harmony_message):
|
||||
if response.content:
|
||||
for content in response.content:
|
||||
if isinstance(content, TextContent):
|
||||
result_text += content.text
|
||||
return result_text or f'No results for query: {query}'
|
||||
|
||||
|
||||
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:
|
||||
payload = {'id': id, 'cursor': cursor, 'loc': loc, 'num_lines': num_lines, 'view_source': view_source, 'source': source}
|
||||
|
||||
harmony_message = HarmonyMessage(
|
||||
author=Author(role=Role.USER),
|
||||
content=[TextContent(text=json.dumps(payload))],
|
||||
recipient='browser.open',
|
||||
)
|
||||
|
||||
result_text: str = ''
|
||||
async for response in _browser_tool._process(harmony_message):
|
||||
if response.content:
|
||||
for content in response.content:
|
||||
if isinstance(content, TextContent):
|
||||
result_text += content.text
|
||||
return result_text or f'Could not open: {id}'
|
||||
|
||||
|
||||
async def _browser_find_async(pattern: str, cursor: int = -1) -> str:
|
||||
payload = {'pattern': pattern, 'cursor': cursor}
|
||||
|
||||
harmony_message = HarmonyMessage(
|
||||
author=Author(role=Role.USER),
|
||||
content=[TextContent(text=json.dumps(payload))],
|
||||
recipient='browser.find',
|
||||
)
|
||||
|
||||
result_text: str = ''
|
||||
async for response in _browser_tool._process(harmony_message):
|
||||
if response.content:
|
||||
for content in response.content:
|
||||
if isinstance(content, TextContent):
|
||||
result_text += content.text
|
||||
return result_text or f'Pattern not found: {pattern}'
|
||||
|
||||
|
||||
def browser_search(query: str, topn: int = 10, source: Optional[str] = None) -> str:
|
||||
return asyncio.run(_browser_search_async(query=query, topn=topn, source=source))
|
||||
|
||||
|
||||
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:
|
||||
return asyncio.run(_browser_open_async(id=id, cursor=cursor, loc=loc, num_lines=num_lines, view_source=view_source, source=source))
|
||||
|
||||
|
||||
def browser_find(pattern: str, cursor: int = -1) -> str:
|
||||
return asyncio.run(_browser_find_async(pattern=pattern, cursor=cursor))
|
||||
|
||||
|
||||
# Schema definitions for each browser tool
|
||||
browser_search_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.search',
|
||||
},
|
||||
}
|
||||
|
||||
browser_open_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.open',
|
||||
},
|
||||
}
|
||||
|
||||
browser_find_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.find',
|
||||
},
|
||||
}
|
||||
|
||||
available_tools = {
|
||||
'browser.search': browser_search,
|
||||
'browser.open': browser_open,
|
||||
'browser.find': browser_find,
|
||||
}
|
||||
|
||||
|
||||
model = 'gpt-oss:20b'
|
||||
print('Model: ', model, '\n')
|
||||
|
||||
prompt = 'What is Ollama?'
|
||||
print('You: ', prompt, '\n')
|
||||
messages = [{'role': 'user', 'content': prompt}]
|
||||
|
||||
client = Client()
|
||||
|
||||
# gpt-oss can call tools while "thinking"
|
||||
# a loop is needed to call the tools and get the results
|
||||
final = True
|
||||
while True:
|
||||
response_stream: Iterator[ChatResponse] = client.chat(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=[browser_search_schema, browser_open_schema, browser_find_schema],
|
||||
options={'num_ctx': 8192}, # 8192 is the recommended lower limit for the context window
|
||||
stream=True,
|
||||
)
|
||||
|
||||
tool_calls = []
|
||||
thinking = ''
|
||||
content = ''
|
||||
|
||||
for chunk in response_stream:
|
||||
if chunk.message.tool_calls:
|
||||
tool_calls.extend(chunk.message.tool_calls)
|
||||
|
||||
if chunk.message.content:
|
||||
if not (chunk.message.thinking or chunk.message.thinking == '') and final:
|
||||
heading('\n\nFinal result: ')
|
||||
final = False
|
||||
print(chunk.message.content, end='', flush=True)
|
||||
|
||||
if chunk.message.thinking:
|
||||
thinking += chunk.message.thinking
|
||||
print(chunk.message.thinking, end='', flush=True)
|
||||
|
||||
if thinking != '':
|
||||
messages.append({'role': 'assistant', 'content': thinking, 'tool_calls': tool_calls})
|
||||
|
||||
print()
|
||||
|
||||
if tool_calls:
|
||||
for tool_call in tool_calls:
|
||||
tool_name = tool_call.function.name
|
||||
args = tool_call.function.arguments or {}
|
||||
function_to_call = available_tools.get(tool_name)
|
||||
|
||||
if function_to_call:
|
||||
heading(f'\nCalling tool: {tool_name}')
|
||||
if args:
|
||||
print(f'Arguments: {args}')
|
||||
|
||||
try:
|
||||
result = function_to_call(**args)
|
||||
print(f'Tool result: {result[:200]}')
|
||||
if len(result) > 200:
|
||||
heading('... [truncated]')
|
||||
print()
|
||||
|
||||
result_message = {'role': 'tool', 'content': result, 'tool_name': tool_name}
|
||||
messages.append(result_message)
|
||||
|
||||
except Exception as e:
|
||||
err = f'Error from {tool_name}: {e}'
|
||||
print(err)
|
||||
messages.append({'role': 'tool', 'content': err, 'tool_name': tool_name})
|
||||
else:
|
||||
print(f'Tool {tool_name} not found')
|
||||
else:
|
||||
# no more tool calls, we can stop the loop
|
||||
break
|
||||
@@ -0,0 +1,175 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "gpt-oss",
|
||||
# "ollama",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from gpt_oss.tools.simple_browser import ExaBackend, SimpleBrowserTool
|
||||
from openai_harmony import Author, Role, TextContent
|
||||
from openai_harmony import Message as HarmonyMessage
|
||||
|
||||
from ollama import Client
|
||||
|
||||
_backend = ExaBackend(source='web')
|
||||
_browser_tool = SimpleBrowserTool(backend=_backend)
|
||||
|
||||
|
||||
def heading(text):
|
||||
print(text)
|
||||
print('=' * (len(text) + 3))
|
||||
|
||||
|
||||
async def _browser_search_async(query: str, topn: int = 10, source: str | None = None) -> str:
|
||||
# map Ollama message to Harmony format
|
||||
harmony_message = HarmonyMessage(
|
||||
author=Author(role=Role.USER),
|
||||
content=[TextContent(text=json.dumps({'query': query, 'topn': topn}))],
|
||||
recipient='browser.search',
|
||||
)
|
||||
|
||||
result_text: str = ''
|
||||
async for response in _browser_tool._process(harmony_message):
|
||||
if response.content:
|
||||
for content in response.content:
|
||||
if isinstance(content, TextContent):
|
||||
result_text += content.text
|
||||
return result_text or f'No results for query: {query}'
|
||||
|
||||
|
||||
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:
|
||||
payload = {'id': id, 'cursor': cursor, 'loc': loc, 'num_lines': num_lines, 'view_source': view_source, 'source': source}
|
||||
|
||||
harmony_message = HarmonyMessage(
|
||||
author=Author(role=Role.USER),
|
||||
content=[TextContent(text=json.dumps(payload))],
|
||||
recipient='browser.open',
|
||||
)
|
||||
|
||||
result_text: str = ''
|
||||
async for response in _browser_tool._process(harmony_message):
|
||||
if response.content:
|
||||
for content in response.content:
|
||||
if isinstance(content, TextContent):
|
||||
result_text += content.text
|
||||
return result_text or f'Could not open: {id}'
|
||||
|
||||
|
||||
async def _browser_find_async(pattern: str, cursor: int = -1) -> str:
|
||||
payload = {'pattern': pattern, 'cursor': cursor}
|
||||
|
||||
harmony_message = HarmonyMessage(
|
||||
author=Author(role=Role.USER),
|
||||
content=[TextContent(text=json.dumps(payload))],
|
||||
recipient='browser.find',
|
||||
)
|
||||
|
||||
result_text: str = ''
|
||||
async for response in _browser_tool._process(harmony_message):
|
||||
if response.content:
|
||||
for content in response.content:
|
||||
if isinstance(content, TextContent):
|
||||
result_text += content.text
|
||||
return result_text or f'Pattern not found: {pattern}'
|
||||
|
||||
|
||||
def browser_search(query: str, topn: int = 10, source: Optional[str] = None) -> str:
|
||||
return asyncio.run(_browser_search_async(query=query, topn=topn, source=source))
|
||||
|
||||
|
||||
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:
|
||||
return asyncio.run(_browser_open_async(id=id, cursor=cursor, loc=loc, num_lines=num_lines, view_source=view_source, source=source))
|
||||
|
||||
|
||||
def browser_find(pattern: str, cursor: int = -1) -> str:
|
||||
return asyncio.run(_browser_find_async(pattern=pattern, cursor=cursor))
|
||||
|
||||
|
||||
# Schema definitions for each browser tool
|
||||
browser_search_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.search',
|
||||
},
|
||||
}
|
||||
|
||||
browser_open_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.open',
|
||||
},
|
||||
}
|
||||
|
||||
browser_find_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.find',
|
||||
},
|
||||
}
|
||||
|
||||
available_tools = {
|
||||
'browser.search': browser_search,
|
||||
'browser.open': browser_open,
|
||||
'browser.find': browser_find,
|
||||
}
|
||||
|
||||
|
||||
model = 'gpt-oss:20b'
|
||||
print('Model: ', model, '\n')
|
||||
|
||||
prompt = 'What is Ollama?'
|
||||
print('You: ', prompt, '\n')
|
||||
messages = [{'role': 'user', 'content': prompt}]
|
||||
|
||||
client = Client()
|
||||
while True:
|
||||
response = client.chat(
|
||||
model=model,
|
||||
messages=messages,
|
||||
tools=[browser_search_schema, browser_open_schema, browser_find_schema],
|
||||
options={'num_ctx': 8192}, # 8192 is the recommended lower limit for the context window
|
||||
)
|
||||
|
||||
if hasattr(response.message, 'thinking') and response.message.thinking:
|
||||
heading('Thinking')
|
||||
print(response.message.thinking.strip() + '\n')
|
||||
|
||||
if hasattr(response.message, 'content') and response.message.content:
|
||||
heading('Assistant')
|
||||
print(response.message.content.strip() + '\n')
|
||||
|
||||
# add message to chat history
|
||||
messages.append(response.message)
|
||||
|
||||
if response.message.tool_calls:
|
||||
for tool_call in response.message.tool_calls:
|
||||
tool_name = tool_call.function.name
|
||||
args = tool_call.function.arguments or {}
|
||||
function_to_call = available_tools.get(tool_name)
|
||||
if not function_to_call:
|
||||
print(f'Unknown tool: {tool_name}')
|
||||
continue
|
||||
|
||||
try:
|
||||
result = function_to_call(**args)
|
||||
heading(f'Tool: {tool_name}')
|
||||
if args:
|
||||
print(f'Arguments: {args}')
|
||||
print(result[:200])
|
||||
if len(result) > 200:
|
||||
print('... [truncated]')
|
||||
print()
|
||||
messages.append({'role': 'tool', 'content': result, 'tool_name': tool_name})
|
||||
except Exception as e:
|
||||
err = f'Error from {tool_name}: {e}'
|
||||
print(err)
|
||||
messages.append({'role': 'tool', 'content': err, 'tool_name': tool_name})
|
||||
else:
|
||||
# break on no more tool calls
|
||||
break
|
||||
@@ -41,13 +41,9 @@ class CrawlClient(Protocol):
|
||||
def crawl(self, urls: List[str]): ...
|
||||
|
||||
|
||||
# ---- Constants ---------------------------------------------------------------
|
||||
|
||||
DEFAULT_VIEW_TOKENS = 1024
|
||||
CAPPED_TOOL_CONTENT_LEN = 8000
|
||||
|
||||
# ---- Helpers ----------------------------------------------------------------
|
||||
|
||||
|
||||
def cap_tool_content(text: str) -> str:
|
||||
if not text:
|
||||
@@ -68,9 +64,6 @@ def _safe_domain(u: str) -> str:
|
||||
return u
|
||||
|
||||
|
||||
# ---- BrowserState ------------------------------------------------------------
|
||||
|
||||
|
||||
class BrowserState:
|
||||
def __init__(self, initial_state: Optional[BrowserStateData] = None):
|
||||
self._data = initial_state or BrowserStateData(view_tokens=DEFAULT_VIEW_TOKENS)
|
||||
@@ -82,9 +75,6 @@ class BrowserState:
|
||||
self._data = data
|
||||
|
||||
|
||||
# ---- Browser ----------------------------------------------------------------
|
||||
|
||||
|
||||
class Browser:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -203,8 +193,6 @@ class Browser:
|
||||
|
||||
return header + '\n'.join(body_lines)
|
||||
|
||||
# ---- page builders ----
|
||||
|
||||
def _build_search_results_page_collection(self, query: str, results: Dict[str, Any]) -> Page:
|
||||
page = Page(
|
||||
url=f'search_results_{query}',
|
||||
@@ -338,8 +326,6 @@ class Browser:
|
||||
find_page.lines = self._wrap_lines(find_page.text, 80)
|
||||
return find_page
|
||||
|
||||
# ---- public API: search / open / find ------------------------------------
|
||||
|
||||
def search(self, *, query: str, topn: int = 5) -> Dict[str, Any]:
|
||||
if not self._client:
|
||||
raise RuntimeError('Client not provided')
|
||||
@@ -1,99 +0,0 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "ollama",
|
||||
# ]
|
||||
# ///
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from web_search_gpt_oss_helper import Browser
|
||||
|
||||
from ollama import Client
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = Client()
|
||||
browser = Browser(initial_state=None, client=client)
|
||||
|
||||
def browser_search(query: str, topn: int = 10) -> str:
|
||||
return browser.search(query=query, topn=topn)['pageText']
|
||||
|
||||
def browser_open(id: int | str | None = None, 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']
|
||||
|
||||
def browser_find(pattern: str, cursor: int = -1, **_: Any) -> str:
|
||||
return browser.find(pattern=pattern, cursor=cursor)['pageText']
|
||||
|
||||
browser_search_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.search',
|
||||
},
|
||||
}
|
||||
|
||||
browser_open_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.open',
|
||||
},
|
||||
}
|
||||
|
||||
browser_find_schema = {
|
||||
'type': 'function',
|
||||
'function': {
|
||||
'name': 'browser.find',
|
||||
},
|
||||
}
|
||||
|
||||
available_tools = {
|
||||
'browser.search': browser_search,
|
||||
'browser.open': browser_open,
|
||||
'browser.find': browser_find,
|
||||
}
|
||||
|
||||
query = "what is ollama's new engine"
|
||||
print('Prompt:', query, '\n')
|
||||
|
||||
messages: List[Dict[str, Any]] = [{'role': 'user', 'content': query}]
|
||||
|
||||
while True:
|
||||
resp = client.chat(
|
||||
model='gpt-oss:120b-cloud',
|
||||
messages=messages,
|
||||
tools=[browser_search_schema, browser_open_schema, browser_find_schema],
|
||||
think=True,
|
||||
)
|
||||
|
||||
if resp.message.thinking:
|
||||
print('Thinking:\n========\n')
|
||||
print(resp.message.thinking + '\n')
|
||||
|
||||
if resp.message.content:
|
||||
print('Response:\n========\n')
|
||||
print(resp.message.content + '\n')
|
||||
|
||||
messages.append(resp.message)
|
||||
|
||||
if not resp.message.tool_calls:
|
||||
break
|
||||
|
||||
for tc in resp.message.tool_calls:
|
||||
tool_name = tc.function.name
|
||||
args = tc.function.arguments or {}
|
||||
print(f'Tool name: {tool_name}, args: {args}')
|
||||
fn = available_tools.get(tool_name)
|
||||
if not fn:
|
||||
messages.append({'role': 'tool', 'content': f'Tool {tool_name} not found', 'tool_name': tool_name})
|
||||
continue
|
||||
|
||||
try:
|
||||
result_text = fn(**args)
|
||||
print('Result: ', result_text[:200] + '...')
|
||||
except Exception as e:
|
||||
result_text = f'Error from {tool_name}: {e}'
|
||||
|
||||
messages.append({'role': 'tool', 'content': result_text, 'tool_name': tool_name})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -49,7 +49,7 @@ print('Query: ', query)
|
||||
|
||||
messages = [{'role': 'user', 'content': query}]
|
||||
while True:
|
||||
response = chat(model='deepseek-v3.1:671b-cloud', messages=messages, tools=[web_search, web_fetch], think=True)
|
||||
response = chat(model='qwen3', messages=messages, tools=[web_search, web_fetch], think=True)
|
||||
if response.message.thinking:
|
||||
print('Thinking: ')
|
||||
print(response.message.thinking + '\n\n')
|
||||
Reference in New Issue
Block a user