Introduce FastTools: pre-flight classifier + context enrichment
New fast_tools.py module: - FastTool base class (matches + run interface) - RealTimeSearchTool: SearXNG search for weather/news/prices/scores - FastToolRunner: classifier that checks all tools, runs matching ones concurrently and returns combined context Router accepts FastToolRunner; any_matches() forces medium tier before LLM classification (replaces _MEDIUM_FORCE_PATTERNS regex). agent.py: _REALTIME_RE and _searxng_search_async removed; pre-flight gather now includes fast_tool_runner.run_matching() alongside URL fetch and memory retrieval. To add a new fast tool: subclass FastTool, add to the list in agent.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
116
fast_tools.py
Normal file
116
fast_tools.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Fast Tools — pre-flight tools invoked by a classifier before the main LLM call.
|
||||
|
||||
Each FastTool has:
|
||||
- matches(message) → bool : regex classifier that determines if this tool applies
|
||||
- run(message) → str : async fetch that returns enrichment context
|
||||
|
||||
FastToolRunner holds a list of FastTools. The Router uses any_matches() to force
|
||||
the tier to medium before LLM classification. run_agent_task() calls run_matching()
|
||||
to build extra context that is injected into the system prompt.
|
||||
|
||||
To add a new fast tool:
|
||||
1. Subclass FastTool, implement name/matches/run
|
||||
2. Add an instance to the list passed to FastToolRunner in agent.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class FastTool(ABC):
|
||||
"""Base class for all pre-flight fast tools."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str: ...
|
||||
|
||||
@abstractmethod
|
||||
def matches(self, message: str) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
async def run(self, message: str) -> str: ...
|
||||
|
||||
|
||||
class RealTimeSearchTool(FastTool):
|
||||
"""
|
||||
Injects live SearXNG search snippets for queries that require real-time data:
|
||||
weather, news, prices, scores, business hours.
|
||||
|
||||
Matched queries are also forced to medium tier by the Router so the richer
|
||||
model handles the injected context.
|
||||
"""
|
||||
|
||||
_PATTERN = re.compile(
|
||||
r"\b(weather|forecast|temperature|rain(ing)?|snow(ing)?|humidity|wind\s*speed"
|
||||
r"|today.?s news|breaking news|latest news|news today|current events"
|
||||
r"|bitcoin price|crypto price|stock price|exchange rate"
|
||||
r"|right now|currently|at the moment|live score|score now|score today"
|
||||
r"|open now|hours today|is .+ open)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
def __init__(self, searxng_url: str):
|
||||
self._searxng_url = searxng_url
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "real_time_search"
|
||||
|
||||
def matches(self, message: str) -> bool:
|
||||
return bool(self._PATTERN.search(message))
|
||||
|
||||
async def run(self, message: str) -> str:
|
||||
"""Search SearXNG and return top snippets as a context block."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
r = await client.get(
|
||||
f"{self._searxng_url}/search",
|
||||
params={"q": message, "format": "json"},
|
||||
)
|
||||
r.raise_for_status()
|
||||
items = r.json().get("results", [])[:4]
|
||||
except Exception as e:
|
||||
return f"[real_time_search error: {e}]"
|
||||
if not items:
|
||||
return ""
|
||||
lines = [f"Live web search results for: {message}\n"]
|
||||
for i, item in enumerate(items, 1):
|
||||
title = item.get("title", "")
|
||||
url = item.get("url", "")
|
||||
snippet = item.get("content", "")[:400]
|
||||
lines.append(f"[{i}] {title}\nURL: {url}\n{snippet}\n")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class FastToolRunner:
|
||||
"""
|
||||
Classifier + executor for fast tools.
|
||||
|
||||
Used in two places:
|
||||
- Router.route(): any_matches() forces medium tier before LLM classification
|
||||
- run_agent_task(): run_matching() builds enrichment context in the pre-flight gather
|
||||
"""
|
||||
|
||||
def __init__(self, tools: list[FastTool]):
|
||||
self._tools = tools
|
||||
|
||||
def any_matches(self, message: str) -> bool:
|
||||
"""True if any fast tool applies to this message."""
|
||||
return any(t.matches(message) for t in self._tools)
|
||||
|
||||
def matching_names(self, message: str) -> list[str]:
|
||||
"""Names of tools that match this message (for logging)."""
|
||||
return [t.name for t in self._tools if t.matches(message)]
|
||||
|
||||
async def run_matching(self, message: str) -> str:
|
||||
"""Run all matching tools concurrently and return combined context."""
|
||||
matching = [t for t in self._tools if t.matches(message)]
|
||||
if not matching:
|
||||
return ""
|
||||
results = await asyncio.gather(*[t.run(message) for t in matching])
|
||||
parts = [r for r in results if r and not r.startswith("[")]
|
||||
return "\n\n".join(parts)
|
||||
Reference in New Issue
Block a user