Coverage for baibu/chat/tools.py: 100%
47 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"""Tools the assistant can call during a reply.
3A tool has a stable name, a description and a JSON schema for its arguments
4(the format LiteLLM passes to models), and returns text for the model plus
5optional sources to show under the reply. Tools are offered only when they
6are configured; with none, replies are plain model calls.
7"""
9import json
10from dataclasses import dataclass
11from dataclasses import field
13from django.conf import settings
15from . import search
18class ToolError(Exception):
19 """The tool failed. The model is told, and may answer without it."""
22@dataclass
23class ToolResult:
24 content: str
25 sources: list[dict] = field(default_factory=list)
26 provider: str = ""
29class Tool:
30 name = ""
31 description = ""
32 parameters: dict = {}
34 def is_available(self) -> bool:
35 return True
37 def schema(self) -> dict:
38 return {
39 "type": "function",
40 "function": {"name": self.name, "description": self.description, "parameters": self.parameters},
41 }
43 def run(self, arguments: dict) -> ToolResult:
44 raise NotImplementedError
47class InternetSearchTool(Tool):
48 name = "internet_search"
49 description = (
50 "Search the web for current or local information. Use it when the answer depends on recent events, "
51 "places, opening times, prices or facts you are not sure of. Cite the sources you use."
52 )
53 parameters = {
54 "type": "object",
55 "properties": {"query": {"type": "string", "description": "What to search for."}},
56 "required": ["query"],
57 }
59 def is_available(self) -> bool:
60 return bool(settings.CHAT_SEARCH_PROVIDER)
62 def run(self, arguments: dict) -> ToolResult:
63 query = str(arguments.get("query") or "").strip()[:300]
64 if not query:
65 msg = "A search query is required."
66 raise ToolError(msg)
67 provider = search.get_provider()
68 try:
69 results = provider.search(query, max_results=settings.CHAT_SEARCH_MAX_RESULTS)
70 except search.SearchError as exc:
71 raise ToolError(str(exc)) from exc
72 except Exception as exc:
73 msg = f"Search failed: {type(exc).__name__}"
74 raise ToolError(msg) from exc
75 results = results[: settings.CHAT_SEARCH_MAX_RESULTS]
76 payload = [r.as_dict() for r in results]
77 content = json.dumps({"query": query, "results": payload}, ensure_ascii=False)
78 sources = [{"title": r.title, "url": r.url} for r in results if r.url.startswith(("https://", "http://"))]
79 return ToolResult(content=content, sources=sources, provider=provider.name)
82TOOLS: dict[str, Tool] = {tool.name: tool for tool in (InternetSearchTool(),)}
85def available_tools() -> list[Tool]:
86 return [tool for tool in TOOLS.values() if tool.is_available()]