Files
the-nexus/nexus/mcdonald_wizard.py
Claude (Opus 4.6) 38218277c3
Some checks failed
Deploy Nexus / deploy (push) Failing after 8s
Staging Verification Gate / verify-staging (push) Failing after 10s
[claude] feat: McDonald wizard Hermes shim — McAttack (#1689) (#1690)
2026-04-21 15:17:02 +00:00

284 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""
McDonald Wizard — Hermes shim for the McDonald chatbot API
Exposes the `mcdonald-wizard` Hermes tool, which forwards prompts to the
McDonald chatbot API and returns wizard-style responses. Registered as a
Hermes skill via ~/.hermes/skills/shim-mcdonald-wizard.py.
Usage:
from nexus.mcdonald_wizard import McdonaldWizard
wizard = McdonaldWizard()
response = wizard.ask("What is your quest?")
print(response.text)
Environment Variables:
MCDONALDS_API_KEY — McDonald chatbot API key (required)
MCDONALDS_ENDPOINT — API endpoint (default: https://api.mcdonalds.com/v1/chat)
MCDONALDS_TIMEOUT — Request timeout in seconds (default: 30)
MCDONALDS_RETRIES — Max retry attempts (default: 3)
"""
from __future__ import annotations
import logging
import os
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
import requests
log = logging.getLogger("mcdonald_wizard")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [mcdonald_wizard] %(message)s",
datefmt="%H:%M:%S",
)
DEFAULT_ENDPOINT = "https://api.mcdonalds.com/v1/chat"
DEFAULT_TIMEOUT = 30
DEFAULT_RETRIES = 3
WIZARD_ID = "mcdonald-wizard"
# Retry backoff: base * 2^(attempt-1)
RETRY_BASE_DELAY = 1.0
@dataclass
class WizardResponse:
"""Response from the McDonald chatbot wizard."""
text: str = ""
model: str = ""
latency_ms: float = 0.0
attempt: int = 1
error: Optional[str] = None
timestamp: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
def to_dict(self) -> dict:
return {
"text": self.text,
"model": self.model,
"latency_ms": self.latency_ms,
"attempt": self.attempt,
"error": self.error,
"timestamp": self.timestamp,
}
class McdonaldWizard:
"""
McDonald chatbot wizard client.
Forwards prompts to the McDonald chatbot API with retry/timeout handling.
Integrates with Hermes as the `mcdonald-wizard` tool.
"""
def __init__(
self,
api_key: Optional[str] = None,
endpoint: Optional[str] = None,
timeout: Optional[int] = None,
max_retries: Optional[int] = None,
):
self.api_key = api_key or os.environ.get("MCDONALDS_API_KEY", "")
self.endpoint = endpoint or os.environ.get(
"MCDONALDS_ENDPOINT", DEFAULT_ENDPOINT
)
self.timeout = timeout or int(
os.environ.get("MCDONALDS_TIMEOUT", DEFAULT_TIMEOUT)
)
self.max_retries = max_retries or int(
os.environ.get("MCDONALDS_RETRIES", DEFAULT_RETRIES)
)
if not self.api_key:
log.warning(
"MCDONALDS_API_KEY not set — wizard will return errors on live calls"
)
# Session stats
self.request_count = 0
self.total_latency_ms = 0.0
def _headers(self) -> dict:
return {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
def _post_with_retry(self, payload: dict) -> tuple[dict, int, float]:
"""
POST to the McDonald API with retry/backoff.
Returns (response_json, attempt_number, latency_ms).
Raises on final failure.
"""
last_exc: Optional[Exception] = None
for attempt in range(1, self.max_retries + 1):
t0 = time.monotonic()
try:
resp = requests.post(
self.endpoint,
json=payload,
headers=self._headers(),
timeout=self.timeout,
)
latency_ms = (time.monotonic() - t0) * 1000
if resp.status_code in (429, 500, 502, 503, 504):
raise requests.HTTPError(
f"HTTP {resp.status_code}: {resp.text[:200]}"
)
resp.raise_for_status()
return resp.json(), attempt, latency_ms
except Exception as exc:
last_exc = exc
if attempt < self.max_retries:
delay = RETRY_BASE_DELAY * (2 ** (attempt - 1))
log.warning(
"attempt %d/%d failed (%s) — retrying in %.1fs",
attempt,
self.max_retries,
exc,
delay,
)
time.sleep(delay)
else:
log.error(
"all %d attempts failed: %s", self.max_retries, exc
)
raise last_exc # type: ignore[misc]
def ask(
self,
prompt: str,
system: Optional[str] = None,
context: Optional[str] = None,
) -> WizardResponse:
"""
Send a prompt to the McDonald wizard chatbot.
Args:
prompt: User message to the wizard.
system: Optional system instruction override.
context: Optional prior context to prepend.
Returns:
WizardResponse with text, latency, and error fields.
"""
if not self.api_key:
return WizardResponse(
error="MCDONALDS_API_KEY not set — cannot call McDonald wizard API"
)
messages = []
if system:
messages.append({"role": "system", "content": system})
if context:
messages.append({"role": "user", "content": context})
messages.append(
{"role": "assistant", "content": "Understood, I have the context."}
)
messages.append({"role": "user", "content": prompt})
payload = {"messages": messages}
t0 = time.monotonic()
try:
data, attempt, latency_ms = self._post_with_retry(payload)
except Exception as exc:
latency_ms = (time.monotonic() - t0) * 1000
self.request_count += 1
self.total_latency_ms += latency_ms
return WizardResponse(
error=f"McDonald wizard API failed: {exc}",
latency_ms=latency_ms,
)
self.request_count += 1
self.total_latency_ms += latency_ms
text = (
data.get("choices", [{}])[0]
.get("message", {})
.get("content", "")
)
model = data.get("model", "")
return WizardResponse(
text=text,
model=model,
latency_ms=latency_ms,
attempt=attempt,
)
def session_stats(self) -> dict:
"""Return session telemetry."""
return {
"wizard_id": WIZARD_ID,
"request_count": self.request_count,
"total_latency_ms": self.total_latency_ms,
"avg_latency_ms": (
self.total_latency_ms / self.request_count
if self.request_count
else 0.0
),
}
# ── Hermes tool function ──────────────────────────────────────────────────
_wizard_instance: Optional[McdonaldWizard] = None
def _get_wizard() -> McdonaldWizard:
global _wizard_instance
if _wizard_instance is None:
_wizard_instance = McdonaldWizard()
return _wizard_instance
def mcdonald_wizard(prompt: str, system: Optional[str] = None) -> dict:
"""
Hermes tool: forward *prompt* to the McDonald chatbot wizard.
Args:
prompt: The message to send to the wizard.
system: Optional system instruction.
Returns:
dict with keys: text, model, latency_ms, attempt, error.
"""
wizard = _get_wizard()
resp = wizard.ask(prompt, system=system)
return resp.to_dict()
# ── CLI ───────────────────────────────────────────────────────────────────
def main() -> None:
import argparse
parser = argparse.ArgumentParser(description="McDonald Wizard CLI")
parser.add_argument("prompt", nargs="?", default="Greetings, wizard!", help="Prompt to send")
parser.add_argument("--system", default=None, help="System instruction")
parser.add_argument("--endpoint", default=None, help="API endpoint override")
args = parser.parse_args()
wizard = McdonaldWizard(endpoint=args.endpoint)
resp = wizard.ask(args.prompt, system=args.system)
if resp.error:
print(f"[ERROR] {resp.error}")
else:
print(resp.text)
print(f"\n[latency={resp.latency_ms:.0f}ms attempt={resp.attempt} model={resp.model}]")
if __name__ == "__main__":
main()