File size: 7,886 Bytes
bc40e09
adee44c
85f31d0
96e7641
adee44c
85f31d0
 
 
adee44c
85f31d0
 
 
 
 
bc40e09
 
85f31d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc40e09
 
85f31d0
 
 
 
 
 
 
 
adee44c
 
bc40e09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adee44c
85f31d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc40e09
85f31d0
 
bc40e09
 
 
85f31d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adee44c
 
85f31d0
 
adee44c
 
85f31d0
 
 
 
 
 
 
 
 
bc40e09
 
 
 
 
 
 
 
 
 
 
 
 
 
85f31d0
bc40e09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85f31d0
bc40e09
85f31d0
 
 
bc40e09
 
 
85f31d0
bc40e09
85f31d0
 
bc40e09
 
 
 
 
 
 
 
 
 
 
 
 
 
85f31d0
 
 
 
 
 
 
 
 
 
 
 
 
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
"""Northwind Ops POC chat — Hugging Face Space (app.py)."""

from __future__ import annotations

import os
from functools import lru_cache
from typing import Any

import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

try:
    import spaces
except ImportError:  # CPU Space / local without ZeroGPU

    class _SpacesShim:
        @staticmethod
        def GPU(fn=None, **_kwargs):
            if fn is None:
                return lambda f: f
            return fn

    spaces = _SpacesShim()  # type: ignore

MODEL_ID = os.environ.get("NORTHWIND_MODEL_ID", "UnaverageTech411/northwind-ops")
SYSTEM = os.environ.get(
    "NORTHWIND_SYSTEM",
    (
        "You are Northwind Ops Assistant for Northwind Traders — the showcase company model "
        "from the Arriella custom-model factory. For Northwind IT, HR, and finance procedures, "
        "answer from documented company knowledge and name the official tool (ServiceNow, Concur, Workday). "
        "Give crisp step paths (for example ServiceNow → IT → VPN Token Reset with Northwind SSO). "
        "For ordinary world knowledge, math, spelling, and general chat, answer normally and concisely. "
        "Only say a fact is not documented when it is a Northwind-internal detail missing from training "
        "(salary bands, PTO balances, unpublished policies). Never say not documented for VPN, Concur, "
        "or Workday procedures that are in your training. Never refuse common general-knowledge questions."
    ),
)

# (button label, prompt) — training-data / gate probes
EXAMPLE_PROMPTS: list[tuple[str, str]] = [
    ("VPN reset", "How do I reset my VPN token at Northwind in ServiceNow?"),
    ("VPN by email?", "Can I reset my Northwind VPN token by emailing IT?"),
    ("Expense / Concur", "How do I submit an expense report in Concur?"),
    ("PTO / Workday", "Where do I check PTO in Workday?"),
    ("Salary band", "What is my exact salary band code?"),
    ("SaaS access", "Where do I request SaaS access?"),
    ("Who is CTO?", "Who is the Northwind CTO?"),
    ("Capital of France", "What is the capital of France?"),
]

CSS = """
.gradio-container {
  max-width: 920px !important;
  margin: auto;
  font-family: "IBM Plex Sans", "Segoe UI", sans-serif !important;
}
footer { display: none !important; }
#poc-title h1 {
  font-family: "Syne", "Arial Narrow", sans-serif !important;
  letter-spacing: -0.02em;
  margin-bottom: 0.25rem !important;
}
#poc-lede {
  color: #9aabbd !important;
  font-size: 0.98rem !important;
  line-height: 1.5 !important;
  margin-top: 0 !important;
}
#chatbot { border: 2px solid rgba(244,247,251,0.22) !important; }
#probe-row button, #probe-row-2 button {
  font-size: 0.82rem !important;
}
"""


@lru_cache(maxsize=1)
def _load() -> tuple[Any, Any]:
    tok = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
    dtype = torch.float16 if torch.cuda.is_available() else torch.float32
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID,
        torch_dtype=dtype,
        device_map="auto" if torch.cuda.is_available() else None,
        trust_remote_code=True,
    )
    if not torch.cuda.is_available():
        model = model.to("cpu")
    model.eval()
    return tok, model


def _history_to_messages(history: list[dict[str, str]]) -> list[dict[str, str]]:
    messages = [{"role": "system", "content": SYSTEM}]
    for turn in history or []:
        role = turn.get("role")
        content = (turn.get("content") or "").strip()
        if role in {"user", "assistant"} and content:
            messages.append({"role": role, "content": content})
    return messages


@spaces.GPU(duration=90)
def _generate(messages: list[dict[str, str]], max_new_tokens: int = 220) -> str:
    tok, model = _load()
    prompt = tok.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=True
    )
    inputs = tok(prompt, return_tensors="pt")
    device = next(model.parameters()).device
    inputs = {k: v.to(device) for k, v in inputs.items()}
    with torch.inference_mode():
        out = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            do_sample=False,
            pad_token_id=tok.eos_token_id,
        )
    gen = out[0, inputs["input_ids"].shape[-1] :]
    return tok.decode(gen, skip_special_tokens=True).strip()


def respond(message: str, history: list[dict[str, str]]):
    text = (message or "").strip()
    if not text:
        return history, ""
    history = list(history or [])
    history.append({"role": "user", "content": text})
    try:
        reply = _generate(_history_to_messages(history))
    except Exception as exc:  # noqa: BLE001
        reply = f"(Demo error: {exc})"
    history.append({"role": "assistant", "content": reply or "(empty reply)"})
    return history, ""


def use_example(prompt: str, history: list[dict[str, str]]):
    return respond(prompt, history)


def clear_chat():
    return [], ""


try:
    _load()
except Exception:
    pass

theme = gr.themes.Soft(
    primary_hue=gr.themes.colors.amber,
    secondary_hue=gr.themes.colors.emerald,
    neutral_hue=gr.themes.colors.slate,
).set(
    body_background_fill="#06090d",
    body_background_fill_dark="#06090d",
    block_background_fill="#111922",
    block_background_fill_dark="#111922",
    body_text_color="#f4f7fb",
    body_text_color_dark="#f4f7fb",
    border_color_primary="rgba(244,247,251,0.22)",
    border_color_primary_dark="rgba(244,247,251,0.22)",
)

with gr.Blocks(title="Northwind Ops Assistant", theme=theme, css=CSS) as demo:
    with gr.Column(elem_id="poc-title"):
        gr.Markdown("# Northwind Ops — try the POC")
        gr.Markdown(
            "1B company model: ServiceNow / Concur / Workday procedures, "
            "Pile-hardened, Heretic ×2, openness-gated. "
            "Tap a probe from the training set, or type your own. Clear anytime.",
            elem_id="poc-lede",
        )

    chatbot = gr.Chatbot(
        label="Northwind Ops",
        height=440,
        type="messages",
        elem_id="chatbot",
        value=[
            {
                "role": "assistant",
                "content": (
                    "Ask a company procedure and a general fact — that contrast is the demo. "
                    "Try **VPN reset** or **Capital of France** below."
                ),
            }
        ],
    )

    with gr.Row():
        msg = gr.Textbox(
            label="Message",
            show_label=False,
            placeholder="How do I reset my VPN token at Northwind in ServiceNow?",
            scale=5,
            autofocus=True,
            container=False,
        )
        send = gr.Button("Send", variant="primary", scale=1)
        clear_btn = gr.Button("Clear chat", variant="secondary", scale=1)

    gr.Markdown("**Training-data probes**")
    with gr.Row(elem_id="probe-row"):
        btns_a = [gr.Button(label, size="sm") for label, _ in EXAMPLE_PROMPTS[:4]]
    with gr.Row(elem_id="probe-row-2"):
        btns_b = [gr.Button(label, size="sm") for label, _ in EXAMPLE_PROMPTS[4:]]
    example_btns = btns_a + btns_b

    gr.Markdown(
        "Weights: [`UnaverageTech411/northwind-ops`](https://huggingface.co/UnaverageTech411/northwind-ops) · "
        "Local: `ollama run northwind-ops` · "
        "Arriella custom-model factory POC"
    )

    send.click(respond, inputs=[msg, chatbot], outputs=[chatbot, msg])
    msg.submit(respond, inputs=[msg, chatbot], outputs=[chatbot, msg])
    clear_btn.click(clear_chat, outputs=[chatbot, msg])
    for btn, (_label, prompt) in zip(example_btns, EXAMPLE_PROMPTS):
        btn.click(
            lambda hist, p=prompt: use_example(p, hist),
            inputs=[chatbot],
            outputs=[chatbot, msg],
        )

if __name__ == "__main__":
    demo.queue(default_concurrency_limit=1).launch()