Coverage for baibu/chat/search.py: 100%
30 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1"""Web search providers for the ``internet_search`` tool.
3No provider is bundled. A deployment writes a subclass of
4:class:`SearchProvider` for the search service it uses and names it in
5``CHAT_SEARCH_PROVIDER``. :class:`MockSearchProvider` returns made-up results
6for development and tests.
7"""
9from dataclasses import asdict
10from dataclasses import dataclass
11from functools import cache
13from django.conf import settings
14from django.utils.module_loading import import_string
17class SearchError(Exception):
18 """The search failed. The message is stored with the tool invocation."""
21@dataclass(frozen=True)
22class SearchResult:
23 title: str
24 url: str
25 snippet: str = ""
27 def as_dict(self) -> dict:
28 return asdict(self)
31class SearchProvider:
32 name = "provider"
34 def search(self, query: str, *, max_results: int) -> list[SearchResult]:
35 raise NotImplementedError
38class MockSearchProvider(SearchProvider):
39 """Deterministic results under example.org. A query containing
40 ``[search-fail]`` fails; ``[search-empty]`` finds nothing."""
42 name = "mock"
44 def search(self, query: str, *, max_results: int) -> list[SearchResult]:
45 if "[search-fail]" in query:
46 msg = "Mock search failure."
47 raise SearchError(msg)
48 if "[search-empty]" in query:
49 return []
50 return [
51 SearchResult(
52 title=f"Result {n} for {query}",
53 url=f"https://example.org/search/{n}",
54 snippet=f"Made-up text about {query}, number {n}.",
55 )
56 for n in range(1, max_results + 1)
57 ]
60@cache
61def _provider_class(path: str) -> type[SearchProvider]:
62 return import_string(path)
65def get_provider() -> SearchProvider | None:
66 path = settings.CHAT_SEARCH_PROVIDER
67 return _provider_class(path)() if path else None