FHC_OCR / agent /tools.py
Shubadecka's picture
fixing thinking and search
5a6288c
Raw
History Blame Contribute Delete
6.83 kB
"""
Tool definitions (HF/OpenAI schema) and dispatcher for the VL agent.
Three tools:
- search_web: search the internet and return snippets
- final_output: signal the agent loop to stop and return an answer
- abort: signal the agent loop to stop and report a failure/reason
"""
import json
import logging
import os
import urllib.error
import urllib.parse
import urllib.request
from dotenv import load_dotenv
load_dotenv()
_BRAVE_WEB_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search"
logger = logging.getLogger(__name__)
if not logger.handlers:
_handler = logging.StreamHandler()
_handler.setFormatter(logging.Formatter("%(levelname)s %(name)s: %(message)s"))
logger.addHandler(_handler)
logger.setLevel(logging.INFO)
logger.propagate = False
# ---------------------------------------------------------------------------
# Tool schemas (passed to InferenceClient.chat_completion(tools=...))
# ---------------------------------------------------------------------------
AGENT_TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": (
"Search the internet for up-to-date information. "
"Use this when you need current facts, news, or data that you are "
"not confident about from your training knowledge."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up on the web.",
}
},
"required": ["query"],
},
},
},
{
"type": "function",
"function": {
"name": "final_output",
"description": (
"Deliver the final answer to the user. "
"Call this once you have gathered enough information and are ready "
"to give a complete, accurate response. The 'answer' field will be "
"shown directly to the user."
),
"parameters": {
"type": "object",
"properties": {
"answer": {
"type": "string",
"description": "The complete final answer to present to the user.",
}
},
"required": ["answer"],
},
},
},
{
"type": "function",
"function": {
"name": "abort",
"description": (
"Abort the current task when it cannot be completed. "
"Use this if the task is impossible, unsafe, or the user asked to stop."
),
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Explanation of why the task is being aborted.",
}
},
"required": [],
},
},
},
]
# ---------------------------------------------------------------------------
# Tool implementations
# ---------------------------------------------------------------------------
def _brave_api_key() -> str | None:
return os.environ.get("BRAVE_SEARCH_API_KEY") or os.environ.get("BRAVE_API_KEY")
def _search_web(query: str, max_results: int = 5) -> str:
"""Run a Brave Web Search and return formatted snippets."""
count = max(1, min(max_results, 20))
logger.info("search_web input: query=%r max_results=%d", query, count)
def _finish(out: str) -> str:
logger.info("search_web output:\n%s", out)
return out
token = _brave_api_key()
if not token:
return _finish(
"Search unavailable: set BRAVE_SEARCH_API_KEY or BRAVE_API_KEY "
"in a .env file or the process environment."
)
params = urllib.parse.urlencode({"q": query, "count": str(count)})
url = f"{_BRAVE_WEB_SEARCH_URL}?{params}"
req = urllib.request.Request(
url,
headers={
"X-Subscription-Token": token,
"Accept": "application/json",
},
method="GET",
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
payload = json.loads(resp.read().decode())
except urllib.error.HTTPError as exc:
detail = exc.read().decode(errors="replace")[:800]
return _finish(f"Brave Search API error ({exc.code}): {detail}")
except urllib.error.URLError as exc:
return _finish(f"Search request failed: {exc.reason or exc}")
except json.JSONDecodeError as exc:
return _finish(f"Search returned invalid JSON: {exc}")
results = (payload.get("web") or {}).get("results") or []
if not results:
return _finish("No results found for the given query.")
blocks = []
for item in results[:count]:
title = item.get("title") or ""
body = item.get("description") or ""
href = item.get("url") or ""
blocks.append(f"**{title}**\n{body}\nSource: {href}")
return _finish("\n\n---\n\n".join(blocks))
def _final_output(answer: str) -> str:
"""No-op implementation; the orchestrator reads 'answer' directly."""
return "Answer delivered."
def _abort(reason: str = "") -> str:
"""No-op implementation; the orchestrator reads 'reason' directly."""
return f"Aborted: {reason}" if reason else "Task aborted."
# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------
def dispatch_tool(tool_name: str, arguments: dict | str) -> str:
"""
Call the named tool with the given arguments and return the result string.
`arguments` may arrive as a JSON string (from the model) or already as a dict.
"""
if isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except json.JSONDecodeError:
arguments = {}
if tool_name == "search_web":
query = arguments.get("query", "")
if not query:
msg = "Error: 'query' parameter is required for search_web."
logger.info("search_web input: query=%r (missing)", query)
logger.info("search_web output:\n%s", msg)
return msg
return _search_web(query)
if tool_name == "final_output":
answer = arguments.get("answer", "")
return _final_output(answer)
if tool_name == "abort":
reason = arguments.get("reason", "")
return _abort(reason)
return f"Error: unknown tool '{tool_name}'."