Spaces:
Running on Zero
Running on Zero
| """ | |
| 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. | |
| * The model name is configurable via `LLM_MODEL_ID` (defaults to a small, | |
| fast, freely-hostable instruct model). | |
| """ | |
| import os | |
| import time | |
| from dataclasses import dataclass | |
| from typing import List | |
| from src.retriever import KBRetriever, RetrievedDoc | |
| DEFAULT_MODEL_ID = os.environ.get("LLM_MODEL_ID", "Qwen/Qwen2.5-7B-Instruct") | |
| 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." | |
| ) | |
| class AssistantResponse: | |
| answer: str | |
| used_llm: bool | |
| sources: List[RetrievedDoc] | |
| latency_s: float | |
| model_id: str | |
| 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"(Offline / no LLM API key configured -- showing retrieved " | |
| f"knowledge instead of a generated answer.)\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, | |
| model_id: str = DEFAULT_MODEL_ID, | |
| 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 = os.environ.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", | |
| ) | |
| try: | |
| from huggingface_hub import InferenceClient | |
| client = InferenceClient(model=model_id, token=hf_token) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| { | |
| "role": "user", | |
| "content": f"CONTEXT:\n{context_block}\n\nQUESTION: {query}", | |
| }, | |
| ] | |
| completion = client.chat_completion(messages=messages, max_tokens=max_tokens, temperature=0.3) | |
| text = completion.choices[0].message.content | |
| return AssistantResponse( | |
| answer=text, | |
| used_llm=True, | |
| sources=sources, | |
| latency_s=time.time() - start, | |
| model_id=model_id, | |
| ) | |
| except Exception as e: # noqa: BLE001 -- deliberately broad: any API/network issue -> fallback | |
| answer = _extractive_fallback(query, sources) | |
| answer += f"\n\n_(LLM call failed: {type(e).__name__}. Showing retrieval-only answer.)_" | |
| return AssistantResponse( | |
| answer=answer, | |
| used_llm=False, | |
| sources=sources, | |
| latency_s=time.time() - start, | |
| model_id="extractive-fallback", | |
| ) | |