import logging import os from random import randint from typing import Callable, TypedDict, Iterator import requests from openai.types.responses import FunctionToolParam import config logger = logging.getLogger(__name__) class ToolEntry(TypedDict): spec: FunctionToolParam fn: Callable[..., str] class ToolRegistry: """ Register/manage the tools made available to an LLM. Each tool requires a `FunctionToolParam` spec to advertise to the model, and a `Callable` to run when invoked by the model. """ def __init__(self, tools: dict[str, ToolEntry] | None = None): self._tools = tools or {} def __contains__(self, name: str) -> bool: return name in self._tools def __getitem__(self, name: str) -> ToolEntry: return self._tools[name] def __iter__(self) -> Iterator[str]: return iter(self._tools) def __repr__(self) -> str: return f"ToolRegistry({list(self)})" def add(self, spec: FunctionToolParam, fn: Callable[..., str]): """Register a tool (under the name given in `spec`).""" self._tools[spec["name"]] = {"spec": spec, "fn": fn} def subset(self, names: list[str]) -> "ToolRegistry": """Return a new `ToolRegistry` narrowed to just the specified tool names.""" return ToolRegistry({name: self._tools[name] for name in names if name in self}) def get_specs(self, names: list[str] | None = None) -> list[FunctionToolParam]: """Return function specs for tool `names` (or for all tools, if none are specified).""" return [self[name]["spec"] for name in (names or self) if name in self] ### tool processing functions def llm_send_notification(message: str) -> str: """ Send a push notification via the Pushover service. API documentation: https://pushover.net/api """ payload = { "user": os.getenv("PUSHOVER_USER"), "token": os.getenv("PUSHOVER_TOKEN"), "message": message, } response = requests.post(config.PUSHOVER_ENDPOINT, data=payload, timeout=8) if response.ok and response.json().get('status') == 1: return "Push notification sent successfully." else: logger.error('send_notification failed with HTTP status %s: %s', response.status_code, response.text) return "Error. Unable to send notification at this time." def llm_roll_dice() -> str: """Get a 1d6 random value. Useful for testing sequential tool calls.""" return str(randint(1, 6)) ### tool specifications SEND_NOTIFICATION_SPEC: FunctionToolParam = { "type": "function", "strict": True, "name": "send_notification", "description": "Sends a push notification to the real-world Lamonte Smith via the " "Pushover service. Use this if the user needs to alert the " "real-world Lamonte about something.", "parameters": { "type": "object", "properties": { "message": { "type": "string", "description": "The notification message to send to Lamonte's device", }, }, "required": ["message"], "additionalProperties": False, }, } ROLL_DICE_SPEC: FunctionToolParam = { "type": "function", "strict": True, "name": "roll_dice", "description": "Roll a single six-sided dice and return the value.", "parameters": { "type": "object", "properties": {}, "required": [], "additionalProperties": False, }, } def build_all_tools() -> ToolRegistry: """Create and return a ToolRegistry with all available tools.""" tools = ToolRegistry() if (os.getenv("PUSHOVER_USER") is None or os.getenv("PUSHOVER_TOKEN") is None): logger.warning("Pushover credentials not available, cannot register send_notification tool") else: tools.add(SEND_NOTIFICATION_SPEC, llm_send_notification) tools.add(ROLL_DICE_SPEC, llm_roll_dice) return tools