Skip to content

Commit 16f344f

Browse files
npardalParthSareen
andauthored
client/types: update web search and fetch API (#584)
--------- Co-authored-by: ParthSareen <[email protected]>
1 parent d0f71bc commit 16f344f

File tree

5 files changed

+95
-107
lines changed

5 files changed

+95
-107
lines changed

examples/web-search-crawl.py

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,47 @@
99

1010
from rich import print
1111

12-
from ollama import WebCrawlResponse, WebSearchResponse, chat, web_crawl, web_search
12+
from ollama import WebFetchResponse, WebSearchResponse, chat, web_fetch, web_search
1313

1414

15-
def format_tool_results(results: Union[WebSearchResponse, WebCrawlResponse]):
15+
def format_tool_results(
16+
results: Union[WebSearchResponse, WebFetchResponse],
17+
user_search: str,
18+
):
19+
output = []
1620
if isinstance(results, WebSearchResponse):
17-
if not results.success:
18-
error_msg = ', '.join(results.errors) if results.errors else 'Unknown error'
19-
return f'Web search failed: {error_msg}'
20-
21-
output = []
22-
for query, search_results in results.results.items():
23-
output.append(f'Search results for "{query}":')
24-
for i, result in enumerate(search_results, 1):
25-
output.append(f'{i}. {result.title}')
26-
output.append(f' URL: {result.url}')
27-
output.append(f' Content: {result.content}')
28-
output.append('')
29-
21+
output.append(f'Search results for "{user_search}":')
22+
for result in results.results:
23+
output.append(f'{result.title}' if result.title else f'{result.content}')
24+
output.append(f' URL: {result.url}')
25+
output.append(f' Content: {result.content}')
26+
output.append('')
3027
return '\n'.join(output).rstrip()
3128

32-
elif isinstance(results, WebCrawlResponse):
33-
if not results.success:
34-
error_msg = ', '.join(results.errors) if results.errors else 'Unknown error'
35-
return f'Web crawl failed: {error_msg}'
36-
37-
output = []
38-
for url, crawl_results in results.results.items():
39-
output.append(f'Crawl results for "{url}":')
40-
for i, result in enumerate(crawl_results, 1):
41-
output.append(f'{i}. {result.title}')
42-
output.append(f' URL: {result.url}')
43-
output.append(f' Content: {result.content}')
44-
if result.links:
45-
output.append(f' Links: {", ".join(result.links)}')
46-
output.append('')
47-
29+
elif isinstance(results, WebFetchResponse):
30+
output.append(f'Fetch results for "{user_search}":')
31+
output.extend(
32+
[
33+
f'Title: {results.title}',
34+
f'URL: {user_search}' if user_search else '',
35+
f'Content: {results.content}',
36+
]
37+
)
38+
if results.links:
39+
output.append(f'Links: {", ".join(results.links)}')
40+
output.append('')
4841
return '\n'.join(output).rstrip()
4942

5043

51-
# Set OLLAMA_API_KEY in the environment variable or use the headers parameter to set the authorization header
52-
# client = Client(headers={'Authorization': 'Bearer <OLLAMA_API_KEY>'})
44+
# client = Client(headers={'Authorization': f"Bearer {os.getenv('OLLAMA_API_KEY')}"} if api_key else None)
45+
available_tools = {'web_search': web_search, 'web_fetch': web_fetch}
5346

54-
available_tools = {'web_search': web_search, 'web_crawl': web_crawl}
55-
56-
query = "ollama's new engine"
47+
query = "what is ollama's new engine"
5748
print('Query: ', query)
5849

5950
messages = [{'role': 'user', 'content': query}]
6051
while True:
61-
response = chat(model='qwen3', messages=messages, tools=[web_search, web_crawl], think=True)
52+
response = chat(model='qwen3', messages=messages, tools=[web_search, web_fetch], think=True)
6253
if response.message.thinking:
6354
print('Thinking: ')
6455
print(response.message.thinking + '\n\n')
@@ -72,12 +63,20 @@ def format_tool_results(results: Union[WebSearchResponse, WebCrawlResponse]):
7263
for tool_call in response.message.tool_calls:
7364
function_to_call = available_tools.get(tool_call.function.name)
7465
if function_to_call:
75-
result: WebSearchResponse | WebCrawlResponse = function_to_call(**tool_call.function.arguments)
76-
print('Result from tool call name: ', tool_call.function.name, 'with arguments: ', tool_call.function.arguments)
77-
print('Result: ', format_tool_results(result)[:200])
66+
args = tool_call.function.arguments
67+
result: Union[WebSearchResponse, WebFetchResponse] = function_to_call(**args)
68+
print('Result from tool call name:', tool_call.function.name, 'with arguments:')
69+
print(args)
70+
print()
71+
72+
user_search = args.get('query', '') or args.get('url', '')
73+
formatted_tool_results = format_tool_results(result, user_search=user_search)
74+
75+
print(formatted_tool_results[:300])
76+
print()
7877

7978
# caps the result at ~2000 tokens
80-
messages.append({'role': 'tool', 'content': format_tool_results(result)[: 2000 * 4], 'tool_name': tool_call.function.name})
79+
messages.append({'role': 'tool', 'content': formatted_tool_results[: 2000 * 4], 'tool_name': tool_call.function.name})
8180
else:
8281
print(f'Tool {tool_call.function.name} not found')
8382
messages.append({'role': 'tool', 'content': f'Tool {tool_call.function.name} not found', 'tool_name': tool_call.function.name})

ollama/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
ShowResponse,
1616
StatusResponse,
1717
Tool,
18-
WebCrawlResponse,
18+
WebFetchResponse,
1919
WebSearchResponse,
2020
)
2121

@@ -37,7 +37,7 @@
3737
'ShowResponse',
3838
'StatusResponse',
3939
'Tool',
40-
'WebCrawlResponse',
40+
'WebFetchResponse',
4141
'WebSearchResponse',
4242
]
4343

@@ -56,4 +56,4 @@
5656
show = _client.show
5757
ps = _client.ps
5858
web_search = _client.web_search
59-
web_crawl = _client.web_crawl
59+
web_fetch = _client.web_fetch

ollama/_client.py

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
ShowResponse,
6767
StatusResponse,
6868
Tool,
69-
WebCrawlRequest,
70-
WebCrawlResponse,
69+
WebFetchRequest,
70+
WebFetchResponse,
7171
WebSearchRequest,
7272
WebSearchResponse,
7373
)
@@ -633,13 +633,13 @@ def ps(self) -> ProcessResponse:
633633
'/api/ps',
634634
)
635635

636-
def web_search(self, queries: Sequence[str], max_results: int = 3) -> WebSearchResponse:
636+
def web_search(self, query: str, max_results: int = 3) -> WebSearchResponse:
637637
"""
638638
Performs a web search
639639
640640
Args:
641-
queries: The queries to search for
642-
max_results: The maximum number of results to return.
641+
query: The query to search for
642+
max_results: The maximum number of results to return (default: 3)
643643
644644
Returns:
645645
WebSearchResponse with the search results
@@ -654,32 +654,30 @@ def web_search(self, queries: Sequence[str], max_results: int = 3) -> WebSearchR
654654
'POST',
655655
'https://ollama.com/api/web_search',
656656
json=WebSearchRequest(
657-
queries=queries,
657+
query=query,
658658
max_results=max_results,
659659
).model_dump(exclude_none=True),
660660
)
661661

662-
def web_crawl(self, urls: Sequence[str]) -> WebCrawlResponse:
662+
def web_fetch(self, url: str) -> WebFetchResponse:
663663
"""
664-
Gets the content of web pages for the provided URLs.
664+
Fetches the content of a web page for the provided URL.
665665
666666
Args:
667-
urls: The URLs to crawl
667+
url: The URL to fetch
668668
669669
Returns:
670-
WebCrawlResponse with the crawl results
671-
Raises:
672-
ValueError: If OLLAMA_API_KEY environment variable is not set
670+
WebFetchResponse with the fetched result
673671
"""
674672
if not self._client.headers.get('authorization', '').startswith('Bearer '):
675673
raise ValueError('Authorization header with Bearer token is required for web fetch')
676674

677675
return self._request(
678-
WebCrawlResponse,
676+
WebFetchResponse,
679677
'POST',
680-
'https://ollama.com/api/web_crawl',
681-
json=WebCrawlRequest(
682-
urls=urls,
678+
'https://ollama.com/api/web_fetch',
679+
json=WebFetchRequest(
680+
url=url,
683681
).model_dump(exclude_none=True),
684682
)
685683

@@ -752,13 +750,13 @@ async def inner():
752750

753751
return cls(**(await self._request_raw(*args, **kwargs)).json())
754752

755-
async def websearch(self, queries: Sequence[str], max_results: int = 3) -> WebSearchResponse:
753+
async def web_search(self, query: str, max_results: int = 3) -> WebSearchResponse:
756754
"""
757755
Performs a web search
758756
759757
Args:
760-
queries: The queries to search for
761-
max_results: The maximum number of results to return.
758+
query: The query to search for
759+
max_results: The maximum number of results to return (default: 3)
762760
763761
Returns:
764762
WebSearchResponse with the search results
@@ -768,27 +766,27 @@ async def websearch(self, queries: Sequence[str], max_results: int = 3) -> WebSe
768766
'POST',
769767
'https://ollama.com/api/web_search',
770768
json=WebSearchRequest(
771-
queries=queries,
769+
query=query,
772770
max_results=max_results,
773771
).model_dump(exclude_none=True),
774772
)
775773

776-
async def webcrawl(self, urls: Sequence[str]) -> WebCrawlResponse:
774+
async def web_fetch(self, url: str) -> WebFetchResponse:
777775
"""
778-
Gets the content of web pages for the provided URLs.
776+
Fetches the content of a web page for the provided URL.
779777
780778
Args:
781-
urls: The URLs to crawl
779+
url: The URL to fetch
782780
783781
Returns:
784-
WebCrawlResponse with the crawl results
782+
WebFetchResponse with the fetched result
785783
"""
786784
return await self._request(
787-
WebCrawlResponse,
785+
WebFetchResponse,
788786
'POST',
789-
'https://ollama.com/api/web_crawl',
790-
json=WebCrawlRequest(
791-
urls=urls,
787+
'https://ollama.com/api/web_fetch',
788+
json=WebFetchRequest(
789+
url=url,
792790
).model_dump(exclude_none=True),
793791
)
794792

ollama/_types.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -542,37 +542,28 @@ class Model(SubscriptableBaseModel):
542542

543543

544544
class WebSearchRequest(SubscriptableBaseModel):
545-
queries: Sequence[str]
545+
query: str
546546
max_results: Optional[int] = None
547547

548548

549549
class WebSearchResult(SubscriptableBaseModel):
550-
title: str
551-
url: str
552-
content: str
550+
content: Optional[str] = None
551+
title: Optional[str] = None
552+
url: Optional[str] = None
553553

554554

555-
class WebCrawlResult(SubscriptableBaseModel):
556-
title: str
555+
class WebFetchRequest(SubscriptableBaseModel):
557556
url: str
558-
content: str
559-
links: Optional[Sequence[str]] = None
560557

561558

562559
class WebSearchResponse(SubscriptableBaseModel):
563-
results: Mapping[str, Sequence[WebSearchResult]]
564-
success: bool
565-
errors: Optional[Sequence[str]] = None
566-
560+
results: Sequence[WebSearchResult]
567561

568-
class WebCrawlRequest(SubscriptableBaseModel):
569-
urls: Sequence[str]
570562

571-
572-
class WebCrawlResponse(SubscriptableBaseModel):
573-
results: Mapping[str, Sequence[WebCrawlResult]]
574-
success: bool
575-
errors: Optional[Sequence[str]] = None
563+
class WebFetchResponse(SubscriptableBaseModel):
564+
title: Optional[str] = None
565+
content: Optional[str] = None
566+
links: Optional[Sequence[str]] = None
576567

577568

578569
class RequestError(Exception):

tests/test_client.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,29 +1203,29 @@ def test_client_web_search_requires_bearer_auth_header(monkeypatch: pytest.Monke
12031203
client = Client()
12041204

12051205
with pytest.raises(ValueError, match='Authorization header with Bearer token is required for web search'):
1206-
client.web_search(['test query'])
1206+
client.web_search('test query')
12071207

12081208

1209-
def test_client_web_crawl_requires_bearer_auth_header(monkeypatch: pytest.MonkeyPatch):
1209+
def test_client_web_fetch_requires_bearer_auth_header(monkeypatch: pytest.MonkeyPatch):
12101210
monkeypatch.delenv('OLLAMA_API_KEY', raising=False)
12111211

12121212
client = Client()
12131213

12141214
with pytest.raises(ValueError, match='Authorization header with Bearer token is required for web fetch'):
1215-
client.web_crawl(['https://example.com'])
1215+
client.web_fetch('https://example.com')
12161216

12171217

12181218
def _mock_request_web_search(self, cls, method, url, json=None, **kwargs):
12191219
assert method == 'POST'
12201220
assert url == 'https://ollama.com/api/web_search'
1221-
assert json is not None and 'queries' in json and 'max_results' in json
1221+
assert json is not None and 'query' in json and 'max_results' in json
12221222
return httpxResponse(status_code=200, content='{"results": {}, "success": true}')
12231223

12241224

1225-
def _mock_request_web_crawl(self, cls, method, url, json=None, **kwargs):
1225+
def _mock_request_web_fetch(self, cls, method, url, json=None, **kwargs):
12261226
assert method == 'POST'
1227-
assert url == 'https://ollama.com/api/web_crawl'
1228-
assert json is not None and 'urls' in json
1227+
assert url == 'https://ollama.com/api/web_fetch'
1228+
assert json is not None and 'url' in json
12291229
return httpxResponse(status_code=200, content='{"results": {}, "success": true}')
12301230

12311231

@@ -1234,31 +1234,31 @@ def test_client_web_search_with_env_api_key(monkeypatch: pytest.MonkeyPatch):
12341234
monkeypatch.setattr(Client, '_request', _mock_request_web_search)
12351235

12361236
client = Client()
1237-
client.web_search(['what is ollama?'], max_results=2)
1237+
client.web_search('what is ollama?', max_results=2)
12381238

12391239

1240-
def test_client_web_crawl_with_env_api_key(monkeypatch: pytest.MonkeyPatch):
1240+
def test_client_web_fetch_with_env_api_key(monkeypatch: pytest.MonkeyPatch):
12411241
monkeypatch.setenv('OLLAMA_API_KEY', 'test-key')
1242-
monkeypatch.setattr(Client, '_request', _mock_request_web_crawl)
1242+
monkeypatch.setattr(Client, '_request', _mock_request_web_fetch)
12431243

12441244
client = Client()
1245-
client.web_crawl(['https://example.com'])
1245+
client.web_fetch('https://example.com')
12461246

12471247

12481248
def test_client_web_search_with_explicit_bearer_header(monkeypatch: pytest.MonkeyPatch):
12491249
monkeypatch.delenv('OLLAMA_API_KEY', raising=False)
12501250
monkeypatch.setattr(Client, '_request', _mock_request_web_search)
12511251

12521252
client = Client(headers={'Authorization': 'Bearer custom-token'})
1253-
client.web_search(['what is ollama?'], max_results=1)
1253+
client.web_search('what is ollama?', max_results=1)
12541254

12551255

1256-
def test_client_web_crawl_with_explicit_bearer_header(monkeypatch: pytest.MonkeyPatch):
1256+
def test_client_web_fetch_with_explicit_bearer_header(monkeypatch: pytest.MonkeyPatch):
12571257
monkeypatch.delenv('OLLAMA_API_KEY', raising=False)
1258-
monkeypatch.setattr(Client, '_request', _mock_request_web_crawl)
1258+
monkeypatch.setattr(Client, '_request', _mock_request_web_fetch)
12591259

12601260
client = Client(headers={'Authorization': 'Bearer custom-token'})
1261-
client.web_crawl(['https://example.com'])
1261+
client.web_fetch('https://example.com')
12621262

12631263

12641264
def test_client_bearer_header_from_env(monkeypatch: pytest.MonkeyPatch):
@@ -1274,4 +1274,4 @@ def test_client_explicit_bearer_header_overrides_env(monkeypatch: pytest.MonkeyP
12741274

12751275
client = Client(headers={'Authorization': 'Bearer explicit-token'})
12761276
assert client._client.headers['authorization'] == 'Bearer explicit-token'
1277-
client.web_search(['override check'])
1277+
client.web_search('override check')

0 commit comments

Comments
 (0)