kuechenpassagent / src /nlp /parser.py
lederyou's picture
Upload folder using huggingface_hub
db662ea verified
Raw
History Blame Contribute Delete
4.09 kB
"""OpenAI-backed order parser.
Wraps the OpenAI Chat Completions API. Returns a validated
:class:`OrderTicket`. If the API key is missing OR the API call fails,
``parse_order`` falls back to a deterministic heuristic parser so that the
Gradio demo never crashes (this is documented in the README).
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
from typing import Optional
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from src.config import DISH_TO_STATION, OPENAI_MODEL # noqa: E402
from src.nlp.prompts import PROMPTS # noqa: E402
from src.nlp.schema import ORDER_JSON_SCHEMA, OrderItem, OrderTicket, route_to_station # noqa: E402
def _maybe_load_dotenv() -> None:
try:
from dotenv import load_dotenv # type: ignore
except ImportError:
return
env_path = Path(__file__).resolve().parents[2] / ".env"
if env_path.exists():
load_dotenv(env_path)
_maybe_load_dotenv()
def _openai_client():
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
return None
try:
from openai import OpenAI # type: ignore
except ImportError:
return None
return OpenAI(api_key=api_key)
def call_llm(text: str, prompt_version: str = "v3_constrained") -> Optional[dict]:
"""Call the LLM. Returns the parsed JSON dict, or None on failure."""
client = _openai_client()
if client is None:
return None
system = PROMPTS.get(prompt_version, PROMPTS["v3_constrained"])
try:
response = client.chat.completions.create(
model=OPENAI_MODEL,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": text},
],
response_format={"type": "json_object"},
temperature=0.0,
)
content = response.choices[0].message.content or "{}"
return json.loads(content)
except Exception as exc: # noqa: BLE001 - we degrade to fallback
print(f"[nlp.parser] OpenAI call failed: {exc}")
return None
_NUMBER_WORDS = {
"one": 1, "ein": 1, "eine": 1, "einen": 1, "a": 1,
"two": 2, "zwei": 2,
"three": 3, "drei": 3,
"four": 4, "vier": 4,
"five": 5, "fuenf": 5, "fünf": 5,
"six": 6, "sechs": 6,
"seven": 7, "sieben": 7,
"eight": 8, "acht": 8,
}
def heuristic_parse(text: str) -> OrderTicket:
"""Very small fallback parser used when OpenAI is unavailable."""
text_low = text.lower()
items: list[OrderItem] = []
for keyword, station in DISH_TO_STATION.items():
if keyword in text_low:
qty = 1
# try to find a count immediately before the keyword
match = re.search(rf"(\d+)\s+\w*\s*{re.escape(keyword)}", text_low)
if match:
qty = int(match.group(1))
else:
for word, value in _NUMBER_WORDS.items():
if re.search(rf"\b{word}\b\s+\w*\s*{re.escape(keyword)}", text_low):
qty = value
break
items.append(
OrderItem(
dish=keyword,
quantity=qty,
modifiers=[],
station=station,
)
)
if not items:
items.append(
OrderItem(dish="unknown_dish", quantity=1, modifiers=[], station=None)
)
return OrderTicket(items=items, raw_text=text).finalize()
def parse_order(text: str, prompt_version: str = "v3_constrained") -> OrderTicket:
"""Public entry point. Returns a finalized :class:`OrderTicket`."""
data = call_llm(text, prompt_version=prompt_version)
if data is None:
return heuristic_parse(text)
try:
ticket = OrderTicket.model_validate({**data, "raw_text": text})
return ticket.finalize()
except Exception as exc: # noqa: BLE001
print(f"[nlp.parser] schema validation failed: {exc}. Using heuristic.")
return heuristic_parse(text)