Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions examples/websearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "rich",
# ]
# ///
import os
from typing import Union

from rich import print

from ollama import Client, WebCrawlResponse, WebSearchResponse


def format_tool_results(results: Union[WebSearchResponse, WebCrawlResponse]):
if isinstance(results, WebSearchResponse):
if not results.success:
error_msg = ', '.join(results.errors) if results.errors else 'Unknown error'
return f'Web search failed: {error_msg}'

output = []
for query, search_results in results.results.items():
output.append(f'Search results for "{query}":')
for i, result in enumerate(search_results, 1):
output.append(f'{i}. {result.title}')
output.append(f' URL: {result.url}')
output.append(f' Content: {result.content}')
output.append('')

return '\n'.join(output).rstrip()

elif isinstance(results, WebCrawlResponse):
if not results.success:
error_msg = ', '.join(results.errors) if results.errors else 'Unknown error'
return f'Web crawl failed: {error_msg}'

output = []
for url, crawl_results in results.results.items():
output.append(f'Crawl results for "{url}":')
for i, result in enumerate(crawl_results, 1):
output.append(f'{i}. {result.title}')
output.append(f' URL: {result.url}')
output.append(f' Content: {result.content}')
if result.links:
output.append(f' Links: {", ".join(result.links)}')
output.append('')

return '\n'.join(output).rstrip()


client = Client(headers={'Authorization': (os.getenv('OLLAMA_API_KEY'))})
available_tools = {'websearch': client.websearch, 'webcrawl': client.webcrawl}

query = "ollama's new engine"
print('Query: ', query)

messages = [{'role': 'user', 'content': query}]
while True:
response = client.chat(model='qwen3', messages=messages, tools=[client.websearch, client.webcrawl], think=True)
if response.message.thinking:
print('Thinking: ')
print(response.message.thinking + '\n\n')
if response.message.content:
print('Content: ')
print(response.message.content + '\n')

messages.append(response.message)

if response.message.tool_calls:
for tool_call in response.message.tool_calls:
function_to_call = available_tools.get(tool_call.function.name)
if function_to_call:
result: WebSearchResponse | WebCrawlResponse = function_to_call(**tool_call.function.arguments)
print('Result from tool call name: ', tool_call.function.name, 'with arguments: ', tool_call.function.arguments)
print('Result: ', format_tool_results(result)[:200])

# caps the result at ~2000 tokens
messages.append({'role': 'tool', 'content': format_tool_results(result)[: 2000 * 4], 'tool_name': tool_call.function.name})
else:
print(f'Tool {tool_call.function.name} not found')
messages.append({'role': 'tool', 'content': f'Tool {tool_call.function.name} not found', 'tool_name': tool_call.function.name})
else:
# no more tool calls, we can stop the loop
break
6 changes: 6 additions & 0 deletions ollama/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
ShowResponse,
StatusResponse,
Tool,
WebCrawlResponse,
WebSearchResponse,
)

__all__ = [
Expand All @@ -35,6 +37,8 @@
'ShowResponse',
'StatusResponse',
'Tool',
'WebCrawlResponse',
'WebSearchResponse',
]

_client = Client()
Expand All @@ -51,3 +55,5 @@
copy = _client.copy
show = _client.show
ps = _client.ps
websearch = _client.websearch
webcrawl = _client.webcrawl
46 changes: 46 additions & 0 deletions ollama/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
ShowResponse,
StatusResponse,
Tool,
WebCrawlRequest,
WebCrawlResponse,
WebSearchRequest,
WebSearchResponse,
)

T = TypeVar('T')
Expand Down Expand Up @@ -102,6 +106,8 @@ def __init__(
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': f'ollama-python/{__version__} ({platform.machine()} {platform.system().lower()}) Python/{platform.python_version()}',
# TODO: this is to make the client feel good
# 'Authorization': f'Bearer {(headers or {}).get("Authorization") or os.getenv("OLLAMA_API_KEY")}' if (headers or {}).get("Authorization") or os.getenv("OLLAMA_API_KEY") else None,
}.items()
},
**kwargs,
Expand Down Expand Up @@ -622,6 +628,46 @@ def ps(self) -> ProcessResponse:
'/api/ps',
)

def websearch(self, queries: Sequence[str], max_results: int = 3) -> WebSearchResponse:
"""
Performs a web search

Args:
queries: The queries to search for
max_results: The maximum number of results to return.

Returns:
WebSearchResponse with the search results
"""
return self._request(
WebSearchResponse,
'POST',
'https://ollama.com/api/web_search',
json=WebSearchRequest(
queries=queries,
max_results=max_results,
).model_dump(exclude_none=True),
)

def webcrawl(self, urls: Sequence[str]) -> WebCrawlResponse:
"""
Gets the content of web pages for the provided URLs.

Args:
urls: The URLs to crawl

Returns:
WebCrawlResponse with the crawl results
"""
return self._request(
WebCrawlResponse,
'POST',
'https://ollama.com/api/web_crawl',
json=WebCrawlRequest(
urls=urls,
).model_dump(exclude_none=True),
)


class AsyncClient(BaseClient):
def __init__(self, host: Optional[str] = None, **kwargs) -> None:
Expand Down
45 changes: 45 additions & 0 deletions ollama/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,51 @@ class Model(SubscriptableBaseModel):
models: Sequence[Model]


class WebSearchRequest(SubscriptableBaseModel):
queries: Sequence[str]
max_results: Optional[int] = None


class SearchResult(SubscriptableBaseModel):
title: str
url: str
content: str


class CrawlResult(SubscriptableBaseModel):
title: str
url: str
content: str
links: Optional[Sequence[str]] = None


class SearchResultContent(SubscriptableBaseModel):
snippet: str
full_text: str


class WebSearchResponse(SubscriptableBaseModel):
results: Mapping[str, Sequence[SearchResult]]
success: bool
errors: Optional[Sequence[str]] = None


class WebCrawlRequest(SubscriptableBaseModel):
urls: Sequence[str]


class CrawlResultContent(SubscriptableBaseModel):
# provides the first 200 characters of the full text
snippet: str
full_text: str


class WebCrawlResponse(SubscriptableBaseModel):
results: Mapping[str, Sequence[CrawlResult]]
success: bool
errors: Optional[Sequence[str]] = None


class RequestError(Exception):
"""
Common class for request errors.
Expand Down
Loading