284 lines
8.7 KiB
Python
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()
|