Files
odysseus/services/search/service.py
Mubashir R 535d05c142 fix: SearchService.search() calls comprehensive_web_search incorrectly (broken public API) (#1720)
SearchService.search() did:

    raw_results = await comprehensive_web_search(
        query, max_results=10 * depth, fetch_content=fetch_content)

comprehensive_web_search is a synchronous function whose count knob is
`max_pages` (not `max_results`) and which has no `fetch_content` parameter, so
the call raised TypeError on argument binding; `await` on its non-coroutine
return would also fail. It returns a context string, or a (context, sources)
tuple with return_sources=True — not the list of dicts the wrapper iterates.

The method is exported in services/search/__init__.py and services/__init__.py
with a usage example in its docstring, so any caller of the documented public
API hit an immediate crash. Call it correctly via asyncio.to_thread with
max_pages + return_sources=True and use the returned source list as the rows.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:33:56 +09:00

103 lines
2.7 KiB
Python

# services/search/service.py
"""Search service — clean interface for web search."""
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
from . import (
comprehensive_web_search,
fetch_webpage_content,
get_search_config,
)
@dataclass
class SearchResult:
"""A single search result."""
url: str
title: str
snippet: str
content: Optional[str] = None
@dataclass
class SearchResponse:
"""Response from a search query."""
query: str
results: List[SearchResult]
total: int
cached: bool = False
class SearchService:
"""
Web search service.
Usage:
service = SearchService()
result = await service.search("python async patterns")
for r in result.results:
print(f"{r.title}: {r.url}")
"""
def __init__(self, default_depth: int = 1, fetch_content: bool = True):
self.default_depth = default_depth
self.fetch_content = fetch_content
async def search(
self,
query: str,
depth: Optional[int] = None,
fetch_content: Optional[bool] = None,
) -> SearchResponse:
"""
Search the web.
Args:
query: Search query
depth: Search depth (1=quick, 2=thorough, 3=comprehensive)
fetch_content: Whether to fetch full page content
Returns:
SearchResponse with results
"""
depth = depth or self.default_depth
# comprehensive_web_search is synchronous and, with return_sources=True,
# returns (context_str, [{"url", "title"}, ...]). Run it off the event
# loop so we don't block it, and use the source list as the result rows.
# `fetch_content` is accepted for API compatibility; the comprehensive
# search always fetches page content.
import asyncio
_context, raw_results = await asyncio.to_thread(
comprehensive_web_search,
query,
max_pages=10 * depth,
return_sources=True,
)
results = []
for r in raw_results:
if not isinstance(r, dict):
continue
results.append(SearchResult(
url=r.get("url", ""),
title=r.get("title", ""),
snippet=r.get("snippet", ""),
content=r.get("content"),
))
return SearchResponse(
query=query,
results=results,
total=len(results),
)
async def fetch_content(self, url: str) -> Optional[str]:
"""Fetch content from a URL."""
return await fetch_webpage_content(url)
def get_config(self) -> Dict[str, Any]:
"""Get current search configuration."""
return get_search_config()