web search via exa (spec-015): web_search tool, sanitized results, per-user cap, host-config key

This commit is contained in:
Oleksandr Kozachuk
2026-07-14 12:08:48 +02:00
parent 5d01400638
commit cdd5a4cd48
6 changed files with 388 additions and 0 deletions
+12
View File
@@ -17,6 +17,8 @@ from .leonardo_draw import LeonardoAIDrawMixIn
from .news import GET_NEWS_TOOL, query_news
from .quota import QuotaLedger
from .url_reader import FETCH_URL_TOOL, URLReader
from .websearch import DEFAULT_RESULTS as WEB_DEFAULT_RESULTS
from .websearch import WEB_SEARCH_TOOL, WebSearch
# The response envelope, enforced server-side via structured outputs
# (ENV-19). All fields required, closed object, nullable where the
@@ -166,6 +168,8 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
self.url_reader = URLReader(lambda: self.config, self.image_cache)
# Codex Mechanicus search (SPEC-014); Luma's own archive at binaric.tech
self.codex = CodexSearch(lambda: self.config)
# Web search (SPEC-015) via Exa; general "look it up" beyond fetch_url/news/codex
self.web_search = WebSearch(lambda: self.config)
def _available_tools(self) -> List[Dict[str, Any]]:
"""Assemble the function-tool list from every enabled provider (URL-01)."""
@@ -183,6 +187,8 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
functions.append(CODEX_SEARCH_TOOL)
if self.config.get("enable-news-tool", False) and self.store is not None: # NEWS-10
functions.append(GET_NEWS_TOOL)
if self.web_search.enabled(): # WEB-01
functions.append(WEB_SEARCH_TOOL)
return functions
async def _dispatch_tool(self, name: str, args: Dict[str, Any], author: str) -> Any:
@@ -207,6 +213,12 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
self.ledger._add(f"news:{author}", 1)
summary_chars = int(self.config.get("news-summary-chars", 200))
return query_news(self.store, args.get("topic"), args.get("source"), args.get("limit", 10), summary_chars)
if name == "web_search":
per_user_cap = int(self.config.get("web-daily-per-user", 30))
if self.ledger._get(f"web:{author}") >= per_user_cap: # WEB-05
return {"error": "daily web search limit reached"}
self.ledger._add(f"web:{author}", 1)
return await self.web_search.search(str(args.get("query", "")), int(args.get("num_results", WEB_DEFAULT_RESULTS)))
return await self._execute_igdb_function(name, args)
async def draw_openai(self, description: str, count: int = 1) -> List[BytesIO]: