Spaces:
Running on Zero
Running on Zero
File size: 7,520 Bytes
f0fae3f 8f9afd8 f0fae3f 8f9afd8 f0fae3f 8f9afd8 a221c9f 8f9afd8 a221c9f 8f9afd8 f0fae3f f9c247c f0fae3f 8f9afd8 f0fae3f 8f9afd8 f0fae3f f9c247c f0fae3f f9c247c f0fae3f 8f9afd8 f0fae3f 8f9afd8 f0fae3f 8f9afd8 a221c9f 8f9afd8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | """
llm_client.py
-------------
Wraps a hosted LLM (via huggingface_hub's InferenceClient, using HF's
serverless Inference Providers) to answer warehouse-operations questions,
grounded with context retrieved from the local knowledge base (simple RAG).
Design notes
------------
* Reads the HF token from the `HF_TOKEN` environment variable, which should
be added as a Space "secret" when deployed (Settings -> Variables and
secrets). The public demo also works without a token: it falls back to a
deterministic, still-useful extractive answer built from the retrieved
knowledge-base passages, so the Space never shows a broken demo.
* Serverless model availability on the free HF Inference API changes over
time (models get gated, deprecated, or moved between providers), so
rather than hard-depending on a single model id, we try a short list of
candidates in order and use the first one that responds successfully.
`LLM_MODEL_ID` (env var) is tried first if set, ahead of the built-in list.
* On failure, the *actual* exception message (not just its type) is
surfaced back to the UI, so a broken deployment is debuggable from the
Space itself instead of requiring log access.
"""
import os
import time
from dataclasses import dataclass, field
from typing import List, Optional
from src.retriever import KBRetriever, RetrievedDoc
# Small, widely-available instruct models known to work well on HF's free
# serverless Inference API. Tried in order; first success wins. If
# LLM_MODEL_ID is set as an env var, it is tried first, ahead of this list.
MODEL_CANDIDATES = [
"Qwen/Qwen2.5-7B-Instruct",
"meta-llama/Llama-3.1-8B-Instruct",
"meta-llama/Llama-3.2-3B-Instruct",
"mistralai/Mistral-7B-Instruct-v0.3",
"microsoft/Phi-3.5-mini-instruct",
"HuggingFaceH4/zephyr-7b-beta",
]
_env_model = os.environ.get("LLM_MODEL_ID")
if _env_model:
MODEL_CANDIDATES = [_env_model] + [m for m in MODEL_CANDIDATES if m != _env_model]
DEFAULT_MODEL_ID = MODEL_CANDIDATES[0]
# The token is normally expected as `HF_TOKEN`, but we also accept a few
# common alternate secret names in case the Space was set up with a
# different name. First one found wins. Add your own name here if needed.
TOKEN_ENV_VAR_CANDIDATES = [
"HF_TOKEN",
"Smart_Warehouse",
"HUGGINGFACE_TOKEN",
"HUGGINGFACEHUB_API_TOKEN",
"HF_API_TOKEN",
"HUGGING_FACE_HUB_TOKEN",
"HF_ACCESS_TOKEN",
]
def _get_hf_token() -> Optional[str]:
for var in TOKEN_ENV_VAR_CANDIDATES:
val = os.environ.get(var)
if val:
return val
return None
SYSTEM_PROMPT = (
"You are the Smart Warehouse AI Assistant, a helpful operations copilot "
"for a large automated distribution center (conveyors, AS/RS, AGVs/AMRs, "
"sortation, and a WMS). Answer concisely and practically, in the tone of "
"an experienced warehouse operations engineer. Use the provided CONTEXT "
"when relevant, and say so plainly if the question is outside the "
"context. Prefer short paragraphs or bullet points over long prose."
)
@dataclass
class AssistantResponse:
answer: str
used_llm: bool
sources: List[RetrievedDoc]
latency_s: float
model_id: str
debug_errors: List[str] = field(default_factory=list) # non-empty only when used_llm is False due to failures
def _extractive_fallback(query: str, sources: List[RetrievedDoc]) -> str:
"""Deterministic answer used when no HF token / API call fails, so the
Space always returns something useful instead of an error."""
if not sources:
return (
"I don't have grounded context for that yet. Try asking about "
"inventory, order status, equipment maintenance, AGV routing, "
"picking strategy, safety incidents, or general warehouse "
"automation concepts."
)
lead = sources[0]
bullets = "\n".join(f"- **{s.title}**: {s.text}" for s in sources)
return (
f"(Showing retrieved knowledge instead of an LLM-generated answer -- "
f"see the diagnostics below.)\n\n"
f"Based on **{lead.title}**, here's the relevant information:\n\n{bullets}"
)
def answer_query(
query: str,
retriever: KBRetriever,
k: int = 2,
max_tokens: int = 350,
) -> AssistantResponse:
start = time.time()
sources = retriever.retrieve(query, k=k)
context_block = "\n\n".join(f"[{s.title}]\n{s.text}" for s in sources)
hf_token = _get_hf_token()
if not hf_token:
answer = _extractive_fallback(query, sources)
return AssistantResponse(
answer=answer,
used_llm=False,
sources=sources,
latency_s=time.time() - start,
model_id="extractive-fallback",
debug_errors=[
"No HF token secret found. Checked env vars: "
+ ", ".join(TOKEN_ENV_VAR_CANDIDATES)
+ ". Set one of these as a Space secret (Settings -> Variables and secrets)."
],
)
try:
from huggingface_hub import InferenceClient
except ImportError as e:
answer = _extractive_fallback(query, sources)
return AssistantResponse(
answer=answer, used_llm=False, sources=sources,
latency_s=time.time() - start, model_id="extractive-fallback",
debug_errors=[f"huggingface_hub not importable: {e}"],
)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"CONTEXT:\n{context_block}\n\nQUESTION: {query}"},
]
errors = []
for candidate in MODEL_CANDIDATES:
try:
# provider="auto" (the huggingface_hub default) lets HF's Inference
# Providers router pick whichever backend (hf-inference, Together,
# Fireworks, Novita, SambaNova, etc.) actually serves this specific
# model -- hardcoding a single provider caused "model not supported
# by provider X" errors for models hosted elsewhere.
client = InferenceClient(model=candidate, token=hf_token, provider="auto")
completion = client.chat_completion(messages=messages, max_tokens=max_tokens, temperature=0.3)
text = completion.choices[0].message.content
if text and text.strip():
return AssistantResponse(
answer=text,
used_llm=True,
sources=sources,
latency_s=time.time() - start,
model_id=candidate,
)
errors.append(f"{candidate}: empty response")
except Exception as e: # noqa: BLE001 -- try the next candidate model
errors.append(f"{candidate}: {type(e).__name__}: {e}")
# All candidates failed -- fall back, but surface the real errors so the
# deployment is debuggable directly from the UI.
answer = _extractive_fallback(query, sources)
return AssistantResponse(
answer=answer,
used_llm=False,
sources=sources,
latency_s=time.time() - start,
model_id="extractive-fallback",
debug_errors=errors,
)
def test_connection(retriever: Optional[KBRetriever] = None) -> AssistantResponse:
"""Runs a single canned query through the full pipeline -- used by the
'Test LLM connection' diagnostics button in the app."""
retriever = retriever or KBRetriever()
return answer_query("What is a WMS?", retriever, k=1, max_tokens=60)
|